文中是我个人的一些开发经验,希望对各位有用,也希望各位 多多支持讨论 ,指出文中 不足 以及提出您的一些 建议 。
得益于近几年移动端的发展,前端早已今非昔比,从大型框架来说angularJS、react、VueJS都有其应用场景,从工程化来说各种配套构建工具也纷纷出世,而从前端复杂度来说,最近几年的前端代码难度着实提升不少,从模块化的必须,到MVC的必要、再到组件化编程,一种分而治之的思想逐渐侵入前端领域,而这种种迹象均表明一个问题,前端代码现在不好写了!!!
抛开近几年前端交互加重而导致的难度,我们今天主要探讨下前端跨平台一块的痛点,也就是Hybrid多容器解决方案。
Hybrid是一种混合开发模式,最简单的理解就是,Native会提供一个webview容器(确实不明白可以理解为iframe),然后在里面加载你的H5站点。
在大约三年前,当时Hybrid平台还比较少,如果一个公司前端团队比较强的话可以做到一套代码三端运行就很不错了,也就是一个H5页面同时运行在:
① 浏览器
② 公司IOS APP Webview容器
③ APP Andriod Webview容器
再这里有个和简单iframe不同的是,处于Native中的话,那么很多H5的表现便不太一样了,比如header一部分的UI是Native的,比如获取定位信息直接由Native给H5,在这里面会有些差异化处理,一般来说只有保持应用层API一致,底层稍作修改即可;但也有一些特殊场景需要判断,比如,一个按钮的回调在H5站点的处理和处于Native中不一样,这个时候可能就需要if else判断处理了。
总的来说,双容器时代持续了一阵子,而因为条件仍然比较单一,无非只是判断H5站点或者自身APP容器,所以问题也就不大。
量变到一定阶段便不再一样了,简单从携程来说,Hybrid的频道从最初的一个发展到现在APP中80%都是Hybrid频道,这个时候携程APP本身有一套完整的Hybrid交互规范,这个时候携程APP已经不再简单是个APP了,而是一个Hybrid平台,开发规范一旦制定,一旦进入工厂化开发就很难更改了,除了携程各个业务团队依赖这个APP外,还有很多携程子公司乃至第三方公司依赖这个APP,那么这个时候底层若是不稳定,那么导致的问题将是连锁的、不可控的。
这种平台化的APP产品远不止携程一家,已知的就有:
① 微信APP平台
② 淘宝APP平台
③ 手机百度APP平台
④ 糯米平台
⑤ 手机QQ平台
......
国内这些“平台”都有各自问题,不论是微信一些版本不支持flex、手机百度IOS、Andriod Webview容器各种不一致,还是糯米Native默认后退不处理导致假死,都可以看出为了抢占市场,各个团队走的太急,考虑的应用场景过少,推出产品后后宣传网站写的漂亮,API看似丰富,但是光鲜的只是表面,真正形成平台后,各个业务方接入便会形成各种小概率场景,而Native发版是无力的,Native不动就只能业务开发代码适配,这个时候受苦的总是各个接入方,而导致骂声一片。
各个平台不稳定、考虑场景太少也无可厚非,毕竟Hybrid才火不到几年,各个公司真正的经验场景又很难被其它公司吸收,所以这种现象还得持续一段时间......
当然,APP底层的问题不是我们今天思考的重点,我们还是回到前端应用层。
上述平台产品虽然有各自的问题,但是其流量优势是无可比拟的!所以很多业务方、第三方公司都会接入,对于前端来说难度便增加了不少,以百度为例:
最初是前端代码运行在浏览器即可,而现在一套前端代码却需要运行在:
① 浏览器
② 自身APP
③ 百度地图APP
④ 手机百度APP
⑤ 糯米APP
而各个APP平台的Hybrid交互又完全不一致,更有甚者后期还需要微信APP、手机QQ等Hybrid平台,那么就简单一个按钮的交互都会令人头疼的!因为我们的代码中可能会出现这种东东:
1 if (shoujibaidu) { 2 //手机百度逻辑 3 4 } else if (baiduditu) { 5 //百度地图逻辑 6 7 } else if (nuomi) { 8 //糯米逻辑 9 } 10 //......其它平台逻辑
这种代码十分令人头疼,所以我们一般会封装一个方法在底层,哪个平台有差异就做特殊处理:
1 hybridCallback({ 2 //默认回调 3 callback: function() { 4 }, 5 //手机百度回调 6 shoubaicallback: function () { 7 }, 8 //...... 9 });
这个方法就是用于处理Hybrid差异而生,只有处于某一个环境,才会执行其中的回调,这其实只是一个语法糖,将判断的逻辑封装了,所以这个方案依旧很烂,如果哪天如果你要多一个容器或者少一个容器,你整个站点的代码要如何处理呢?如果代码量超过万行,这个代码可不好处理!
更好的解决方案是抽离共性,是继承,一般来说,Hybrid还是有一个很大的特点: 主要逻辑与H5一致 ,一些差异往往是显示什么,不显示什么(比如糯米中不显示H5推荐下载APP的广告),更多的是一些点击回调的响应,于是我们找到了更好的方案:
解决多容器的第一步是容器判断,一般来说,不同的Webview容器会有不同的userAgent:
//微信中UA为: Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D257 MicroMessenger/6.1.5 NetType/WIFI //浏览器中为: Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53 //糯米 Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13D15 BDNuomiAppIOS
手机百度也会包含关键字:bdbox_x.x(x.x一般是版本号),根据ua我们可以知道当前处于什么环境(ios还是Andriod)与什么平台。
如果是页面片的开发模式,一个页面往往会有一个js文件,做的好的团队这个js文件会是一个类,通过requireJS可以轻易拿到该文件,我们这里不做无用功,直接在之前代码的基础上做,有疑问的朋友请移步该文章:
【组件化开发】前端进阶篇之如何编写可维护可升级的代码
在上文中,我们将一个个页面以组件化的方式打散了,我们这里新增一个index页面,并且新增一个按钮,点击按钮弹出一个提示:
1 define([ 2 'AbstractView', 3 'text!IndexPath/tpl.layout.html' 4 ], function ( 5 AbstractView, 6 layoutHtml 7 ) { 8 return _.inherit(AbstractView, { 9 propertys: function ($super) { 10 $super(); 11 this.template = layoutHtml; 12 this.events = { 13 'click .js_clickme': 'clickAction' 14 }; 15 }, 16 17 clickAction: function () { 18 this.showMessage('显示消息'); 19 }, 20 21 initHeader: function (name) { 22 var title = '多Webview容器'; 23 this.header.set({ 24 view: this, 25 title: title, 26 back: function () { 27 console.log('回退'); 28 } 29 }); 30 } 31 }); 32 }); View Code
1 propertys: function ($super) { 2 $super(); 3 this.template = layoutHtml; 4 this.events = { 5 'click .js_clickme': 'clickAction' 6 }; 7 }, 8 9 clickAction: function () { 10 this.showMessage('显示消息'); 11 },
首先我们看看这个回调,假如我们需要做到在糯米容器中使用Native的弹出提示的话,代码便有所不同了:
http://dev.nuomi.com/#/guide/doc/api_doc/.%252Fcomdoc%252Fcomp_api%252Fcomp%252Fapi_doc%252Fbnjs_ui_doc.md
我们使用的应该是:
1 /** 2 * 使用BNJS之前,必须声明如下BNJSReady函数,确保BNJS相关属性信息及页面加载准备就绪 3 * BNJSReady直接复制使用,请勿改动 4 */ 5 var BNJSReady = function (readyCallback) { 6 if(readyCallback && typeof readyCallback == 'function'){ 7 if(window.BNJS && typeof window.BNJS == 'object' && BNJS._isAllReady){ 8 readyCallback(); 9 }else{ 10 document.addEventListener('BNJSReady', function() { 11 readyCallback(); 12 }, false) 13 } 14 } 15 }; 16 17 BNJSReady(function(){ 18 19 // 显示确定和取消按钮 20 BNJS.ui.dialog.show({ 21 title: '测试Dialog', 22 message: '我是测试Dialog~~~~', 23 ok: '确定', 24 cancel: '取消', 25 onConfirm: function() { 26 BNJS.ui.toast.show('您刚刚点击了确定按钮'); 27 }, 28 onCancel: function() { 29 BNJS.ui.toast.show('您刚刚点击了取消按钮'); 30 } 31 }); 32 33 // 仅显示'ok'按钮 34 BNJS.ui.dialog.show({ 35 title: '测试Dialog', 36 message: '我是测试Dialog~~~~', 37 ok: 'ok', 38 onConfirm: function() { 39 BNJS.ui.toast.show('您刚刚点击了ok按钮'); 40 } 41 }); 42 43 }); View Code
1 // 仅显示'ok'按钮 2 BNJS.ui.dialog.show({ 3 title: '测试Dialog', 4 message: '我是测试Dialog~~~~', 5 ok: 'ok', 6 onConfirm: function() { 7 BNJS.ui.toast.show('您刚刚点击了ok按钮'); 8 } 9 });
于是我们在index目录中新增了一个nuomi.index.js的文件,继承自index.js,并且在入口文件main_webviews(原main.js文件)中做更改:
1 define([ 2 'IndexPath/index' 3 ], function ( 4 IndexView 5 ) { 6 return _.inherit(IndexView, { 7 8 clickAction: function () { 9 BNJS.ui.dialog.show({ 10 title: '测试Dialog', 11 message: '我是测试Dialog~~~~', 12 ok: 'ok', 13 onConfirm: function () { 14 BNJS.ui.toast.show('您刚刚点击了ok按钮'); 15 } 16 }); 17 } 18 19 }); 20 });
如此,在一般浏览器中点击按钮便是H5的UI组件,在糯米中便是使用的糯米组件了,如果哪天不需要糯米这个平台将nuomi.js删除即可:
可以看到,按钮的点击已经不一样了,当然还有很多不足,比如糯米中header部分便没有做处理。
header这种组件与上述问题又不一致,这种不一致主要体现在两个方面:
① 由于底层实现问题,做不到一致
比如手机百度就不支持返回按钮定制,就连最简单的title改变都是直接监听的document.title的变化,并且Andriod还有BUG,像这种底层实现直接就抹杀的基本没法,一般来说就是把原来的header换个方式显示在页面中,可以是弧形按钮,可以是其它方式。
② header是系统级别的操作,不应该由用户控制
如同该文中对header组件的处理: 浅谈Hybrid技术的设计与实现 ,像header这一类组件,这类组件必须满足在H5站点与Hybrid中API使用一致,而底层实现各异,与之前不同的是,这里的header组件要考虑的可不止2个平台那种问题了,他可能是这样的:
ui.eader //H5站点使用 nuomi.ui.header //糯米使用 xx.ui.header //......
我们这里将场景变小,暂时只考虑糯米与H5的实现,于是会在底层多出一个header的实现:
我这里工作做的多一些,考虑了微信时候的场景,但是这里业务代码暂时只考虑糯米,对应糯米的文档:
http://dev.nuomi.com/#/guide/doc/api_doc/.%252Fcomdoc%252Fcomp_api%252Fcomp%252Fapi_doc%252Fbnjs_ui_doc.md
1 define([], function () { 2 'use strict'; 3 4 return _.inherit({ 5 6 propertys: function () { 7 }, 8 9 //全部更新 10 set: function (opts) { 11 if (!opts) return; 12 var i, len, item; 13 14 var scope = opts.view || this; 15 16 //处理返回逻辑 17 if (opts.back && typeof opts.back == 'function') { 18 BNJS.page.onBtnBackClick({ 19 callback: $.proxy(opts.back, scope) 20 }); 21 } else { 22 23 BNJS.page.onBtnBackClick({ 24 callback: function () { 25 if (history.length > 0) 26 history.back(); 27 else 28 BNJS.page.back(); 29 } 30 }); 31 } 32 33 //处理title 34 if (typeof opts.title == 'string') { 35 BNJS.ui.title.setTitle(opts.title); 36 } 37 38 //删除右上角所有按钮【1.3】 39 //每次都会清理右边所有的按钮 40 BNJS.ui.title.removeBtnAll(); 41 42 //处理右边按钮 43 if (typeof opts.right == 'object' && opts.right.length) { 44 for (i = 0, len = opts.right.length; i < len; i++) { 45 item = opts.right[i]; 46 BNJS.ui.title.addActionButton({ 47 tag: _.uniqueId(), 48 text: item.value, 49 callback: $.proxy(item.callback, scope) 50 }); 51 } 52 } 53 }, 54 55 show: function () { 56 57 }, 58 59 hide: function () { 60 61 }, 62 63 //只更新title 64 update: function (title) { 65 66 }, 67 68 initialize: function () { 69 //隐藏H5头 70 $('#headerview').hide(); 71 this.propertys(); 72 } 73 74 }); 75 76 }); View Code
代码实现很简单,只要保持与H5使用API一致即可,这个时候再简单改下入口文件,便能适配了。
PS:注意,这里的适配只是简单实现,考虑多场景的话不能这样写代码!!!
于是我们在糯米中便能很好的运行了
https://github.com/yexiaochai/mvc
http://yexiaochai.github.io/mvc/webapp/bus/index.html
测试糯米时请扫描第二个二维码:
这里抛出了前端多Webview容器会遇到的一些问题,并提出了一个解决思路,后续可能会有更加完整解决方案与demo出来,希望对各位有用,若是有已经涉及到这块业务的朋友可以私下交流下。