Appearance
📖 实现方案
在线服务

项目研发时,我们不约而同地想到在线服务的加载形式,即客户端使用域名的形式访问提供加载资源的 HTTP 服务器。
在线服务与 Web 应用程序别无二致,使得我们有更多的时间投入到设计应用生命周期和程序 API 中去。
可是随着项目上线若干个月,在线服务存在的缺陷便暴露出来:
- 没有预加载:首次打开的体验很差,所有文件都要从网络请求
- 缓存不可控:缓存的大小和策略由系统 webview 控制。缓存的清理逻辑不可控,往往加载缓存几张图片后,重要的 HTML/JS/CSS 缓存就被清除了
- 网络劫持:页面被运营商或其他第三方劫持,将长时间缓存劫持的页面,导致整个功能失效
离线加载

离线加载方式能较好的解决在线服务存在的若干问题。
离线加载方式把功能模块的页面和资源打包在客户端中,Webview 组件加载本地资源文件,避免了联网因素的产生。 亦可将页面文件压缩成离线包,在自定义时机把离线包下载下来,做解压、校验等工作,达到在线更新/热更新的效果。
离线包:前端项目构建产出的文件打包成 zip 格式,并以 xxx_${verison}.zip 命名方式交付至客户端,存放在 App 项目内作为资源引用,称为离线包。
下载包:与离线包的产出相似,下载包定义为某个离线包的迭代版本,文件将存放于服务器或 CDN 上,便于客户端需要时下载。
应用生命周期
得益于 DSBridge 框架,开发者无需理解原生与 JavaScript 通信的原理和机制。但要理解 DSBridge 提供的 API 如何定义、何时调用问题。

自定义字体
在原生应用和前端中使用自定义字体都有经典的解决方案。在混合应用中,前端字体赖于 CSS3 @font-face 特性。
在原生开发时,字体文件已打包进应用。前端所需的字体从原生应用中获取,拦截浏览器 HTTP 请求,并返回字体文件。实现细节见🔧 实现细节-自定义字体
原理很简单,但会存在一些问题:
加载过程
字体效果有加载过程,字体效果会从无到有。字体文件是通过 HTTP 请求获得,在响应头中加入缓存指令,避免频繁加载文件。
体积过大
文字的使用涉及各行各业,字体出版商在字体设计时,会参考国家标准 GB 18030 规范《信息技术 中文编码字符集》,决定设计多少字符。
上述标准囊括了所有汉字,还有如藏、蒙古、傣、彝、朝鲜、维吾尔文等。事实上,国人在日常书信和交流当中,用到的汉字数也就 3000 ~ 7000个。
直接使用出版商提供的原字体,文件体积直接拉满,造成安装包体积巨大,同时拖慢页面打开速度。
因为原字体是按照标准制作的,其中包含了非常多日常用字和互联网行业中用不到或不常用的字符,这便造成了浪费。
所以,为降低安装包的大小,加快页面打开的速度,字体应该结合实际场景优化,去掉暂时用不到的字符集,生成符合使用场景的字体。
其中,项目中用的思源宋体。它就是基于 GB 18030 和《通用规范汉字表》 进行制作的,光中文汉字就 31,027 个。
图片的离线加载
Newsstand APP 正文图片离线化与拦截加载策略。
为了提升用户在弱网或无网环境下的阅读体验,Newsstand APP 需要支持文章离线查看功能。
在离线包方案中,文章的 HTML 文本及相关媒体资产(图片)会被提前打包下载至客户端本地。
需求分析
离线查看功能时,文章中的图片资产需要提前随离线包下载至客户端本地。为了防止 WebView 容器在离线状态下仍然向线上发起网络请求,同时避免因无网导致大面积图片碎裂,前端必须在渲染前拦截并重写 HTML 中的图片地址。
技术选型和局限性
iOS 的 WKURLSchemeHandler 严禁注册/拦截标准 http 或 https 协议;
Web 视图加载自定义私有协议(如 nfzmscheme://)时,WebKit 内核会触发同源策略限制,导致请求被直接 Block 或引发 Canvas 污染。
实现细节见🔧 实现细节-图片的离线加载
版本管理
该项目运行机理是如网页应用一致: APP 内嵌浏览请求网页服务器,请求资源到本地,再完成渲染。
与传统项目不同的是,选择绑定版本的方式进行部署,主要是因为混合应用方案未经过迭代,可能存在不明显的缺陷,避免在代码层兼容多种客户端,应采用版本一对一的方式进行部署。
这样做的好处是,保证版本正常运行同时,可迅速地对异常版本修复,将异常等级到最低。
我们使用浏览器中的 User-Agent 快速识别客户端版本号,实现细节见🔧 实现细节-User-Agent
版本迭代
每个客户端版本,对应混合应用项目版本。如客户端 7.0.0 对应混合应用 1.0.0,某个版本出现较大的问题,我们可针对具体版本进行修复,避免对其他版本造成影响
| 客户端平台 | 客户端版本 | 混合应用版本 | 备注 |
|---|---|---|---|
| Android | 7.0.0 | 1.0.0 | |
| iOS | 7.0.0 | 1.0.0 | |
| Android | 7.0.0 | 1.0.1 | 修复小问题,更新小版本号 1.0.1 ① |
| iOS | 7.0.0/7.0.1 | 1.0.1 | |
| Android | 7.0.1 | 1.1.0 | 跟随客户端功能迭代,更新中版本号 1.1.0 ② |
| iOS | 7.0.2 | 1.1.0 | |
| iOS | 7.1.0 | 1.3.0 | 跟随客户端功能迭代,更新中版本号 1.3.0 |
修复混合应用项目 ①
经过修复后的混合应用版本号从 1.0.0 更新至 1.0.1 ,产出的代码对应客户端 7.0.0 7.0.1 版本
更新小版本号时,根据数月内版本活跃量,筛选哪些版本适用本次升级迭代
发布新功能 ②
凡新增与客户端相关的功能,更新中版本号, 如1.1.0 。
并确定各平台客户端版本号 如: Android7.0.1、 iOS7.0.2 ,产出的代码对应这些版本即可
版本发布
发布新功能版本
客户端因自身功能迭代,创建了新的版本号,如 7.0.1 时,应重新构建并发布一个稳定的混合式代码与其相对应
- 前端构建好混合应用
1.0.0版本代码 - 将前端代码提交至
1.0.x文件夹中,入口文件为index.html - 添加 Nginx User Agent 判断配置文件,使得 Android
7.0.0请求能返回目录1.0.x内容。iOS 端也一样
WARNING
关于 iOS 平台审核的原因,混合应用会先部署预发布版本,用于通过审核后切换至对应的正式版。
由于客户端迭代版本比较频繁,可能经常部署对应版本,对应过审问题,服务器上部署了默认正式版和默认审核版本,运维可自由地调整新版本对应的项目
默认正式版部署混合项目正式版本在文件夹
default中默认审核版本部署混合项目iOS预发布版本在文件夹
default_pre中
Nginx 实现细节见 🔧 实现细节-Nginx
发布修复版本
如要修复 Android 客户端 7.0.0 版本内容
- 前端修改好项目代码,Git 打上标记
1.0.1版本 - 构建
1.0.1版本上传并覆盖1.0.x文件夹内容
项目部署
因技术和人员限制,并未采用 CI 的方式进行部署。
目前将构建好的文件(按照版本目录)提交至 Git,在部署机上拉取文件。根据版本配置好 Nginx Config,完成部署。