[toc]
混合式应用
如今手机应用的开发已经从客户端发展到跨平台,原生开发不再是一枝独秀。前端领域也尝试分一杯羹,更有后起的 Flutter 。
HybridApp 混合应用指的是,同时使用网页前端技术和原生技术开发的 App,通常由网页负责部分界面开发和业务逻辑,原生提供给前端调用的函数/接口,两者以 WebView 作为媒介建立通信,既拥有 Web 开发的速度又是,又能拥有强大的原生能力。
典型案例
是什么场景适合到混合式应用?
因为产品前期方案设计存在不足,没有合理地规范录入内容的结构,各种乱七八糟内容以 HTML 的形式保存到数据库中。久而久之,客户端难以解析这些结构,导致内容显示空白,甚至应用崩溃。
在内容管理系统中,文本和多媒体组成的内容,一般会以 HTML 代码的形式存储在数据库中。而客户端应用需要解析 HTML,转换成控件。
为了解决该问题,成本较低的方案是改用 Web 技术来展示这些文本和多媒体内容。
它能解决:
- 客户端难以解析文本结构的问题。凭借 Web 前端天然的优势,浏览器可直接解析 HTML 代码
- 发现问题或错误时,可随时修复更新,无需发布新版本客户端,减少因功能失效后的损失。
那么由 Web 实现主要的业务场景,也会存在技术难点:
- 客户端与 Web 之间函数互相调用问题
- Web 应用在客户端 WebView 组件的生命周期问题
- 代码版本维护与发布更新问题
数据影响深远。数据的结构影响到了选取开发方案的结果。Editor.js 给提供了描述内容的方案,将内容描述成简洁的数据,易于清洗、扩展和集成的内容。
原生与 Web 相互调用
Webview 组件提供了功能上相似的 API。Android 平台拥有 JavascriptInterface API,而 iOS 则是 WKWebView API,但两端的调用方式不一致,必定导致一方的取舍。
引入 DSbridge 这个库来保证不同平台与 Web 相互调用功能的统一。以 Android 为例,原生调用前端 API的声明方式:
在 Javascript 中定义
addValue
APIdsBridge.registerAsyn('addValue',function(l,r){ return l+r; })
在 Java 中调用
addValue
APIdwebView.callHandler("addValue",new Object[]{3,4},new OnReturnValue<Integer>(){ @Override public void onValue(Integer retValue) { Log.d("jsbridge","call succeed,return value is "+retValue); } });
更多用例见 DSbridge 中文文档
握手与数据更新
从程序设计角度看,代码运行会有初始化、创建、执行、暂停、结束、销毁等生命周期。将对应的逻辑放在不同的生命周期中运行,才能表现出软件的有序性,易维护性,提高程序的运行效率。
在混合应用中,同样需要生命周期,保证了客户端与 Web 间的调用是有序的。双方传输数据前就必须建立连接,并要使每一方能够确知对方的存在。
利用 DSbridge 定义名为 onWebViewCreated 的 API,确知双方都准备好进入下一生命周期。
定义 onWebViewCreated API
Android:
@JavascriptInterface
public void onWebViewCreated(Object msg, CompletionHandler<Boolean> handler) {
handler.complete(true); //return true value to front end
}
Javascript:
const methodName = "onWebViewCreated";
dsBridge.call(methodName, (res) => {
if(!!res) {
// get true from client
}
});
定义 notifyWebViewUpdated API
在本文开头,混合式应用主要是解决展示富文本的业务需求。那么第一握手成功后,Web 应用即可主动调用相关 API,进入业务流程。
假设遇到用户信息更新,客户端应该通过事件通知的形式,告知 Web 应用主动获取最新的用户信息,更新内部数据和页面显示内容。
这种情况就像你收到一条包裹短信,其内容告知包裹所在的门店,你要赶在打样前到门店取回包裹
notifyWebViewUpdated 传入:
{
"name": "user"
}
Android:
JSONObject updateObj = new JSONObject();
try {
updateObj.put("name", "user");
} catch (JSONException e) {
e.printStackTrace();
}
dWebView.callHandler("notifyWebViewUpdated", new Object[]{ updateObj }, new OnReturnValue<Boolean>() {
@Override
public void onValue(Boolean retValue) { //callback from front end
showToast(retValue); //retVlue is true
}
});
Javascript:
dsBridge.register('notifyWebViewUpdated', (e) => {
if(e.name === 'user') {
//trigger get user API
}
return "true";
})
部署与维护
与多数 Web 应用一样,混合应用的 Web 项目同样需要部署在服务器或分发到 CDN。
版本管理
在混合项目中 Web 应用加载流程与浏览器是一致的,Webview 请求网页服务器,请求资源到本地,再完成渲染。
与传统项目不同的是,Web 应用和客户端版本之间是存在关系的,这种关系影响到部署。是因为混合应用方案未经过迭代,可能存在不明显的缺陷,避免在代码层兼容多种客户端,应采用版本一对一的方式进行部署。
某个Web 应用版本出现缺陷,针对具体版本进行修复并发布,避免了缺陷对其他客户端版本造成影响。这样做的好处是,保证版本正常运行同时,可迅速地对异常版本修复,将异常等级到最低。
版本迭代
每个项目都有其独立的版本号,但在混合式项目中,客户端与 Web 应用项目版本号将产生关联,并记录在案,方便我们在项目迭代和方案复盘时,提供有力的数据。
客户端平台 | 客户端版本 | Web 应用版本 | 备注 |
---|---|---|---|
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.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
。
并确定各平台客户端版本号 如: Android 7.0.1、 iOS 7.0.2 ,产出的代码对应这些版本即可
User-Agent
字符串 <应用名称>/<软件版本号>
会附在 User Agent 末尾,例如 7.0.0
版本 iPhone 客户端带有标识 iread/7.0.0
:
Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) iread/7.0
浏览器包含平台(iPhone/iPad/Android)标识,因此我们不需要再添加平台信息。
发布与部署
版本管理和迭代章节指出,不同项目版本将会分开部署,意味着每个对应版本的应用都是独立部署的,我们可以将部署内容分散在不同文件夹中,这里使用一个简单的配置展示独立部署效果。
D:\WWW\DEV.WEBSITE <----- root
├─1.1.x <----- $path_dir
│ index.html <----- project entry
│
├─1.2.x <----- $path_dir
│ index.html
│
├─1.3.x <----- $path_dir
│ index.html
│
└─default <----- default version for all
index.html
配合使用 Nginx map 和虚拟主机的特性,获取 HTTP Header 中的 User-Agent 后将虚拟主机的根目录映射至不同目录。
map $http_user_agent $path_dir {
~*iread/7.0.0 "1.1.x";
~*iread/7.0.1 "1.1.x";
~*iread/7.1.0 "1.2.x";
~*iread/7.2.0 "1.3.x";
default "default";
}
server {
listen 80;
server_name dev.website;
root "D:/www/dev.website/$path_dir/";
location / {
index index.html index.htm index.php;
}
}
也可以加入平台标识的判断