Hybrid APP架构设计思路

关于Hybrid模式开发app的好处,网络上已有不少文章阐述了,这里不展开。javascript

本文将从如下几个方面阐述Hybrid app架构设计的一些经验和思考。css

原文及讨论请到 github issuehtml

通信

做为一种跨语言开发模式,通信层是Hybrid架构首先应该考虑和设计的,日后全部的逻辑都是基于通信层展开。前端

Native(以Android为例)和H5通信,基本原理:java

  • Android调用H5:经过webview类的loadUrl方法能够直接执行js代码,相似浏览器地址栏输入一段js同样的效果git

    webview.loadUrl("javascript: alert('hello world')");
  • H5调用Android:webview能够拦截H5发起的任意url请求,webview经过约定的规则对拦截到的url进行处理(消费),便可实现H5调用Androidgithub

    var ifm = document.createElement('iframe');
    ifm.src = 'jsbridge://namespace.method?[...args]';

JSBridge即咱们一般说的桥协议,基本的通信原理很简单,接下来就是桥协议具体实现。web

P.S:注册私有协议的作法很常见,咱们常常遇到的在网页里拉起一个系统app就是采用私有协议实现的。app在安装完成以后会注册私有协议到OS,浏览器发现自身不能识别的协议(http、https、file等)时,会将连接抛给OS,OS会寻找可识别此协议的app并用该app处理连接。好比在网页里以itunes://开头的连接是Apple Store的私有协议,点击后能够启动Apple Store而且跳转到相应的界面。国内软件开发商也常常这么作,好比支付宝的私有协议alipay://,腾讯的tencent://等等。ajax

桥协议的具体实现

因为JavaScript语言自身的特殊性(单进程),为了避免阻塞主进程而且保证H5调用的有序性,与Native通信时对于须要获取结果的接口(GET类),采用相似于JSONP的设计理念:json

hybrid jsbridge1

类比HTTP的request和response对象,调用方会将调用的api、参数、以及请求签名(由调用方生成)带上传给被调用方,被调用方处理完以后会吧结果以及请求签名回传调用方,调用方再根据请求签名找到本次请求对应的回调函数并执行,至此完成了一次通信闭环。

H5调用Native(以Android为例)示意图:

hybrid jsbridge2

Native(以Android为例)调用H5示意图:

hybrid jsbridge3

基于桥协议的api设计(HybridApi)

jsbridge做为一种通用私有协议,通常会在团队级或者公司级产品进行共享,因此须要和业务层进行解耦,将jsbridge的内部细节进行封装,对外暴露平台级的API。

如下是笔者剥离公司业务代码后抽象出的一份HybridApi js部分的实现,项目地址:

hybrid-js

另外,对于Native提供的各类接口,也能够简单封装下,使之更贴近前端工程师的使用习惯:

// /lib/jsbridge/core.js
function assignAPI(name, callback) {
    var names = name.split(/\./);
    var ns = names.shift();

    var fnName = names.pop();
    var root = createNamespace(JSBridge[ns], names);

    if(fnName) root[fnName] = callback || function() {};
}

增长api:

// /lib/jsbridge/api.js
var assign = require('./core.js').assignAPI;
...
assign('util.compassImage', function(path, callback, quality, width, height) {
    JSBridge.invokeApp('os.getInfo', {
        path: path,
        quality: quality || 80,
        width: width || 'auto',
        height: height || 'auto',
        callback: callback
    });
});

H5上层应用调用:

// h5/music/index.js
JSBridge.util.compassImage('http://cdn.foo.com/images/bar.png', function(r) {
    console.log(r.value); // => base64 data
});

界面与交互(Native与H5职责划分)

本质上,Native和H5都能完成界面开发。几乎全部hybrid的开发模式都会碰到一样的一个问题:哪些由Native负责哪些由H5负责?

这个回到原始的问题上来:咱们为何要采用hybrid模式开发?简而言之就是同时利用H5的跨平台、快速迭代能力以及Native的流畅性、系统API调用能力。

根据这个原则,为了充分利用两者的优点,应该尽量地将app内容使用H5来呈现,而对于js语言自己的缺陷,应该使用Native语言来弥补,如转场动画、多线程做业(密集型任务)、IO性能等。即总的原则是H5提供内容,Native提供容器,在有可能的条件下对Android原生webview进行优化和改造(参考阿里Hybrid容器的JSM),提高H5的渲染效率。

可是,在实际的项目中,将整个app全部界面都使用H5来开发也有不妥之处,根据经验,如下情形仍是使用Native界面为好:

关键界面、交互性强的的界面使用Native

因H5比较容易被恶意攻击,对于安全性要求比较高的界面,如注册界面、登录、支付等界面,会采用Native来取代H5开发,保证数据的安全性,这些页面一般UI变动的频率也不高。

对于这些界面,降级的方案也有,就是HTTPS。可是想说的是在国内的若网络环境下,HTTPS的体验实在是不咋地(主要是慢),并且只能走现网不能走离线通道。

另外,H5自己的动画开发成本比较高,在低端机器上可能有些绕不过的性能坎,原生js对于手势的支持也比较弱,所以对于这些类型的界面,能够选择使用Native来实现,这也是Native自己的优点不是。好比要实现下面这个音乐播放界面,用H5开发门槛不小吧,留意下中间的波浪线背景,手指左右滑动能够切换动画。

layout ui1

导航组件采用Native

导航组件,就是页面的头组件,左上角通常都是一个back键,中间通常都是界面的标题,右边的话有时是一个隐藏的悬浮菜单触发按钮有时则什么也没有。

移动端有一个特性就是界面下拉有个回弹效果,头不动body部分跟着滑动,这种效果H5比较难实现。

再者,也是最重要的一点,若是整个界面都是H5的,在H5加载过程当中界面将是白屏,在弱网络下用户可能会很疑惑。

因此基于这两点,打开的界面都是Native的导航组件+webview来组成,这样即便H5加载失败或者太慢用户能够选择直接关闭。

在API层面,会相应的有一个接口来实现这一逻辑(例如叫JSBridge.layout.setHeader),下面代码演示定制一个只有back键和标题的导航组件:

// /h5/pages/index.js
JSBridge.layout.setHeader({
    background: {
        color: '#00FF00',
        opacity: 0.8
    },
    buttons: [
        // 默认只有back键,而且back键的默认点击处理函数就是back()
        {
            icon: '../images/back.png',
            width: 16,
            height: 16,
            onClick: function() {
                // todo...
                JSBridge.back();
            }
        },
        {
            text: '音乐首页',
            color: '#00FF00',
            fontSize: 14,
            left: 10
        }
    ]
});

上面的接口,能够知足绝大多数的需求,可是还有一些特殊的界面,经过H5代码控制生成导航组件这种方式达不到需求:

layout ui2

如上图所示,界面含有tab,且能够左右滑动切换,tab标题的下划线会跟着手势左右滑动。大多见于app的首页(mainActivity)或者分频道首页,这种界面通常采用定制webview的作法:定制的导航组件和内容框架(为了支持左右滑动手势),H5打开此类界面通常也是开特殊的API:

// /h5/pages/index.js
// 开打音乐频道下“个人音乐”tab
JSBridge.view.openMusic({'tab': 'personal'});

这种打开特殊的界面的API之因此特殊,是由于它内部要么是纯Native实现,要么是和某个约定的html文件绑定,调用时打开指定的html。假设这个例子中,tab内容是H5的,若是H5是SPA架构的那么openMusic({'tab': 'personal'})则对应/music.html#personal这个url,反之多页面的则可能对应/mucic-personal.html

至于通常的打开新界面,则有两种可能:

  • app内H5界面

    指的是由app开发者开发的H5页面,也便是app的功能界面,通常互相跳转须要转场动画,打开方式是采用Native提供的接口打开,例如:
    JSBridge.view.openUrl({
        url: '/music-list.html',
        title: '音乐列表'
    });
    再配合下面即将提到的离线访问方式,基本能够作到模拟Native界面的效果。
  • 第三方H5页面

    指的是app内嵌的第三方页面,通常由`a`标签直接打开,没有转场动画,可是要求打开webview默认的历史列表,以避免打开多个连接后点回退直接回到Native主界面。

系统级UI组件采用Native

基于如下缘由,一些通用的UI组件,如alert、toast等将采用Native来实现:

  • H5自己有这些组件,可是一般比较简陋,不能和APP UI风格统一,须要再定制,好比alert组件背景增长遮罩层

  • H5来实现这些组件有时会存在坐标、尺寸计算偏差,好比笔者以前遇到的是页面load异常须要调用对话框组件提示,可是这时候页面高度为0,因此会出现弹窗“消失”的现象

  • 这些组件一般功能单一可是通用,适合作成公用组件整合到HybridApi里边

下面代码演示H5调用Native提供的UI组件:

JSBridge.ui.toast('Hello world!');

默认界面采用Native

因为H5是在H5容器里进行加载和渲染,因此Native很容易对H5页面的行为进行监控,包括进度条、loading动画、404监控、5xx监控、网络诊断等,而且在H5加载异常时提供默认界面供用户操做,防止APP“假死”。

下面是微信的5xx界面示意:

webview monitor

设计H5容器

Native除了负责部分界面开发和公共UI组件设计以外,做为H5的runtime,H5容器是hybrid架构的核心部分,为了让H5运行更快速稳定和健壮,还应当提供并但不局限于下面几方面。

H5离线访问

之因此选择hybrid方式来开发,其中一个缘由就是要解决webapp访问慢的问题。即便咱们的H5性能优化作的再好服务器在牛逼,碰到蜗牛同样的运营商网络你也没辙,有时候还会碰到流氓运营商再给webapp插点广告。。。哎说多了都是泪。

离线访问,顾名思义就是将H5预先放到用户手机,这样访问时就不会再走网络从而作到看起来和Native APP同样的快了。

可是离线机制毫不是把H5打包解压到手机sd卡这么简单粗暴,应该解决如下几个问题:

  1. H5应该有线上版本

    做为访问离线资源的降级方案,当本地资源不存在的时候应该走现网去拉取对应资源,保证H5可用。另外就是,对于H5,咱们不会把全部页面都使用离线访问,例如活动页面,这类快速上线又快速下线的页面,设计离线访问方式开发周期比较高,也有多是页面彻底是动态的,不一样的用户在不一样的时间看到的页面不同,无法落地成静态页面,还有一类就是一些说明类的静态页面,更新频率很小的,也不必作成离线占用手机存储空间。
  2. 开发调试&抓包

    咱们知道,基于file协议开发是彻底基于开发机的,代码必须存放于物理机器,这意味着修改代码须要push到sd卡再看效果,虽然能够经过假连接访问开发机本地server发布时移除的方式,可是我的以为仍是太麻烦易出错。

为了实现同一资源的线上和离线访问,Native须要对H5的静态资源请求进行拦截判断,将静态资源“映射”到sd卡资源,即实现一个处理H5资源的本地路由,实现这一逻辑的模块暂且称之为Local Url Router,具体实现细节在文章后面。

H5离线动态更新机制

将H5资源放置到本地离线访问,最大的挑战就是本地资源的动态更新如何设计,这部分能够说是最复杂的了,由于这同时涉及到H五、Native和服务器三方,覆盖式离线更新示意图以下:

workflow

解释下上图,开发阶段H5代码能够经过手机设置HTTP代理方式直接访问开发机。完成开发以后,将H5代码推送到管理平台进行构建、打包,而后管理平台再经过事先设计好的长链接通道将H5新版本信息推送给客户端,客户端收到更新指令后开始下载新包、对包进行完整性校验、merge回本地对应的包,更新结束。

其中,管理平台推送给客户端的信息主要包括项目名(包名)、版本号、更新策略(增量or全量)、包CDN地址、MD5等。

一般来讲,H5资源分为两种,常常更新的业务代码和不常常更新的框架、库代码和公用组件代码,为了实现离线资源的共享,在H5打包时能够采用分包的策略,将公用部分单独打包,在本地也是单独存放,分包及合并示意图:

multi package

Local Url Router

离线资源更新的问题解决了,剩下的就是如何使用离线资源了。

上面已经提到,对于H5的请求,线上和离线采用相同的url访问,这就须要H5容器对H5的资源请求进行拦截“映射”到本地,即Local Url Router

Local Url Router主要负责H5静态资源请求的分发(线上资源到sd卡资源的映射),可是不论是白名单仍是过滤静态文件类型,Native拦截规则和映射规则将变得比较复杂。这里,阿里去啊app的思路就比较赞,咱们借鉴一下,将映射规则交给H5去生成:H5开发完成以后会扫描H5项目而后生成一份线上资源和离线资源路径的映射表(souce-router.json),H5容器只需负责解析这个映射表便可。

H5资源包解压以后在本地的目录结构相似:

$ cd h5 && tree
.
├── js/
├── css/
├── img/
├── pages
│   ├── index.html
│   └── list.html
└── souce-router.json

souce-router.json的数据结构相似:

{
    "protocol": "http",
    "host": "o2o.xx.com",
    "localRoot": "[/storage/0/data/h5/o2o/]",
    "localFolder": "o2o.xx.com",
    "rules": {
        "/index.html": "pages/index.html",
        "/js/": "js/"
    }
}

H5容器拦截到静态资源请求时,若是本地有对应的文件则直接读取本地文件返回,不然发起HTTP请求获取线上资源,若是设计完整一点还能够考虑同时开启新线程去下载这个资源到本地,下次就走离线了。

下图演示资源在app内部的访问流程图:

url router

其中proxy指的是开发时手机设置代理http代理到开发机。

数据通道

  • 上报

因为界面由H5和Native共同完成,界面上的用户交互埋点数据最好由H5容器统一采集、上报,还有,由页面跳转产生的浏览轨迹(转化漏斗),也由H5容器记录和上报

  • ajax代理

因ajax受同源策略限制,能够在hybridApi层对ajax进行统一封装,同时兼容H5容器和浏览器runtime,采用更高效的通信通道加速H5的数据传输

Native对H5的扩展

主要指扩展H5的硬件接口调用能力,好比屏幕旋转、摄像头、麦克风、位置服务等等,将Native的能力经过接口的形式提供给H5。

综述

最后来张图总结下,hybrid客户端总体架构图:

hybrid architecture

其中的Synchronize Service模块表示和服务器的长链接通讯模块,用于接受服务器端各类推送,包括离线包等。Source Merge Service模块表示对解压后的H5资源进行更新,包括增长文件、以旧换新以及删除过时文件等。

能够看到,hybrid模式的app架构,最核心和最难的部分都是H5容器的设计。