浅谈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开发,到我离开时,整个机制已经十分成熟了,而前端也有几百人了。
场景重现
狼厂有三大@R_515_403@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】
前端框架定义了一个全局变量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',0); line-height:1.5!important">请求参数,会被Native使用 param: {},0); line-height:1.5!important">Native处理成功后回调前端的方法 callback: function (data) { } });
这个方法执行会形成一个URL,比如:
hybridschema://hybridapi?callback=hybrid_1446276509894¶m=%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才能使用的话:
代码实现
因为Native对于H5来是底层,框架&底层一般来说是不会关注业务实现的,所以真实业务中Native调用H5场景较少,这里不予关注了。
常用交互API
良好的交互设计是成功的第一步,在真实业务开发中有一些API一定会用到。
跳转
② H5跳转Native界面
③ H5新开Webview跳转H5页面,一般为做页面动画切换
如果要使用动画,按业务来说有向前与向后两种,forward&back,所以约定如下,首先是H5跳Native某一个页面
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是不允许重复的):
因为Header左边一般来说只有一个按钮,所以其对象可以使用这种形式:
为完成Native端的实现,这里会新增两个接口,向Native注册事件,以及注销事件:
Native Header组件的实现:
请求类
虽然get类请求可以用jsonp的方式绕过跨域问题,但是post请求却是真正的拦路虎,为了安全性服务器设置cors会仅仅针对几个域名,Hybrid内嵌静态资源是通过file的方式读取,这种场景使用cors就不好使了,所以每个请求需要经过Native做一层代理发出去。
这个使用场景与Header组件一致,前端框架层必须做到对业务透明化,业务事实上不必关心这个请求是由浏览器发出还是由Native发出:
1 HybridGet = function (url,param,128); line-height:1.5!important">2 }; 3 HybridPost = 4 };
真实的业务场景,会将之封装到数据请求模块,在底层做适配,在H5站点下使用ajax请求,在Native内嵌时使用代理发出,与Native的约定为: