浅谈Hybrid技术的设计与实现第三弹,浅谈Hybrid技术的设计与实现

API式交互

手白、糯米底层如何做我们无从得知,但我们发现调用Native
API接口的方式和我们使用AJAX调用服务器端提供的接口是及其相似的:

图片 1

这里类似的微薄开放平台的接口是这样定义的:

粉丝服务(新手接入指南)

读取接口

接收消息

接收用户私信、关注、取消关注、@等消息接口

写入接口

发送消息

向用户回复私信消息接口

生成带参数的二维码

生成带参数的二维码接口

我们要做的就是通过一种方式创建ajax请求即可:

JavaScript

1
https://api.weibo.com/2/statuses/public_timeline.json

所以我在实际设计Hybrid交互模型时,是以接口为单位进行设计的,比如获取通讯录的总体交互是:

图片 2

native代理请求

在H5想要做某一块老的App业务,这个APP80%以上的业务都是Native做的,这类APP在接口方面就没有考虑过H5的感受,会要求很多信息如:

① 设备号

② 地理信息

③ 网络情况

④ 系统版本

有很多H5拿不到或者不容易拿到的公共信息,因为H5做的往往是一些比较小的业务,像什么个人主页之类的不重要的业务,Server端可能不愿意提供额外的接口适配,而使用额外的接口还有可能打破他们统一的某些规则;加之native对接口有自己的一套公共处理逻辑,所以便出了Native代理H5发请求的方案,公共参数会由Native自动带上。

JavaScript

//暂时只关注hybrid调试,后续得关注三端匹配 _.requestHybrid({ tagname:
‘apppost’, param: { url: this.url, param: params }, callback: function
(data) { scope.baseDataValidate(data, onComplete, onError); } });

1
2
3
4
5
6
7
8
9
10
11
12
//暂时只关注hybrid调试,后续得关注三端匹配
_.requestHybrid({
     tagname: ‘apppost’,
     param: {
         url: this.url,
         param: params
     },
     callback: function (data) {
         scope.baseDataValidate(data, onComplete, onError);
     }
});

这种方案有一些好处,接口统一,前端也不需要关注接口权限验证,但是这个会带给前端噩梦!

前端相对于native一个很大的优点,就是调试灵活,这种代理请求的方式,会限制请求只能在APP容器中生效,对前端调试造成了很大的痛苦

1
前端相对于native一个很大的优点,就是调试灵活,这种代理请求的方式,会限制请求只能在APP容器中生效,对前端调试造成了很大的痛苦

从真实的生产效果来说,也是很影响效率的,容易导致后续前端再也不愿意做那个APP的业务了,所以使用要慎重……

Native与前端分工

在做Hybrid架构设计之前需要分清Native与前端的界限,首先Native提供的是一宿主环境,要合理的利用Native提供的能力,要实现通用的Hybrid平台架构,站在前端视角,我认为需要考虑以下核心设计问题。

交互设计

Hybrid架构设计第一个要考虑的问题是如何设计与前端的交互,如果这块设计的不好会对后续开发、前端框架维护造成深远的影响,并且这种影响往往是不可逆的,所以这里需要前端与Native好好配合,提供通用的接口,比如:

① NativeUI组件,header组件、消息类组件

② 通讯录、系统、设备信息读取接口


H5与Native的互相跳转,比如H5如何跳到一个Native页面,H5如何新开Webview做动画跳到另一个H5页面

资源访问机制

Native首先需要考虑如何访问H5资源,做到既能以file的方式访问Native内部资源,又能使用url的方式访问线上资源;需要提供前端资源增量替换机制,以摆脱APP迭代发版问题,避免用户升级APP。这里就会涉及到静态资源在APP中的存放策略,更新策略的设计,复杂的话还会涉及到服务器端的支持。

账号信息设计

账号系统是重要并且无法避免的,Native需要设计良好安全的身份验证机制,保证这块对业务开发者足够透明,打通账户信息。

Hybrid开发调试

功能设计完并不是结束,Native与前端需要商量出一套可开发调试的模型,不然很多业务开发的工作将难以继续,这个很多文章已经接受过了,本文不赘述。

至于Native还会关注的一些通讯设计、并发设计、异常处理、日志监控以及安全模块因为不是我涉及的领域便不予关注了(事实上是想关注不得其门),而前端要做的事情就是封装Native提供的各种能力,整体架构是这样的:

图片 3

真实业务开发时,Native除了会关注登录模块之外还会封装支付等重要模块,这里视业务而定。

更新率

我们有时候想要的是一旦增量包发布,用户拿着手机就马上能看到最新的内容了,而这样需要app调用增量包的频率增高,所以我们是设置每30分钟检查一次更新。

Header 组件的设计

最初我其实是抵制使用Native提供的UI组件的,尤其是Header,因为平台化后,Native每次改动都很慎重并且响应很慢,但是出于两点核心因素考虑,我基本放弃了抵抗:

① 其它主流容器都是这么做的,比如微信、手机百度、携程


没有header一旦网络出错出现白屏,APP将陷入假死状态,这是不可接受的,而一般的解决方案都太业务了

PS:Native吊起Native时,如果300ms没有响应需要出loading组件,避免白屏

因为H5站点本来就有Header组件,站在前端框架层来说,需要确保业务的代码是一致的,所有的差异需要在框架层做到透明化,简单来说Header的设计需要遵循:

① H5 header组件与Native提供的header组件使用调用层接口一致

② 前端框架层根据环境判断选择应该使用H5的header组件抑或Native的header组件

一般来说header组件需要完成以下功能:


header左侧与右侧可配置,显示为文字或者图标(这里要求header实现主流图标,并且也可由业务控制图标),并需要控制其点击回调


header的title可设置为单标题或者主标题、子标题类型,并且可配置lefticon与righticon(icon居中)

③ 满足一些特殊配置,比如标签类header

所以,站在前端业务方来说,header的使用方式为(其中tagname是不允许重复的):

JavaScript

//Native以及前端框架会对特殊tagname的标识做默认回调,如果未注册callback,或者点击回调callback无返回则执行默认方法
//
back前端默认执行History.back,如果不可后退则回到指定URL,Native如果检测到不可后退则返回Naive大首页
// home前端默认返回指定URL,Native默认返回大首页 this.header.set({ left:
[ { //如果出现value字段,则默认不使用icon tagname: ‘back’, value:
‘回退’, //如果设置了lefticon或者righticon,则显示icon
//native会提供常用图标icon映射,如果找不到,便会去当前业务频道专用目录获取图标
lefticon: ‘back’, callback: function () { } } ], right: [ {
//默认icon为tagname,这里为icon tagname: ‘search’, callback: function ()
{ } }, //自定义图标 { tagname: ‘me’,
//会去hotel频道存储静态header图标资源目录搜寻该图标,没有便使用默认图标
icon: ‘hotel/me.png’, callback: function () { } } ], title: ‘title’,
//显示主标题,子标题的场景 title: [‘title’, ‘subtitle’], //定制化title
title: { value: ‘title’, //标题右边图标 righticon: ‘down’,
//也可以设置lefticon //标题类型,默认为空,设置的话需要特殊处理 //type:
‘tabs’, //点击标题时的回调,默认为空 callback: function () { } } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//Native以及前端框架会对特殊tagname的标识做默认回调,如果未注册callback,或者点击回调callback无返回则执行默认方法
// back前端默认执行History.back,如果不可后退则回到指定URL,Native如果检测到不可后退则返回Naive大首页
// home前端默认返回指定URL,Native默认返回大首页
this.header.set({
    left: [
        {
            //如果出现value字段,则默认不使用icon
            tagname: ‘back’,
            value: ‘回退’,
            //如果设置了lefticon或者righticon,则显示icon
            //native会提供常用图标icon映射,如果找不到,便会去当前业务频道专用目录获取图标
            lefticon: ‘back’,
            callback: function () { }
        }
    ],
    right: [
        {
            //默认icon为tagname,这里为icon
            tagname: ‘search’,
            callback: function () { }
        },
    //自定义图标
        {
        tagname: ‘me’,
        //会去hotel频道存储静态header图标资源目录搜寻该图标,没有便使用默认图标
        icon: ‘hotel/me.png’,
        callback: function () { }
    }
    ],
    title: ‘title’,
    //显示主标题,子标题的场景
    title: [‘title’, ‘subtitle’],
 
    //定制化title
    title: {
        value: ‘title’,
        //标题右边图标
        righticon: ‘down’, //也可以设置lefticon
        //标题类型,默认为空,设置的话需要特殊处理
        //type: ‘tabs’,
        //点击标题时的回调,默认为空
        callback: function () { }
    }
});

因为Header左边一般来说只有一个按钮,所以其对象可以使用这种形式:

JavaScript

this.header.set({ back: function () { }, title: ” }); //语法糖=>
this.header.set({ left: [{ tagname: ‘back’, callback: function(){} }],
title: ”, });

1
2
3
4
5
6
7
8
9
10
11
12
this.header.set({
    back: function () { },
    title: ”
});
//语法糖=>
this.header.set({
    left: [{
        tagname: ‘back’,
        callback: function(){}
    }],
    title: ”,
});

为完成Native端的实现,这里会新增两个接口,向Native注册事件,以及注销事件:

JavaScript

var registerHybridCallback = function (ns, name, callback) {
if(!window.Hybrid[ns]) window.Hybrid[ns] = {};
window.Hybrid[ns][name] = callback; }; var unRegisterHybridCallback
= function (ns) { if(!window.Hybrid[ns]) return; delete
window.Hybrid[ns]; };

1
2
3
4
5
6
7
8
9
var registerHybridCallback = function (ns, name, callback) {
  if(!window.Hybrid[ns]) window.Hybrid[ns] = {};
  window.Hybrid[ns][name] = callback;
};
 
var unRegisterHybridCallback = function (ns) {
  if(!window.Hybrid[ns]) return;
  delete window.Hybrid[ns];
};

Native Header组件的实现:

JavaScript

define([], function () { ‘use strict’; return _.inherit({ propertys:
function () { this.left = []; this.right = []; this.title = {};
this.view = null; this.hybridEventFlag = ‘Header_Event’; }, //全部更新
set: function (opts) { if (!opts) return; var left = []; var right =
[]; var title = {}; var tmp = {}; //语法糖适配 if (opts.back) { tmp =
{ tagname: ‘back’ }; if (typeof opts.back == ‘string’) tmp.value =
opts.back; else if (typeof opts.back == ‘function’) tmp.callback =
opts.back; else if (typeof opts.back == ‘object’) _.extend(tmp,
opts.back); left.push(tmp); } else { if (opts.left) left = opts.left; }
//右边按钮必须保持数据一致性 if (typeof opts.right == ‘object’ &&
opts.right.length) right = opts.right if (typeof opts.title == ‘string’)
{ title.title = opts.title; } else if (_.isArray(opts.title) &&
opts.title.length > 1) { title.title = opts.title[0];
title.subtitle = opts.title[1]; } else if (typeof opts.title ==
‘object’) { _.extend(title, opts.title); } this.left = left; this.right
= right; this.title = title; this.view = opts.view;
this.registerEvents(); _.requestHybrid({ tagname: ‘updateheader’,
param: { left: this.left, right: this.right, title: this.title } }); },
//注册事件,将事件存于本地 registerEvents: function () {
_.unRegisterHybridCallback(this.hybridEventFlag);
this._addEvent(this.left); this._addEvent(this.right);
this._addEvent(this.title); }, _addEvent: function (data) { if
(!_.isArray(data)) data = [data]; var i, len, tmp, fn, tagname; var t
= ‘header_’ + (new Date().getTime()); for (i = 0, len = data.length; i
< len; i++) { tmp = data[i]; tagname = tmp.tagname || ”; if
(tmp.callback) { fn = $.proxy(tmp.callback, this.view); tmp.callback =
t; _.registerHeaderCallback(this.hybridEventFlag, t + ‘_’ + tagname,
fn); } } }, //显示header show: function () { _.requestHybrid({ tagname:
‘showheader’ }); }, //隐藏header hide: function () { _.requestHybrid({
tagname: ‘hideheader’, param: { animate: true } }); },
//只更新title,不重置事件,不对header其它地方造成变化,仅仅最简单的header能如此操作
update: function (title) { _.requestHybrid({ tagname:
‘updateheadertitle’, param: { title: ‘aaaaa’ } }); }, initialize:
function () { this.propertys(); } }); }); Native Header组件的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
define([], function () {
    ‘use strict’;
 
    return _.inherit({
 
        propertys: function () {
 
            this.left = [];
            this.right = [];
            this.title = {};
            this.view = null;
 
            this.hybridEventFlag = ‘Header_Event’;
 
        },
 
        //全部更新
        set: function (opts) {
            if (!opts) return;
 
            var left = [];
            var right = [];
            var title = {};
            var tmp = {};
 
            //语法糖适配
            if (opts.back) {
                tmp = { tagname: ‘back’ };
                if (typeof opts.back == ‘string’) tmp.value = opts.back;
                else if (typeof opts.back == ‘function’) tmp.callback = opts.back;
                else if (typeof opts.back == ‘object’) _.extend(tmp, opts.back);
                left.push(tmp);
            } else {
                if (opts.left) left = opts.left;
            }
 
            //右边按钮必须保持数据一致性
            if (typeof opts.right == ‘object’ && opts.right.length) right = opts.right
 
            if (typeof opts.title == ‘string’) {
                title.title = opts.title;
            } else if (_.isArray(opts.title) && opts.title.length > 1) {
                title.title = opts.title[0];
                title.subtitle = opts.title[1];
            } else if (typeof opts.title == ‘object’) {
                _.extend(title, opts.title);
            }
 
            this.left = left;
            this.right = right;
            this.title = title;
            this.view = opts.view;
 
            this.registerEvents();
 
            _.requestHybrid({
                tagname: ‘updateheader’,
                param: {
                    left: this.left,
                    right: this.right,
                    title: this.title
                }
            });
 
        },
 
        //注册事件,将事件存于本地
        registerEvents: function () {
            _.unRegisterHybridCallback(this.hybridEventFlag);
            this._addEvent(this.left);
            this._addEvent(this.right);
            this._addEvent(this.title);
        },
 
        _addEvent: function (data) {
            if (!_.isArray(data)) data = [data];
            var i, len, tmp, fn, tagname;
            var t = ‘header_’ + (new Date().getTime());
 
            for (i = 0, len = data.length; i < len; i++) {
                tmp = data[i];
                tagname = tmp.tagname || ”;
                if (tmp.callback) {
                    fn = $.proxy(tmp.callback, this.view);
                    tmp.callback = t;
                    _.registerHeaderCallback(this.hybridEventFlag, t + ‘_’ + tagname, fn);
                }
            }
        },
 
        //显示header
        show: function () {
            _.requestHybrid({
                tagname: ‘showheader’
            });
        },
 
        //隐藏header
        hide: function () {
            _.requestHybrid({
                tagname: ‘hideheader’,
                param: {
                    animate: true
                }
            });
        },
 
        //只更新title,不重置事件,不对header其它地方造成变化,仅仅最简单的header能如此操作
        update: function (title) {
            _.requestHybrid({
                tagname: ‘updateheadertitle’,
                param: {
                    title: ‘aaaaa’
                }
            });
        },
 
        initialize: function () {
            this.propertys();
        }
    });
 
});
 
Native Header组件的封装

一些坑点

Hybrid的资源

离线更新

根据之前的约定,Native中如果存在静态资源,也是按频道划分的:

JavaScript

webapp //根目录 ├─flight ├─hotel //酒店频道 │ │ index.html
//业务入口html资源,如果不是单页应用会有多个入口 │ │ main.js
//业务所有js资源打包 │ │ │ └─static //静态样式资源 │ ├─css │ ├─hybrid
//存储业务定制化类Native Header图标 │ └─images ├─libs │ libs.js
//框架所有js资源打包 │ └─static //框架静态资源样式文件 ├─css └─images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
webapp //根目录
├─flight
├─hotel //酒店频道
│  │  index.html //业务入口html资源,如果不是单页应用会有多个入口
│  │  main.js //业务所有js资源打包
│  │
│  └─static //静态样式资源
│      ├─css
│      ├─hybrid //存储业务定制化类Native Header图标
│      └─images
├─libs
│      libs.js //框架所有js资源打包
└─static //框架静态资源样式文件
    ├─css
    └─images

我们这里制定一个规则,native会过滤某一个规则的请求,检查本地是否有该文件,如果本地有那么就直接读取本地,比如说,我们会将这个类型的请求映射到本地:

JavaScript

//===>> file ===> flight/static/hybrid/icon-search.png

1
2
3
http://domain.com/webapp/flight/static/hybrid/icon-search.png
//===>>
file ===> flight/static/hybrid/icon-search.png

这样在浏览器中便继续读取线上文件,在native中,如果有本地资源,便读取本地资源:

图片 4

但是我们在真实使用场景中却遇到了一些麻烦。

常用交互API

良好的交互设计是成功的第一步,在真实业务开发中有一些API一定会用到。

结语

希望此文能对准备接触Hybrid技术的朋友提供一些帮助,关于Hybrid的系列这里是最后一篇实战类文章介绍,这里是demo期间的一些效果图,后续git库的代码会再做整理:

图片 5

图片 6

图片 7

JS to Native

Native在每个版本会提供一些API,前端会有一个对应的框架团队对其进行封装,释放业务接口。比如糯米对外的接口是这样的:

JavaScript

BNJS.http.get();//向业务服务器拿请求据【1.0】 1.3版本接口有扩展
BNJS.http.post();//向业务服务器提交数据【1.0】
BNJS.http.sign();//计算签名【1.0】
BNJS.http.getNA();//向NA服务器拿请求据【1.0】 1.3版本接口有扩展
BNJS.http.postNA();//向NA服务器提交数据【1.0】
BNJS.http.getCatgData();//从Native本地获取筛选数据【1.1】

1
2
3
4
5
6
BNJS.http.get();//向业务服务器拿请求据【1.0】 1.3版本接口有扩展
BNJS.http.post();//向业务服务器提交数据【1.0】
BNJS.http.sign();//计算签名【1.0】
BNJS.http.getNA();//向NA服务器拿请求据【1.0】 1.3版本接口有扩展
BNJS.http.postNA();//向NA服务器提交数据【1.0】
BNJS.http.getCatgData();//从Native本地获取筛选数据【1.1】

JavaScript

BNJSReady(function(){ BNJS.http.post({ url :
”,
params : { msg : ‘测试post’, contact : ‘18721687903’ }, onSuccess :
function(res){ alert(‘发送post请求成功!’); }, onFail : function(res){
alert(‘发送post请求失败!’); } }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BNJSReady(function(){
    BNJS.http.post({
        url : ‘http://cp01-testing-tuan02.cp01.baidu.com:8087/naserver/user/feedback’,
        params : {
            msg : ‘测试post’,
            contact : ‘18721687903’
        },
        onSuccess : function(res){
            alert(‘发送post请求成功!’);
        },
        onFail : function(res){
            alert(‘发送post请求失败!’);
        }
    });
});

前端框架定义了一个全局变量BNJS作为Native与前端交互的对象,只要引入了糯米提供的这个JS库,并且在糯米封装的Webview容器中,前端便获得了调用Native的能力,我揣测糯米这种设计是因为这样便于第三方团队的接入使用,手机百度有一款轻应用框架也走的这种路线:

JavaScript

clouda.mbaas.account //释放了clouda全局变量

1
clouda.mbaas.account //释放了clouda全局变量

这样做有一个前提是,Native本身已经十分稳定了,很少新增功能了,否则在直连情况下就会面临一个尴尬,因为web站点永远保持最新的,就会在一些低版本容器中调用了没有提供的Native能力而报错。

header-搜索

根据我们之前的约定,header是比较中规中矩的,但是由于产品和视觉强迫,我们实现了一个不一样的header,最开始虽然不太乐意,做完了后感觉还行……

图片 8

这块工作量主要是native的,我们只需要约定即可:

JavaScript

this.header.set({ view: this, //左边按钮 left: [], //右边按钮 right:
[{ tagname: ‘cancel’, value: ‘取消’, callback: function () {
this.back(); } }], //searchbox定制 title: { //特殊tagname tagname:
‘searchbox’, //标题,该数据为默认文本框文字 title: ‘取消’,
//没有文字时候的占位提示 placeholder: ‘搜索医院、科室、医生和病症’,
//是否默认进入页面获取焦点 focus: true, //文本框相关具有的回调事件
//data为一个json串 //editingdidbegin
为点击或者文本框获取焦点时候触发的事件 //editingdidend
为文本框失去焦点触发的事件 //editingchanged
为文本框数据改变时候触发的事件 type: ”, data: ” //真实数据 },
callback: function(data) { var _data = JSON.parse(data); if
(_data.type == ‘editingdidend’ && this.keyword != $.trim(_data.data))
{ this.keyword = $.trim(_data.data); this.reloadList(); } } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
this.header.set({
    view: this,
     //左边按钮
     left: [],
    //右边按钮
     right: [{
         tagname: ‘cancel’,
        value: ‘取消’,
         callback: function () {
            this.back();
        }
    }],
    //searchbox定制
     title: {
         //特殊tagname
         tagname: ‘searchbox’,
        //标题,该数据为默认文本框文字
         title: ‘取消’,
         //没有文字时候的占位提示
        placeholder: ‘搜索医院、科室、医生和病症’,
         //是否默认进入页面获取焦点
        focus: true,
         //文本框相关具有的回调事件
         //data为一个json串
         //editingdidbegin 为点击或者文本框获取焦点时候触发的事件
        //editingdidend 为文本框失去焦点触发的事件
         //editingchanged 为文本框数据改变时候触发的事件
         type: ”,
        data: ” //真实数据
     },
     callback: function(data) {
         var _data = JSON.parse(data);
         if (_data.type == ‘editingdidend’ && this.keyword != $.trim(_data.data)) {
             this.keyword = $.trim(_data.data);
            this.reloadList();
         }
     }
});

格式约定

交互的第一步是设计数据格式,这里分为请求数据格式与响应数据格式,参考ajax的请求模型大概是:

$.ajax(options) ⇒ XMLHttpRequest type (默认值:”GET”)
HTTP的请求方法(“GET”, “POST”, or other)。 url (默认值:当前url)
请求的url地址。 data (默认值:none)
请求中包含的数据,对于GET请求来说,这是包含查询字符串的url地址,如果是包含的是object的话,$.param会将其转化成string。

1
2
3
4
$.ajax(options) ⇒ XMLHttpRequest
type (默认值:"GET") HTTP的请求方法(“GET”, “POST”, or other)。
url (默认值:当前url) 请求的url地址。
data (默认值:none) 请求中包含的数据,对于GET请求来说,这是包含查询字符串的url地址,如果是包含的是object的话,$.param会将其转化成string。

所以我这边与Native约定的请求模型是:

JavaScript

requestHybrid({ //创建一个新的webview对话框窗口 tagname: ‘hybridapi’,
//请求参数,会被Native使用 param: {}, //Native处理成功后回调前端的方法
callback: function (data) { } });

1
2
3
4
5
6
7
8
9
requestHybrid({
  //创建一个新的webview对话框窗口
  tagname: ‘hybridapi’,
  //请求参数,会被Native使用
  param: {},
  //Native处理成功后回调前端的方法
  callback: function (data) {
  }
});

这个方法执行会形成一个URL,比如:

hybridschema://hybridapi?callback=hybrid_1446276509894&param=%7B%22data1%22%3A1%2C%22data2%22%3A2%7D

这里提一点,APP安装后会在手机上注册一个schema,比如淘宝是taobao://,Native会有一个进程监控Webview发出的所有schema://请求,然后分发到“控制器”hybridapi处理程序,Native控制器处理时会需要param提供的参数(encode过),处理结束后将携带数据获取Webview
window对象中的callback(hybrid_1446276509894)调用之

数据返回的格式约定是:

JavaScript

{ data: {}, errno: 0, msg: “success” }

1
2
3
4
5
{
  data: {},
  errno: 0,
  msg: "success"
}

真实的数据在data对象中,如果errno不为0的话,便需要提示msg,这里举个例子如果错误码1代表该接口需要升级app才能使用的话:

JavaScript

{ data: {}, errno: 1, msg: “APP版本过低,请升级APP版本” }

1
2
3
4
5
{
  data: {},
  errno: 1,
  msg: "APP版本过低,请升级APP版本"
}

代码实现

这里给一个简单的代码实现,真实代码在APP中会有所变化:

JavaScript

window.Hybrid = window.Hybrid || {}; var bridgePostMsg = function (url)
{ if ($.os.ios) { window.location = url; } else { var ifr =
$(‘<iframe style=”display: none;” src=”‘%20+%20url%20+%20′”/>’);
$(‘body’).append(ifr); setTimeout(function () { ifr.remove(); }, 1000) }
}; var _getHybridUrl = function (params) { var k, paramStr = ”, url =
‘scheme://’; url += params.tagname + ‘?t=’ + new Date().getTime();
//时间戳,防止url不起效 if (params.callback) { url += ‘&callback=’ +
params.callback; delete params.callback; } if (params.param) { paramStr
= typeof params.param == ‘object’ ? JSON.stringify(params.param) :
params.param; url += ‘&param=’ + encodeURIComponent(paramStr); } return
url; }; var requestHybrid = function (params) {
//生成唯一执行函数,执行后销毁 var tt = (new Date().getTime()); var t =
‘hybrid_’ + tt; var tmpFn; //处理有回调的情况 if (params.callback) {
tmpFn = params.callback; params.callback = t; window.Hybrid[t] =
function (data) { tmpFn(data); delete window.Hybrid[t]; } }
bridgePostMsg(_getHybridUrl(params)); };
//获取版本信息,约定APP的navigator.userAgent版本包含版本信息:scheme/xx.xx.xx
var getHybridInfo = function () { var platform_version = {}; var na =
navigator.userAgent; var info = na.match(/scheme/d.d.d/); if
(info && info[0]) { info = info[0].split(‘/’); if (info &&
info.length == 2) { platform_version.platform = info[0];
platform_version.version = info[1]; } } return platform_version; };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
window.Hybrid = window.Hybrid || {};
var bridgePostMsg = function (url) {
    if ($.os.ios) {
        window.location = url;
    } else {
        var ifr = $(‘<iframe style="display: none;" src="’%20+%20url%20+%20’"/>’);
        $(‘body’).append(ifr);
        setTimeout(function () {
            ifr.remove();
        }, 1000)
    }
};
var _getHybridUrl = function (params) {
    var k, paramStr = ”, url = ‘scheme://’;
    url += params.tagname + ‘?t=’ + new Date().getTime(); //时间戳,防止url不起效
    if (params.callback) {
        url += ‘&callback=’ + params.callback;
        delete params.callback;
    }
    if (params.param) {
        paramStr = typeof params.param == ‘object’ ? JSON.stringify(params.param) : params.param;
        url += ‘&param=’ + encodeURIComponent(paramStr);
    }
    return url;
};
var requestHybrid = function (params) {
    //生成唯一执行函数,执行后销毁
    var tt = (new Date().getTime());
    var t = ‘hybrid_’ + tt;
    var tmpFn;
 
    //处理有回调的情况
    if (params.callback) {
        tmpFn = params.callback;
        params.callback = t;
        window.Hybrid[t] = function (data) {
            tmpFn(data);
            delete window.Hybrid[t];
        }
    }
    bridgePostMsg(_getHybridUrl(params));
};
//获取版本信息,约定APP的navigator.userAgent版本包含版本信息:scheme/xx.xx.xx
var getHybridInfo = function () {
    var platform_version = {};
    var na = navigator.userAgent;
    var info = na.match(/scheme/d.d.d/);
 
    if (info && info[0]) {
        info = info[0].split(‘/’);
        if (info && info.length == 2) {
            platform_version.platform = info[0];
            platform_version.version = info[1];
        }
    }
    return platform_version;
};

因为Native对于H5来是底层,框架&底层一般来说是不会关注业务实现的,所以真实业务中Native调用H5场景较少,这里不予关注了。

一些小特性

为了让H5的表现更加像native我们会约定一些小的特性,这种特性不适合通用架构,但是有了会更有亮点。

增量机制

真实的增量机制需要服务器端的配合,我这里只能简单描述,Native端会有维护一个版本映射表:

JavaScript

{ flight: 1.0.0, hotel: 1.0.0, libs: 1.0.0, static: 1.0.0 }

1
2
3
4
5
6
{
  flight: 1.0.0,
  hotel: 1.0.0,
  libs: 1.0.0,
  static: 1.0.0
}

这个映射表是每次大版本APP发布时由服务器端生成的,如果酒店频道需要在线做增量发布的话,会打包一个与线上一致的文件目录,走发布平台发布,会在数据库中形成一条记录:

channel ver md5
flight 1.0.0 1245355335
hotel 1.0.1 455ettdggd

 

当APP启动时,APP会读取版本信息,这里发现hotel的本地版本号比线上的小,便会下载md5对应的zip文件,然后解压之并且替换整个hotel文件,本次增量结束,因为所有的版本文件不会重复,APP回滚时可用回到任意想去的版本,也可以对任意版本做BUG修复。

交互约定

根据之前的学习,我们知道与Native交互有两种交互:

① URL Schema

② JavaScriptCore

而两种方式在使用上各有利弊,首先来说URL
Schema是比较稳定而成熟的,如果使用上文中提到的“ajax”交互方式,会比较灵活;而从设计的角度来说JavaScriptCore似乎更加合理,但是我们在实际使用中却发现,注入的时机得不到保障。

iOS同事在实体JavaScriptCore注入时,我们的原意是在webview载入前就注入所有的Native能力,而实际情况是页面js已经执行完了才被注入,这里会导致Hybrid交互失效,如果你看到某个Hybrid平台,突然header显示不正确了,就可能是这个问题导致,所以JavaScriptCore就被我们弃用了。

JavaScript

JavaScriptCore可能导致的问题: ① 注入时机不唯一(也许是BUG) ②
刷新页面的时候,JavaScriptCore的注入在不同机型表现不一致,有些就根本不注入了,所以全部hybrid交互失效

1
2
3
JavaScriptCore可能导致的问题:
① 注入时机不唯一(也许是BUG)
② 刷新页面的时候,JavaScriptCore的注入在不同机型表现不一致,有些就根本不注入了,所以全部hybrid交互失效

如果非要使用JavaScriptCore,为了解决这一问题,我们做了一个兼容,用URL
Schema的方式,在页面逻辑载入之初执行一个命令,将native的一些方式重新载入,比如:

JavaScript

_.requestHybrid({ tagname: ‘injection’ });

1
2
3
_.requestHybrid({
     tagname: ‘injection’
});

这个能解决一些问题,但是有些初始化就马上要用到的方法可能就无力了,比如:

① 想要获取native给予的地理信息

② 想要获取native给予的用户信息(直接以变量的方式获取)

作为生产来讲,我们还是求稳,所以最终选择了URL Schema。

明白了基本的边界问题,选取了底层的交互方式,就可以开始进行初步的Hybrid设计了,但是这离一个可用于生产,可离落地的Hybrid方案还比较远。

结语

github上代码会持续更新,现在界面反正不太好看,大家多多包涵吧,这里是一些效果图:

图片 9图片 10图片 11

Hybrid方案是快速迭代项目,快速占领市场的神器,希望此文能对准备接触Hybrid技术的朋友提供一些帮助,并且再次感谢明月同学的配合。

 

1 赞 4 收藏
评论

图片 12

账号切换&注销

账户注销本没有什么注意点,但是因为H5
push了一个个webview页面,这个重新登录后这些页面怎么处理是个问题。

我们这边设计的是一旦重新登录或者注销账户,所有的webview都会被pop掉,然后再新开一个页面,就不会存在一些页面展示怪异的问题了。

常用NativeUI组件

最后,Native会提供几个常用的Native级别的UI,比如loading加载层,比如toast消息框:

JavaScript

var HybridUI = {}; HybridUI.showLoading(); //=> requestHybrid({
tagname: ‘showLoading’ }); HybridUI.showToast({ title: ‘111’,
//几秒后自动关闭提示框,-1需要点击才会关闭 hidesec: 3,
//弹出层关闭时的回调 callback: function () { } }); //=>
requestHybrid({ tagname: ‘showToast’, param: { title: ‘111’, hidesec: 3,
callback: function () { } } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var HybridUI = {};
HybridUI.showLoading();
//=>
requestHybrid({
    tagname: ‘showLoading’
});
 
HybridUI.showToast({
    title: ‘111’,
    //几秒后自动关闭提示框,-1需要点击才会关闭
    hidesec: 3,
    //弹出层关闭时的回调
    callback: function () { }
});
//=>
requestHybrid({
    tagname: ‘showToast’,
    param: {
        title: ‘111’,
        hidesec: 3,
        callback: function () { }
    }
});

Native
UI与前端UI不容易打通,所以在真实业务开发过程中,一般只会使用几个关键的Native
UI。

边界问题

在我们使用Hybrid技术前要注意一个边界问题,什么项目适合Hybrid什么项目不适合,这个要搞清楚,适合Hybrid的项目为:

① 有60%以上的业务为H5

② 对更新(开发效率)有一定要求的APP

不适合使用Hybrid技术的项目有以下特点:

① 只有20%不到的业务使用H5做

② 交互效果要求较高(动画多)

任何技术都有适用的场景,千万不要妄想推翻已有APP的业务用H5去替代,最后会证明那是自讨苦吃,当然如果仅仅想在APP里面嵌入新的实验性业务,这个是没问题的。

目录结构

Hybrid技术既然是将静态资源存于Native,那么就需要目录设计,经过之前的经验,目录结构一般以2层目录划分:

图片 13

如果我们有两个频道酒店与机票,那么目录结构是这样的:

webapp //根目录 ├─flight ├─hotel //酒店频道 │ │ index.html
//业务入口html资源,如果不是单页应用会有多个入口 │ │ main.js
//业务所有js资源打包 │ │ │ └─static //静态样式资源 │ ├─css │ ├─hybrid
//存储业务定制化类Native Header图标 │ └─images ├─libs │ libs.js
//框架所有js资源打包 │ └─static ├─css └─images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
webapp //根目录
├─flight
├─hotel //酒店频道
│  │  index.html //业务入口html资源,如果不是单页应用会有多个入口
│  │  main.js //业务所有js资源打包
│  │
│  └─static //静态样式资源
│      ├─css
│      ├─hybrid //存储业务定制化类Native Header图标
│      └─images
├─libs
│      libs.js //框架所有js资源打包
└─static
    ├─css
    └─images

最初设计的forward跳转中的topage参数规则是:频道/具体页面=>channel/page,其余资源会由index.html这个入口文件带出。

增量的粒度

其实,我们最开始做增量设计的时候就考虑了很多问题,但是真实业务的时候往往因为时间的压迫,做出来的东西就会很简陋,这个只能慢慢迭代,而我们所有的缓存都会考虑两个问题:

① 如何存储&读取缓存

② 如何更新缓存

浏览器的缓存读取更新是比较单纯的:

浏览器只需要自己能读到最新的缓存即可

1
浏览器只需要自己能读到最新的缓存即可

而APP的话,会存在最新发布的APP希望读到离线包,而老APP不希望读到增量包的情况(老的APP下载下来增量包压根不支持),更加复杂的情况是想对某个版本做定向修复,那么就需要定向发增量包了,这让情况变得复杂,而复杂即错误,我们往往可以以简单的约定,解决复杂的场景。

思考以下场景:

我们的APP要发一个新的版本了,我们把最初一版的静态资源给打了进去,完了审核中的时候,我们老版本APP突然有一个临时需求要上线,我知道这听起来很有一些扯淡,但这种扯淡的事情却真实的发生了,这个时候我们如果打了增量包的话,那么最新的APP在审核期间也会拉到这次代码,但也许这不是我们所期望的,于是有了以下与native的约定:

Native请求增量更新的时候带上版本号,并且强迫约定iOS与Android的大版本号一致,比如iOS为2.1.0Android这个版本修复BUG可以是2.1.1但不能是2.2.0

1
Native请求增量更新的时候带上版本号,并且强迫约定iOS与Android的大版本号一致,比如iOS为2.1.0Android这个版本修复BUG可以是2.1.1但不能是2.2.0

然后在服务器端配置一个较为复杂的版本映射表:

JavaScript

## 附录一 // 每个app所需的项目配置 const APP_CONFIG = [ ‘surgery’
=> [ // 包名 ‘channel’ => ‘d2d’, // 主项目频道名 ‘dependencies’
=> [‘blade’, ‘static’, ‘user’], // 依赖的频道 ‘version’ => [ //
各个版本对应的增量包范围,取范围内版本号最大的增量包 ‘2.0.x’ =>
[‘gte’ => ‘1.0.0’, ‘lt’ => ‘1.1.0’], ‘2.2.x’ => [‘gte’ =>
‘1.1.0’, ‘lt’ => ‘1.2.0’] ], ‘version_i’ => [ //
ios需特殊配置的某版本 ], ‘version_a’ => [ //
Android需特殊配置的某版本 ] ] ];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 附录一  
// 每个app所需的项目配置
const APP_CONFIG = [
   ‘surgery’ => [        // 包名
        ‘channel’ => ‘d2d’,      // 主项目频道名
        ‘dependencies’ => [‘blade’, ‘static’, ‘user’],    // 依赖的频道
        ‘version’ => [   // 各个版本对应的增量包范围,取范围内版本号最大的增量包
            ‘2.0.x’ => [‘gte’ => ‘1.0.0’, ‘lt’ => ‘1.1.0’],    
            ‘2.2.x’ => [‘gte’ => ‘1.1.0’, ‘lt’ => ‘1.2.0’]
        ],
        ‘version_i’ => [    // ios需特殊配置的某版本
 
        ],
        ‘version_a’ => [    // Android需特殊配置的某版本
 
        ]
    ]
];

这里解决了APP版本的读取限制,完了我们便需要关心增量的到达率与更新率,我们也会担心我们的APP读到错误的文件。

跳转

跳转是Hybrid必用API之一,对前端来说有以下跳转:

① 页面内跳转,与Hybrid无关

② H5跳转Native界面

③ H5新开Webview跳转H5页面,一般为做页面动画切换

如果要使用动画,按业务来说有向前与向后两种,forward&back,所以约定如下,首先是H5跳Native某一个页面

JavaScript

//H5跳Native页面
//=>baidubus://forward?t=1446297487682&param=%7B%22topage%22%3A%22home%22%2C%22type%22%3A%22h2n%22%2C%22data2%22%3A2%7D
requestHybrid({ tagname: ‘forward’, param: { //要去到的页面 topage:
‘home’, //跳转方式,H5跳Native type: ‘native’, //其它参数 data2: 2 } });

1
2
3
4
5
6
7
8
9
10
11
12
13
//H5跳Native页面
//=>baidubus://forward?t=1446297487682&param=%7B%22topage%22%3A%22home%22%2C%22type%22%3A%22h2n%22%2C%22data2%22%3A2%7D
requestHybrid({
    tagname: ‘forward’,
    param: {
        //要去到的页面
        topage: ‘home’,
        //跳转方式,H5跳Native
        type: ‘native’,
        //其它参数
        data2: 2
    }
});

比如携程H5页面要去到酒店Native某一个页面可以这样:

JavaScript

//=>schema://forward?t=1446297653344&param=%7B%22topage%22%3A%22hotel%2Fdetail%20%20%22%2C%22type%22%3A%22h2n%22%2C%22id%22%3A20151031%7D
requestHybrid({ tagname: ‘forward’, param: { //要去到的页面 topage:
‘hotel/detail’, //跳转方式,H5跳Native type: ‘native’, //其它参数 id:
20151031 } });

1
2
3
4
5
6
7
8
9
10
11
12
//=>schema://forward?t=1446297653344&param=%7B%22topage%22%3A%22hotel%2Fdetail%20%20%22%2C%22type%22%3A%22h2n%22%2C%22id%22%3A20151031%7D
requestHybrid({
    tagname: ‘forward’,
    param: {
        //要去到的页面
        topage: ‘hotel/detail’,
        //跳转方式,H5跳Native
        type: ‘native’,
        //其它参数
        id: 20151031
    }
});

比如H5新开Webview的方式跳转H5页面便可以这样:

JavaScript

requestHybrid({ tagname: ‘forward’, param: {
//要去到的页面,首先找到hotel频道,然后定位到detail模块 topage:
‘hotel/detail ‘, //跳转方式,H5新开Webview跳转,最后装载H5页面 type:
‘webview’, //其它参数 id: 20151031 } });

1
2
3
4
5
6
7
8
9
10
11
requestHybrid({
    tagname: ‘forward’,
    param: {
        //要去到的页面,首先找到hotel频道,然后定位到detail模块
        topage: ‘hotel/detail  ‘,
        //跳转方式,H5新开Webview跳转,最后装载H5页面
        type: ‘webview’,
        //其它参数
        id: 20151031
    }
});

back与forward一致,我们甚至会有animattype参数决定切换页面时的动画效果,真实使用时可能会封装全局方法略去tagname的细节,这时就和糯米对外释放的接口差不多了。

推动感悟

从项目调研到项目落地再到最近一些的优化,已经花了三个月时间了,要做好一件事是不容易的,而且我们这个还涉及到持续优化,和配套业务比如:

① passport

② 钱包业务

③ 反馈业务

…..

等同步制作,很多工作的意义,或者作用,是非技术同事看不到的,但是如果我们不坚持做下去,迫于业务压力或者自我松懈放纵,那么就什么也没有了,我们要推动一件事情,不可能一站出来就说,嘿,小样,我们这个不错,你拿去用吧,这样人家会猜疑你的,我们一定是要先做一定demo让人有一定初步印象,再强制或者偷偷再某一个生产业务试用,一方面将技术依赖弄进去,一方面要告诉其他同事,看看嘛,也没有引起多大问题嘛,呵呵。

做事难,推动难,难在坚持,难在携手共进,这里面是需要信念的,在此尤其感谢团队3个伙伴的无私付出(杨杨、文文、文文)。

后续,我们在持续推动hybrid建设的同时,会尝试React
Native,找寻更好的更适合自己的解决方案。

1 赞 收藏
评论

图片 12

账号系统的设计

根据上面的设计,我们约定在Hybrid中请求有两种发出方式:

① 如果是webview访问线上站点的话,直接使用传统ajax发出

② 如果是file的形式读取Native本地资源的话,请求由Native代理发出

因为静态html资源没有鉴权的问题,真正的权限验证需要请求服务器api响应通过错误码才能获得,这是动态语言与静态语言做入口页面的一个很大的区别。

以网页的方式访问,账号登录与否由是否带有秘钥cookie决定(这时并不能保证秘钥的有效性),因为Native不关注业务实现,而每次载入都有可能是登录成功跳回来的结果,所以每次载入后都需要关注秘钥cookie变化,以做到登录态数据一致性。

以file的方式访问内嵌资源的话,因为API请求控制方为Native,所以鉴权的工作完全由Native完成,接口访问如果没有登录便弹出Native级别登录框引导登录即可,每次访问webview将账号信息种入到webview中,这里有个矛盾点是Native种入webview的时机,因为有可能是网页注销的情况,所以这里的逻辑是:

① webview载入结束

② Native检测webview是否包含账号cookie信息


如果不包含则种入cookie,如果包含则检测与Native账号信息是否相同,不同则替换自身

④ 如果检测到跳到了注销账户的页面,则需要清理自身账号信息

如果登录不统一会就会出现上述复杂的逻辑,所以真实情况下我们会对登录接口收口。

简单化账号接口

平台层面觉得上述操作过于复杂,便强制要求在Hybrid容器中只能使用Native接口进行登录和登出,前端框架在底层做适配,保证上层业务的透明,这样情况会简单很多:

① 使用Native代理做请求接口,如果没有登录直接Native层唤起登录框


直连方式使用ajax请求接口,如果没有登录则在底层唤起登录框(需要前端框架支持)

简单的登录登出接口实现:

JavaScript

/* 无论成功与否皆会关闭登录框 参数包括: success 登录成功的回调 error
登录失败的回调 url
如果没有设置success,或者success执行后没有返回true,则默认跳往此url */
HybridUI.Login = function (opts) { }; //=> requestHybrid({ tagname:
‘login’, param: { success: function () { }, error: function () { }, url:
‘…’ } }); //与登录接口一致,参数一致 HybridUI.logout = function () {
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
无论成功与否皆会关闭登录框
参数包括:
success 登录成功的回调
error 登录失败的回调
url 如果没有设置success,或者success执行后没有返回true,则默认跳往此url
*/
HybridUI.Login = function (opts) {
};
//=>
requestHybrid({
    tagname: ‘login’,
    param: {
        success: function () { },
        error: function () { },
        url: ‘…’
    }
});
//与登录接口一致,参数一致
HybridUI.logout = function () {
};

账号信息获取

在实际的业务开发中,判断用户是否登录、获取用户基本信息的需求比比皆是,所以这里必须保证Hybrid开发模式与H5开发模式保持统一,否则需要在业务代码中做很多无谓的判断,我们在前端框架会封装一个User模块,主要接口包括:

JavaScript

1 var User = {}; 2 User.isLogin = function () { }; 3 User.getInfo =
function () { };

1
2
3
1 var User = {};
2 User.isLogin = function () { };
3 User.getInfo = function () { };

这个代码的底层实现分为前端实现,Native实现,首先是前端的做法是:

当前端页面载入后,会做一次异步请求,请求用户相关数据,如果是登录状态便能获取数据存于localstorage中,这里一定不能存取敏感信息

前端使用localstorage的话需要考虑极端情况下使用内存变量的方式替换localstorage的实现,否则会出现不可使用的情况,而后续的访问皆是使用localstorage中的数据做判断依据,以下情况需要清理localstorage的账号数据:

① 系统登出

② 访问接口提示需要登录

③ 调用登录接口

这种模式多用于单页应用,非单页应用一般会在每次刷新页面先清空账号信息再异步拉取,但是如果当前页面马上就需要判断用户登录数据的话,便不可靠了;处于Hybrid容器中时,因为Native本身就保存了用户信息,封装的接口直接由Native获取即可,这块比较靠谱。

落地项目

真实落地的业务为医联通,有兴趣的朋友试试:

图片 15

图片 16

发表评论

电子邮件地址不会被公开。 必填项已用*标注