守望者--AIR技术交流

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

搜索
热搜: ANE FlasCC 炼金术
查看: 1529|回复: 1

[技术资料] Hybrid交互设计研究

[复制链接]
  • TA的每日心情
    擦汗
    2018-4-10 15:18
  • 签到天数: 447 天

    [LV.9]以坛为家II

    1742

    主题

    2094

    帖子

    13万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    562
    贡献
    29
    金币
    52618
    钢镚
    1422

    开源英雄守望者

    发表于 2016-8-15 12:05:39 | 显示全部楼层 |阅读模式

    Hybrid交互设计

    Hybrid的交互无非是Native调用前端页面的JS方法,或者前端页面通过JS调用Native提供的接口,两者交互的桥梁皆Webview:

    app自身可以自定义url schema,并且把自定义的url注册在调度中心, 例如

    • ctrip://wireless 打开携程App
    • weixin:// 打开微信

    我们JS与Native通信一般就是创建这类URL被Native捕获处理,后续也出现了其它前端调用Native的方式,但可以做底层封装使其透明化,所以重点以及是如何进行前端与Native的交互设计。

    JS to Native

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

    1 BNJS.http.get();//向业务服务器拿请求据【1.0】 1.3版本接口有扩展
    2 BNJS.http.post();//向业务服务器提交数据【1.0】
    3 BNJS.http.sign();//计算签名【1.0】
    4 BNJS.http.getNA();//向NA服务器拿请求据【1.0】 1.3版本接口有扩展
    5 BNJS.http.postNA();//向NA服务器提交数据【1.0】
    6 BNJS.http.getCatgData();//从Native本地获取筛选数据【1.1】
     1 BNJSReady(function(){
     2  BNJS.http.post({
     3 url : 'http://cp01-testing-tuan02.cp01.baidu.com:8087/naserver/user/feedback',
     4  params : {
     5 msg : '测试post',
     6 contact : '18721687903'
     7  },
     8 onSuccess : function(res){
     9 alert('发送post请求成功!');
    10  },
    11 onFail : function(res){
    12 alert('发送post请求失败!');
    13  }
    14  });
    15 });

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

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

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

    API式交互

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

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

    粉丝服务(新手接入指南) 读取接口 接收消息 接收用户私信、关注、取消关注、@等消息接口 写入接口 发送消息 向用户回复私信消息接口 生成带参数的二维码 生成带参数的二维码接口

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

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

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

    格式约定

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

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

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

    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)调用之

    数据返回的格式约定是:

    {
    data: {},
    errno: 0,
    msg: "success"
    }

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

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

    代码实现

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

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

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

    常用交互API

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

    跳转

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

    ① 页面内跳转,与Hybrid无关

    ② H5跳转Native界面

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

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

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

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

     1 //=>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
     2 requestHybrid({
     3 tagname: 'forward',
     4  param: {
     5 //要去到的页面
     6 topage: 'hotel/detail',
     7 //跳转方式,H5跳Native
     8 type: 'native',
     9 //其它参数
    10 id: 20151031
    11  }
    12 });

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

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

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

    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是不允许重复的):

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

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

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

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

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

    Native Header组件的实现:

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

    请求类

    虽然get类请求可以用jsonp的方式绕过跨域问题,但是post请求却是真正的拦路虎,为了安全性服务器设置cors会仅仅针对几个域名,Hybrid内嵌静态资源是通过file的方式读取,这种场景使用cors就不好使了,所以每个请求需要经过Native做一层代理发出去。

    这个使用场景与Header组件一致,前端框架层必须做到对业务透明化,业务事实上不必关心这个请求是由浏览器发出还是由Native发出:

    1 HybridGet = function (url, param, callback) {
    2 };
    3 HybridPost = function (url, param, callback) {
    4 };

    真实的业务场景,会将之封装到数据请求模块,在底层做适配,在H5站点下使用ajax请求,在Native内嵌时使用代理发出,与Native的约定为:

     1 requestHybrid({
     2 tagname: 'ajax',
     3  param: {
     4 url: 'hotel/detail',
     5  param: {},
     6 //默认为get
     7 type: 'post'
     8  },
     9 //响应后的回调
    10 callback: function (data) { }
    11 });

    常用NativeUI组件

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

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

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

    来源:http://div.io/topic/1447

    守望者AIR技术交流社区(www.airmyth.com)
    回复

    使用道具 举报

  • TA的每日心情
    擦汗
    2018-4-10 15:18
  • 签到天数: 447 天

    [LV.9]以坛为家II

    1742

    主题

    2094

    帖子

    13万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    562
    贡献
    29
    金币
    52618
    钢镚
    1422

    开源英雄守望者

     楼主| 发表于 2016-8-15 12:06:35 | 显示全部楼层
    浅谈Hybrid技术的设计与实现

    http://www.cnblogs.com/yexiaochai/p/4921635.html
    守望者AIR技术交流社区(www.airmyth.com)
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    
    关闭

    站长推荐上一条 /4 下一条

    QQ|手机版|Archiver|网站地图|小黑屋|守望者 ( 京ICP备14061876号

    GMT+8, 2024-3-28 17:21 , Processed in 0.044888 second(s), 30 queries .

    守望者AIR

    守望者AIR技术交流社区

    本站成立于 2014年12月31日

    快速回复 返回顶部 返回列表