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,完成部署。