浅谈Hybrid技术的设计与实现

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

前言

随着移动浪潮的兴起,各种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

#p#

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的交互设计。

#p#

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约定的请求模型是: 

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

这个方法执行会形成一个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场景较少,这里不予关注了。

#p#

常用交互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. }); 
  124.  
  125. Native Header组件的封装 

#p#

请求类

虽然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获取即可,这块比较靠谱。

#p#

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技术的朋友提供一些帮助,并且再次感谢明月同学的配合。

责任编辑:王雪燕 来源: 博客园
相关推荐

2018-02-23 14:44:41

负载均衡技术分类

2009-07-08 09:32:25

Java设计模式

2011-09-06 09:27:15

项目设计

2014-05-21 15:13:40

AppCanHybrid

2015-06-16 10:44:42

2011-12-26 15:19:20

聚合

2009-02-17 18:17:42

2019-08-29 10:21:07

IBM存储IBM存储

2016-09-29 12:59:54

大数据采集系统

2022-10-09 14:15:42

短链设计

2009-05-04 13:19:27

2011-11-08 11:21:00

2022-09-20 07:02:20

网络爬虫反爬虫

2009-05-18 09:11:00

IPTV融合宽带

2014-12-18 13:40:16

Web AppHybrid AppNative App

2009-07-07 15:57:29

JSP购物车

2023-05-26 08:24:17

短信渠道模型

2015-06-30 11:05:11

flexibleWebAPP设计

2020-10-23 08:31:15

Nodejs-Ipc设计实现

2022-09-12 07:17:20

redis命令redissynce
点赞
收藏

51CTO技术栈公众号