zoukankan      html  css  js  c++  java
  • 浅谈Hybrid技术的设计与实现(转)

    前言

    随着移动浪潮的兴起,各种APP层出不穷,极速的业务扩展提升了团队对开发效率的要求,这个时候使用IOS&Andriod开发一个APP似乎成本有点过高了,而H5的低成本、高效率、跨平台等特性马上被利用起来形成了一种新的开发模式:Hybrid APP。

    作为一种混合开发的模式,Hybrid APP底层依赖于Native提供的容器(UIWebview),上层使用Html&Css&JS做业务开发,底层透明化、上层多多样化,这种场景非常有利于前端介入,非常适合业务快速迭代,于是Hybrid火啦。

    本来我觉得这种开发模式既然大家都知道了,那么Hybrid就没有什么探讨的价值了,但令我诧异的是依旧有很多人对Hybrid这种模式感到陌生,这种情况在二线城市很常见,所以我这里尝试从另一个方面向各位介绍Hybrid,期望对各位正确的技术选型有所帮助。

    Hybrid发家史

    最初携程的应用全部是Native的,H5站点只占其流量很小的一部分,当时Native有200人红红火火,而H5开仅有5人左右在打酱油,后面无线团队来了一个执行力十分强的服务器端出身的leader,他为了了解前端开发,居然亲手使用jQuery Mobile开发了第一版程序,虽然很快方案便被推翻,但是H5团队开始发力,在短时间内已经赶上了Native的业务进度:

    突然有一天andriod同事跑过来告诉我们andriod中有一个方法最大树限制,可能一些页面需要我们内嵌H5的页面,于是Native与H5框架团队牵头做了第一个Hybrid项目,携程第一次出现了一套代码兼容三端的情况。这个开发效率杠杠的,团队尝到了甜头,于是乎后续的频道基本都开始了Hybrid开发,到我离开时,整个机制已经十分成熟了,而前端也有几百人了。

    场景重现

    狼厂有三大大流量APP,手机百度、百度地图、糯米APP,最近接入糯米的时候,发现他们也在做Hybrid平台化相关的推广,将静态资源打包至Native中,Native提供js调用原生应用的能力,从产品化和工程化来说做的很不错,但是有两个瑕疵:

    ① 资源全部打包至Naive中APP尺寸会增大,就算以增量机制也避免不了APP的膨胀,因为现在接入的频道较少一个频道500K没有感觉,一旦平台化后主APP尺寸会急剧增大

    ② 糯米前端框架团队封装了Native端的能力,但是没有提供配套的前端框架,这个解决方案是不完整的。很多业务已经有H5站点了,为了接入还得单独开发一套程序;而就算是新业务接入,又会面临嵌入资源必须是静态资源的限制,做出来的项目没有SEO,如果关注SEO的话还是需要再开发,从工程角度来说是有问题的。

    但从产品可接入度与产品化来说,糯米Hybrid化的大方向是很乐观的,也确实取得了一些成绩,在短时间就有很多频道接入了,随着推广进行,明年可能会形成一个大型的Hybrid平台。但是因为我也经历过推广框架,当听到他们忽悠我说性能会提高70%,与Native体验基本一致时,不知为何我居然笑了......

    总结

    如果读了上面几个故事你依旧不知道为何要使用Hybrid技术的话,我这里再做一个总结吧:

    Hybrid开发效率高、跨平台、底层本
    Hybrid从业务开发上讲,没有版本问题,有BUG能及时修复

    Hybrid是有缺点的,Hybrid体验就肯定比不上Native,所以使用有其场景,但是对于需要快速试错、快速占领市场的团队来说,Hybrid一定是不二的选择,团队生存下来后还是需要做体验更好的原生APP

    好了,上面扯了那么多没用的东西,今天的目的其实是为大家介绍Hybrid的一些设计知识,如果你认真阅读此文,可能在以下方面对你有所帮助:

    ① Hybrid中Native与前端各自的工作是什么

    ② Hybrid的交互接口如何设计

    ③ Hybrid的Header如何设计

    ④ Hybrid的如何设计目录结构以及增量机制如何实现

    ⑤ 资源缓存策略,白屏问题......

    文中是我个人的一些开发经验,希望对各位有用,也希望各位多多支持讨论,指出文中不足以及提出您的一些建议

    然后文中Andriod相关代码由我的同事明月提供,这里特别感谢明月同学对我的支持,这里扫描二维码可以下载APP进行测试:

    Andriod APP二维码:

    代码地址:

    https://github.com/yexiaochai/hybrid

    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提供的各种能力,整体架构是这样的:

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

    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 = $('<iframe style="display: none;" src="' + url + '"/>');
     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组件的实现:

    复制代码
      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 });
    复制代码

    请求类

    虽然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。

    账号系统的设计

    根据上面的设计,我们约定在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请求接口,如果没有登录则在底层唤起登录框(需要前端框架支持)

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

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

    账号信息获取

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

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

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

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

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

    ① 系统登出

    ② 访问接口提示需要登录

    ③ 调用登录接口

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

    Hybrid的资源

    目录结构

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

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

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

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

    增量机制

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

    {
      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修复。

    结语

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

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

    http://www.cnblogs.com/yexiaochai/p/4921635.html

  • 相关阅读:
    博客园
    未释放的已删除文件
    ssh连接缓慢
    剑指 Offer 38. 字符串的排列
    剑指 Offer 37. 序列化二叉树
    剑指 Offer 50. 第一个只出现一次的字符
    剑指 Offer 36. 二叉搜索树与双向链表
    剑指 Offer 35. 复杂链表的复制
    剑指 Offer 34. 二叉树中和为某一值的路径
    剑指 Offer 33. 二叉搜索树的后序遍历序列
  • 原文地址:https://www.cnblogs.com/softidea/p/4935600.html
Copyright © 2011-2022 走看看