Skip to content

📖 实现方案 ​

在线服务 ​

loading-with-web-server

项目研发时,我们不约而同地想到在线服务的加载形式,即客户端使用域名的形式访问提供加载资源的 HTTP 服务器。

在线服务与 Web 应用程序别无二致,使得我们有更多的时间投入到设计应用生命周期和程序 API 中去。

可是随着项目上线若干个月,在线服务存在的缺陷便暴露出来:

  • 没有预加载:首次打开的体验很差,所有文件都要从网络请求
  • 缓存不可控:缓存的大小和策略由系统 webview 控制。缓存的清理逻辑不可控,往往加载缓存几张图片后,重要的 HTML/JS/CSS 缓存就被清除了
  • 网络劫持:页面被运营商或其他第三方劫持,将长时间缓存劫持的页面,导致整个功能失效

离线加载 ​

loading-with-local

离线加载方式能较好的解决在线服务存在的若干问题。

离线加载方式把功能模块的页面和资源打包在客户端中,Webview 组件加载本地资源文件,避免了联网因素的产生。 亦可将页面文件压缩成离线包,在自定义时机把离线包下载下来,做解压、校验等工作,达到在线更新/热更新的效果。

离线包:前端项目构建产出的文件打包成 zip 格式,并以 xxx_${verison}.zip 命名方式交付至客户端,存放在 App 项目内作为资源引用,称为离线包。

下载包:与离线包的产出相似,下载包定义为某个离线包的迭代版本,文件将存放于服务器或 CDN 上,便于客户端需要时下载。

应用生命周期 ​

得益于 DSBridge 框架,开发者无需理解原生与 JavaScript 通信的原理和机制。但要理解 DSBridge 提供的 API 如何定义、何时调用问题。

lifecycle2

自定义字体 ​

在原生应用和前端中使用自定义字体都有经典的解决方案。在混合应用中,前端字体赖于 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,某个版本出现较大的问题,我们可针对具体版本进行修复,避免对其他版本造成影响

客户端平台客户端版本混合应用版本备注
Android7.0.01.0.0
iOS7.0.01.0.0
Android7.0.01.0.1修复小问题,更新小版本号 1.0.1 ①
iOS7.0.0/7.0.11.0.1
Android7.0.11.1.0跟随客户端功能迭代,更新中版本号 1.1.0 ②
iOS7.0.21.1.0
iOS7.1.01.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. 前端构建好混合应用 1.0.0 版本代码
  2. 将前端代码提交至 1.0.x 文件夹中,入口文件为 index.html
  3. 添加 Nginx User Agent 判断配置文件,使得 Android 7.0.0 请求能返回目录 1.0.x 内容。iOS 端也一样

WARNING

关于 iOS 平台审核的原因,混合应用会先部署预发布版本,用于通过审核后切换至对应的正式版。

由于客户端迭代版本比较频繁,可能经常部署对应版本,对应过审问题,服务器上部署了默认正式版和默认审核版本,运维可自由地调整新版本对应的项目

  • 默认正式版部署混合项目正式版本在文件夹 default 中

  • 默认审核版本部署混合项目iOS预发布版本在文件夹 default_pre 中

Nginx 实现细节见 🔧 实现细节-Nginx

发布修复版本 ​

如要修复 Android 客户端 7.0.0 版本内容

  1. 前端修改好项目代码,Git 打上标记 1.0.1 版本
  2. 构建 1.0.1 版本上传并覆盖 1.0.x 文件夹内容

项目部署 ​

因技术和人员限制,并未采用 CI 的方式进行部署。

目前将构建好的文件(按照版本目录)提交至 Git,在部署机上拉取文件。根据版本配置好 Nginx Config,完成部署。