Liveme离线包方案详细实现
方案概述
为优化前端项目加载速度、节省网络流量并提升用户体验,我们设计了一套完整的H5离线ZIP包方案。该方案通过预下载热门页面离线包、普通离线包按需下载更新以及主动更新机制,确保用户在不同网络环境下都能快速访问页面。
功能
- 预下载:提前下载热门页面的离线包,以便用户在需要时能够快速访问。
- 普通离线包:用户访问页面后,下载并更新离线包,以便下次使用。
- 主动更新:定期检查并更新离线包 主要为了增强版本控制。
实现
前端-打包
- 前端 维护 构建 zip 配置文件
- 前端 打包生成 zip 包 【 会注入
window.KEWLWebZip
变量 (用于配合版本检查)】 - 前端编写
离线包管理脚本 webzip.js
,注入到 需要 离线包 的 页面 中。【版本检查、通知客户端 删除、更新、下载等操作】 - 构建 基于 构建 zip 配置文件 和打包结果(配置中的项目是否发生了变更)* 生成
/webzip/zipVersion.json
配置文件。 - 发布服务
客户端-实现
- 客户端 提供 维护 zip 包 的方法 (包括下载、删除)
- 客户端 在打开h5页面前需要 根据本地的
/webzip/zipVersion.json
配置文件 判断离线包是否存在, 存在则 加载 离线包,否则 加载 在线页面。 - 客户端在 打开zip 时候 需要 拼接
WEBHOST=当前包对应的h5域名
地址 - 客户端 启动后需要打开一个 隐藏的页面,用于 主动更新 离线包[页面执行完毕会主动关闭]。
使用阶段
预下载
- 客户端 启动后,会下载
/webzip/zipVersion.json
配置文件。 - 客户端 检测到 配置文件 更新 进行 替换。
- 空闲时间 扫描 预下载(prefetch)的离线包的下载,zip版本发生变化, 进行下载。
普通离线包
- 用户访问在线页面后, 前端根据 注入的
zip 变量
执行 离线包管理脚本 webzip.js
完成 版本检查、下载,更新 - 下载在打开 该页面, 离线包 存在,客户端会优先使用 离线包。
主动更新
- 由于客户端 每次启动都会 打开这个
/webzip/versionMananger.html
页面。 - 前端 通过 重新部署 && 刷新CDN 方式, 就可以 更新页面,从而 主动更新 离线包。
离线包生成与配置实践
离线包生成流程
1. 打包配置文件
- prefetch 预加载zip
- zipList 离线包列表
- removeList 移除zip
2. 打包特殊处理
- 引入
离线包管理脚本 webzip.js
脚本,用于版本检查、加载 离线包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| import updateLocation2 from './location2';
const KEWLWebZip = window.KEWLWebZip || {}; export const isWebZip = ['content:', 'file:'].includes(window.location.protocol); KEWLWebZip.isWebZip = isWebZip;
KEWLWebZip.jump = ({ url, isReplace }) => { let href = url; if (window.KEWLWebZip.isWebZip) { if (new RegExp(`/app/${window.KEWLWebZip.name}`).test(url)) { href = window.location.pathname.replace(/(\/dist\/.*)|\/[^/]+(?!.)/g, url.split(`/app/${window.KEWLWebZip.name}`)[1]); if (window.KEWLWebZip.WEBHOST && href.indexOf('WEBHOST') == -1) { href += `${href.indexOf('?') > -1 ? '&' : '?'}WEBHOST=${encodeURIComponent(window.KEWLWebZip.WEBHOST)}`; } } else if (/^\/\//g.test(url)) { href = `${window.location2.protocol}${url}`; } else if (/^\//g.test(url)) { href = `${window.location2.origin}${url}`; } else if (/https:\/\/|http:\/\/|^\/\//g.test(url)) { href = url; } } if (isReplace) { window.location.replace(href); } else { window.location.href = href; } };
let zipVersions;
const fetchZipVersion = () => new Promise((resolve) => { const t = new Date().getTime(); window.KEWLHttp && window.KEWLHttp.get(`${window.location2.origin}/app/webzip/zipVersion.json?t=${t}`).then((res) => { if (res.status == 200) { if (res.data.zips) { zipVersions = res.data.zips; resolve(zipVersions); } } }); });
const checkZipVersion = (name, version, callback) => { if (zipVersions) { callback(name, version, zipVersions[name]); } else { fetchZipVersion().then(() => { callback(name, version, zipVersions[name]); }); } };
const findSelfZip = () => { let prex; let name; let version; if (window.KEWLWebZip.isWebZip) { ({ prex, name, version } = window.KEWLWebZip); if (!prex) { prex = `/app/${name}`; } } else { const result = /\/app\/(.*?)\//g.exec(window.location2.href); if (result && Array.isArray(result) && result.length > 1) { [prex, name] = result; } else { return; } version = 0; } checkZipVersion(name, version, (_, ver, latestVer) => { if (!latestVer && window.KEWLWebZip.isWebZip) { window.KEWLApp.deleteWebZipRes(prex); window.location.replace(window.location2.href); } else if (latestVer > ver) { window.KEWLApp.downloadWebZipRes(name, latestVer); if (window.KEWLWebZip.isWebZip) { window.location.replace(window.location2.href); } } }); };
KEWLWebZip.findSelfZip = findSelfZip; KEWLWebZip.fetchZipVersion = fetchZipVersion; KEWLWebZip.checkZipVersion = checkZipVersion;
window.KEWLWebZip = KEWLWebZip;
export default KEWLWebZip;
|
- zip对应的h5打包中,会注入
window.KEWLWebZip
变量 (用于配合版本检查)
1 2 3 4 5 6
| window.KEWLWebZip = { prex: "/app/${dir}", remotePath: "/app/${dir}${dest}", name: "${dir}", version: "${version}" }
|
3. 收集、打包资源
- 对比md5值 是否需要进行 离线包 生成
- 通过脚本收集 页面中 所有的资源,包括 图片、css、js、html等。
- 将收集到的资源 进行打包,生成 zip 包。使用时戳作为 文件后缀,标识文件更新时间。
4. 根据配置 和 打包结果 更新 zipVersion.json
配置文件
/webzip/zipVersion.json
配置文件(用于客户端维护zip依据)
- 文件本身版本号
- 分类型记录: 预加载(提前下载)、普通加载。
- 记录 离线包的路径、离线包的版本号、更新时间。
1 2 3 4 5 6 7 8 9 10
| { "status": 200, "version": "20240613103717", "zips": { "rank-list": "20240827160843", "kingdom": "20240827160612", "grade": "20241014104248" }, "prefetch": [ { "prex": "/app/grade", "zipPath": "/app/grade/dist/grade_20241014104248.zip", "version": "4.6.30" }, { "prex": "/app/kingdom", "zipPath": "/app/kingdom/dist/kingdom_20240827160612.zip", "version": "4.6.30" } ], "remove": [] }
|
5. 发布服务
- 发布服务,将生成的zip包和配置文件上传到服务器。
- 注意 版本文件 跨域问题。
主动更新
- 前端 手动 刷新 方式: 增加 webzip/versionMananger.html 页面(用于主动更新)
- 用于前端通过 更新 html,调用客户端bridge实现 加载、删除 zip 包。
客户端 每次启动都会 隐藏的 打开这个页面。html 设置 缓存10分钟。
当然,如果客户端发现 这个版本的zip已经存在,则不会下载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <!DOCTYPE html> <html>
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> <script src="/app/js/dist/kewlglobal.js?v=202409051029" type="text/javascript"></script> <script> if (window.KEWLApp && window.KEWLApp.downloadWebZipRes) { window.KEWLApp.downloadWebZipRes("grade", "20241014104248"); window.KEWLApp.downloadWebZipRes("kingdom", "20240827160612"); } window.KEWLApp.closePage() </script> </head>
<body> </body>
</html>
|
问题处理与优化
多语言静态文件处理
问题:此前为了优化使用axios 延后加载多语言,但是zip后无法请求本地文件。
解决方案:改为全量打包不进行延后加载,确保所有语言资源在离线包中可用。
远程资源处理
问题:非本地的资源无法直接在离线环境下使用。
解决方案:对于远程资源,下载或以base64形式进行打包,确保所有依赖资源在离线包中完整。
script module兼容问题
问题:在content协议下,触发script module兼容问题(MIME类型错误)。
解决方案:将script标签的type改为兼容模式(Legacy)
location替换问题
问题:在离线包中直接使用window.location可能导致跳转异常。
解决方案:使用 window.location2
代替 window.location
,并提供配套方法 window.KEWLWebZip.jump
用于跳转。
神策数据上报
问题:需要区分不同离线包的数据上报。
解决方案:在原神策上报的url中拼接 zipVer=[当前zip包版本]
,用于区分不同离线包的数据。
iframe使用
问题:iframe的src需要正确解析。
解决方案:iframe的url必须使用 window.location2
拼接全路径后使用。
示例:
1 2 3 4 5
| const iframeUrl = `${window.location2.origin}/activity/2022/dist/nightClub/index.html?source=2`; const iframe = document.createElement('iframe'); iframe.src = iframeUrl; document.body.appendChild(iframe);
|