文中是我我的的一些开发经验,但愿对各位有用,也但愿各位多多支持讨论,指出文中不足以及提出您的一些建议。html
得益于近几年移动端的发展,前端早已今非昔比,从大型框架来讲angularJS、react、VueJS都有其应用场景,从工程化来讲各类配套构建工具也纷纷出世,而从前端复杂度来讲,最近几年的前端代码难度着实提高很多,从模块化的必须,到MVC的必要、再到组件化编程,一种分而治之的思想逐渐侵入前端领域,而这种种迹象均代表一个问题,前端代码如今很差写了!!!前端
抛开近几年前端交互加剧而致使的难度,咱们今天主要探讨下前端跨平台一块的痛点,也就是Hybrid多容器解决方案。react
Hybrid是一种混合开发模式,最简单的理解就是,Native会提供一个webview容器(确实不明白能够理解为iframe),而后在里面加载你的H5站点。ios
在大约三年前,当时Hybrid平台还比较少,若是一个公司前端团队比较强的话能够作到一套代码三端运行就很不错了,也就是一个H5页面同时运行在:git
① 浏览器github
② 公司IOS APP Webview容器web
③ APP Andriod Webview容器编程
再这里有个和简单iframe不一样的是,处于Native中的话,那么不少H5的表现便不太同样了,好比header一部分的UI是Native的,好比获取定位信息直接由Native给H5,在这里面会有些差别化处理,通常来讲只有保持应用层API一致,底层稍做修改便可;但也有一些特殊场景须要判断,好比,一个按钮的回调在H5站点的处理和处于Native中不同,这个时候可能就须要if else判断处理了。api
总的来讲,双容器时代持续了一阵子,而由于条件仍然比较单一,无非只是判断H5站点或者自身APP容器,因此问题也就不大。浏览器
量变到必定阶段便再也不同样了,简单从携程来讲,Hybrid的频道从最初的一个发展到如今APP中80%都是Hybrid频道,携程APP自己有一套完整的Hybrid交互规范,俨然已经再也不简单是个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 });
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的弹出提示的话,代码便有所不一样了:
咱们使用的应该是:
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 });
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的实现:
我这里工做作的多一些,考虑了微信时候的场景,可是这里业务代码暂时只考虑糯米,对应糯米的文档:
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 });
代码实现很简单,只要保持与H5使用API一致便可,这个时候再简单改下入口文件,便能适配了。
PS:注意,这里的适配只是简单实现,考虑多场景的话不能这样写代码!!!
因而咱们在糯米中便能很好的运行了
https://github.com/yexiaochai/mvc
http://yexiaochai.github.io/mvc/webapp/bus/index.html
测试糯米时请扫描第二个二维码:
这里抛出了前端多Webview容器会遇到的一些问题,并提出了一个解决思路,后续可能会有更加完整解决方案与demo出来,但愿对各位有用,如果有已经涉及到这块业务的朋友能够私下交流下。