Mather

We create our own demons.

混合式应用 从理论到实践

默认分类 0 评

[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 API
dsBridge.registerAsyn('addValue',function(l,r){
    return l+r;
})
  • 在 Java 中调用 addValue API
dwebView.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 应用版本备注
Android7.0.01.0.0发布初始版本
iOS7.0.01.0.0-
Android7.0.01.0.1修复小问题,更新小版本号 1.0.1
iOS7.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

并确定各平台客户端版本号 如: 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;
    }
}


发表评论
撰写评论