前端项目优化-7.h5离线ZIP包

Liveme离线包方案详细实现

方案概述

为优化前端项目加载速度、节省网络流量并提升用户体验,我们设计了一套完整的H5离线ZIP包方案。该方案通过预下载热门页面离线包、普通离线包按需下载更新以及主动更新机制,确保用户在不同网络环境下都能快速访问页面。

功能

  • 预下载:提前下载热门页面的离线包,以便用户在需要时能够快速访问。
  • 普通离线包:用户访问页面后,下载并更新离线包,以便下次使用。
  • 主动更新:定期检查并更新离线包 主要为了增强版本控制

实现

前端-打包

  1. 前端 维护 构建 zip 配置文件
  2. 前端 打包生成 zip 包 【 会注入 window.KEWLWebZip 变量 (用于配合版本检查)】
  3. 前端编写 离线包管理脚本 webzip.js,注入到 需要 离线包 的 页面 中。【版本检查、通知客户端 删除、更新、下载等操作】
  4. 构建 基于 构建 zip 配置文件打包结果(配置中的项目是否发生了变更)* 生成 /webzip/zipVersion.json 配置文件。
  5. 发布服务

客户端-实现

  1. 客户端 提供 维护 zip 包 的方法 (包括下载、删除)
  2. 客户端 在打开h5页面前需要 根据本地的 /webzip/zipVersion.json 配置文件 判断离线包是否存在, 存在则 加载 离线包,否则 加载 在线页面。
  3. 客户端在 打开zip 时候 需要 拼接 WEBHOST=当前包对应的h5域名 地址
  4. 客户端 启动后需要打开一个 隐藏的页面,用于 主动更新 离线包[页面执行完毕会主动关闭]。

使用阶段

预下载

  • 客户端 启动后,会下载 /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]);
// 追加WEBHOST参数
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)) {
// http或者https协议链接
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) {
// 移除zip包
window.KEWLApp.deleteWebZipRes(prex);
// 替换为远程地址,展示最新内容
window.location.replace(window.location2.href);
} else if (latestVer > ver) {
// 通知客户端下载最新zip包
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
// iframe使用示例
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);