>

每个看似简单的移动页面背后往往会隐藏5个以上

- 编辑:澳门博发娱乐官网 -

每个看似简单的移动页面背后往往会隐藏5个以上

浅谈Hybrid技术的设计与实现

2015/11/05 · 基础技术 · Hybrid

原文出处: 叶小钗(@欲苍穹)   

浅谈Hybrid技术的设计与实现第三弹——落地篇

2016/10/25 · 基础技术 · Hybrid

原文出处: 叶小钗(@欲苍穹)   

根据之前的介绍,大家对前端与Native的交互应该有一些简单的认识了,很多朋友就会觉得这个交互很简单嘛,其实并不难嘛,事实上单从Native与前端的交互来说就那点东西,真心没有太多可说的,但要真正做一个完整的Hybrid项目却不容易,要考虑的东西就比较多了,单从这个交互协议就有:

① URL Schema

② JavaScriptCore

两种,到底选择哪种方式,每种方式有什么优势,都是我们需要深度挖掘的,而除此之外,一个Hybrid项目还应该具有以下特性:

① 扩展性好——依靠好的约定

② 开发效率高——依赖公共业务

③ 交互体验好——需要解决各种兼容问题

我们在实际工作中如何落地一个Hybrid项目,如何推动一个项目的进行,这是本次我们要讨论的,也希望对各位有用。

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

设计类博客


iOS博客

Android博客

代码地址:

因为IOS不能扫码下载了,大家自己下载下来用模拟器看吧,下面开始今天的内容。

总体概述在第一章,有兴趣大家去看

细节设计在第二章,有兴趣大家去看

本章主要为打补丁

谈一谈前端多容器(多webview平台)处理方案,前端webview

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

前端进阶篇之如何编写可维护可升级的代码

2015/10/16 · JavaScript · 1 评论 · 代码

原文出处: 叶小钗(@欲苍穹)   

前言

我还在携程的做业务的时候,每个看似简单的移动页面背后往往会隐藏5个以上的数据请求,其中最过复杂的当属机票与酒店的订单填写业务代码

这里先看看比较“简单”的机票代码:

sbf282.com 1

然后看看稍微复杂的酒店业务逻辑:

sbf282.com 2

机票一个页面的代码量达到了5000行代码,而酒店的代码竟然超过了8000行,这里还不包括模板(html)文件!!!

然后初略看了机票的代码,就该页面可能发生的接口请求有19个之多!!!而酒店的的交互DOM事件基本多到了令人发指的地步:

sbf282.com 3

当然,机票团队的交互DOM事件已经多到了我笔记本不能截图了:

JavaScript

events: { 'click .js_check_invoice_type': 'checkInvoiceType', //切换发票类型 'click .flight-hxtipshd': 'huiXuanDesc', //惠选说明 'click .js_ListReload': 'hideNetError', 'click #js_return': 'backAction', //返回列表页 'click div[data-rbtType]': 'showRebate', //插烂返现说明 'click #paybtn .j_btn': 'beforePayAction', //提交订单 //flightDetailsStore, passengerQueryStore, mdStore, postAddressStorage, userStore, flightDeliveryStore 'click .flight-loginbtn2': 'bookLogin', //登录 'input #linkTel': 'setContact', //保存用户输入的联系人 'click #addPassenger .flight-labq': 'readmeAction',//姓名帮助 'click .jsDelivery': 'selDelivery', //选择配送方式 'click #jsViewCoupons': 'viewCoupons', //查看消费券使用说明 //flightDetailsStore // 'click .j_refundPolicy': 'fanBoxAction', //查看返现信息 //'click .flight-bkinfo-tgq .f-r': 'tgBoxAction', //查看退改签 'click .js_del_tab': 'showDelListUI', //配送方式 // 'click .js_del_cost .flight-psf i': 'selectPaymentType', // 选择快递费用方式 'click #js_addrList': 'AddrListAction', //选择地址 'click #date-picker': 'calendarAction', //取票日期 //airportDeliveryStore 'click #done-address': 'zqinairselect', //取票柜台 'click #selectCity': 'selectCityAction', //选择城市 'click #date-zqtime': 'showZqTimeUI', //取票时间 //airportDeliveryStore 'click #jsinsure': 'viewInsure', //保险说明 'click #js_invoice_title': 'inTitleChangeWrp', //发票抬头更改 // userStore, flightOrderInfoInviceStore, flightOrderStore //don't move outside 'click #js_invoice_title_div': 'inTitleChangeWrp', 'click .flight-icon-arrrht': 'showinTitleList', //‘+’号,跳转发票抬头列表 //userStore, invoiceURLStore 'focusin #linkTel': 'telInput', 'focusout #linkTel': 'telInputFinish', 'touchstart input': 'touchStartAction', // 处理Android手机上点击不灵敏问题 'click #package .flight-arrrht': 'packageSelect', 'focusin input': 'hideErrorTips', 'click #dist_text_div': 'hideErrorTips', 'click .j_PackageNotice': 'toggletips', 'click .j_AnnouncementNotice': 'toggleNotice', 'click #travalPackageDesc': 'forwardToTravalPackage', //don't move into child modules 'click #airInsureDesc': 'showAirInsureDesc', 'click #paybtn': 'orderDetailAction',//价格明细 'click .J_retriveVerifyCodeBtn': 'getVerifyCode', 'click .J_toPay': 'toPayAction', 'click .J_closeVerifyCode': 'closeVerifyCodePopup', 'keyup .J_verifyCodePopup input': 'setToPayBtnStatus', 'click .js_flight_seat': 'selectRecommendCabin', // 选择推荐仓位 'click .j_changeFlight': 'changeFlightAction', // 推荐航班弹层中更改航班 'focusin input:not([type=tel])': 'adjustInputPosition', // iphone5/5s ios8搜狗输入法遮住input 'click .js_addr,#js_addr_div': 'editDeliverAddress',//报销凭证,详细地址编辑 'click .js_showUserInfo': 'showUserInfo', // add by hkhu v2.5.9 'click #logout': 'logout', // add by hkhu v2.5.9 'click #gotoMyOrder': 'gotoMyOrder', // add by hkhu v2.5.9 'touchstart #logout': function (e) { $(e.currentTarget).addClass('current'); }, 'touchstart #gotoMyOrder': function (e) { $(e.currentTarget).addClass('current'); }, 'click .js_buddypayConfirm': 'buddypayConfirmed', 'click .js_pickupTicket': 'viewPickUp', //261接送机券说明 'click .flt-bking-logintips': 'closelogintips'//关闭接送机券提示 },

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
events: {
'click .js_check_invoice_type': 'checkInvoiceType', //切换发票类型
'click .flight-hxtipshd': 'huiXuanDesc', //惠选说明
'click .js_ListReload': 'hideNetError',
'click #js_return': 'backAction', //返回列表页
'click div[data-rbtType]': 'showRebate', //插烂返现说明
'click #paybtn .j_btn': 'beforePayAction', //提交订单                        //flightDetailsStore, passengerQueryStore, mdStore, postAddressStorage, userStore, flightDeliveryStore
'click .flight-loginbtn2': 'bookLogin', //登录
'input #linkTel': 'setContact', //保存用户输入的联系人
'click #addPassenger .flight-labq': 'readmeAction',//姓名帮助
'click .jsDelivery': 'selDelivery', //选择配送方式
'click #jsViewCoupons': 'viewCoupons', //查看消费券使用说明                                                  //flightDetailsStore
// 'click .j_refundPolicy': 'fanBoxAction', //查看返现信息
//'click .flight-bkinfo-tgq .f-r': 'tgBoxAction', //查看退改签
'click .js_del_tab': 'showDelListUI', //配送方式
//            'click .js_del_cost .flight-psf i': 'selectPaymentType', // 选择快递费用方式
'click #js_addrList': 'AddrListAction', //选择地址
'click #date-picker': 'calendarAction', //取票日期                                                                    //airportDeliveryStore
'click #done-address': 'zqinairselect', //取票柜台
'click #selectCity': 'selectCityAction', //选择城市
'click #date-zqtime': 'showZqTimeUI', //取票时间                                                                        //airportDeliveryStore
'click #jsinsure': 'viewInsure', //保险说明
'click #js_invoice_title': 'inTitleChangeWrp', //发票抬头更改                // userStore, flightOrderInfoInviceStore, flightOrderStore    //don't move outside
'click #js_invoice_title_div': 'inTitleChangeWrp',
'click .flight-icon-arrrht': 'showinTitleList', //‘+’号,跳转发票抬头列表                 //userStore, invoiceURLStore
'focusin #linkTel': 'telInput',
'focusout #linkTel': 'telInputFinish',
'touchstart input': 'touchStartAction', // 处理Android手机上点击不灵敏问题
'click #package .flight-arrrht': 'packageSelect',
'focusin input': 'hideErrorTips',
'click #dist_text_div': 'hideErrorTips',
'click .j_PackageNotice': 'toggletips',
'click .j_AnnouncementNotice': 'toggleNotice',
'click #travalPackageDesc': 'forwardToTravalPackage',       //don't move into child modules
'click #airInsureDesc': 'showAirInsureDesc',
'click #paybtn': 'orderDetailAction',//价格明细
'click .J_retriveVerifyCodeBtn': 'getVerifyCode',
'click .J_toPay': 'toPayAction',
'click .J_closeVerifyCode': 'closeVerifyCodePopup',
'keyup .J_verifyCodePopup input': 'setToPayBtnStatus',
'click .js_flight_seat': 'selectRecommendCabin', // 选择推荐仓位
'click .j_changeFlight': 'changeFlightAction', // 推荐航班弹层中更改航班
'focusin input:not([type=tel])': 'adjustInputPosition', // iphone5/5s ios8搜狗输入法遮住input
'click .js_addr,#js_addr_div': 'editDeliverAddress',//报销凭证,详细地址编辑
'click .js_showUserInfo': 'showUserInfo', // add by hkhu v2.5.9
'click #logout': 'logout', // add by hkhu v2.5.9
'click #gotoMyOrder': 'gotoMyOrder', // add by hkhu v2.5.9
'touchstart #logout': function (e) { $(e.currentTarget).addClass('current'); },
'touchstart #gotoMyOrder': function (e) { $(e.currentTarget).addClass('current'); },
'click .js_buddypayConfirm': 'buddypayConfirmed',
'click .js_pickupTicket': 'viewPickUp', //261接送机券说明
'click .flt-bking-logintips': 'closelogintips'//关闭接送机券提示
},

就这种体量的页面,如果需要迭代需求、打BUG补丁的话,我敢肯定的说,一个BUG的修复很容易引起其它BUG,而上面还仅仅是其中一个业务页面,后面还有强大而复杂的前端框架呢!如此复杂的前端代码维护工作可不是开玩笑的!

PS:说道此处,不得不为携程的前端水平点个赞,业内少有的单页应用,一套代码H5&Hybrid同时运行不说,还解决了SEO问题,嗯,很赞。

如何维护这种页面,如何设计这种页面是我们今天讨论的重点,而上述是携程合并后的代码,他们两个团队的设计思路不便在此处展开。

今天,我这里提供一个思路,认真阅读此文可能在以下方面对你有所帮助:

JavaScript

① 如何将一个复杂的页面拆分为一个个独立的页面组件模块 ② 如何将分拆后的业务组件模块重新合为一个完整的页面 ③ 从重构角度看组件化开发带来的好处 ④ 从前端优化的角度看待组件化开发

1
2
3
4
① 如何将一个复杂的页面拆分为一个个独立的页面组件模块
② 如何将分拆后的业务组件模块重新合为一个完整的页面
③ 从重构角度看组件化开发带来的好处
④ 从前端优化的角度看待组件化开发

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

由于该项目涉及到了项目拆分与合并,基本属于一个完整的前端工程化案例了,所以将之放到了github上:https://github.com/yexiaochai/mvc

其中工程化一块的代码,后续会由另一位小伙伴持续更新,如果该文对各位有所帮助的话请各位给项目点个赞、加颗星:)

我相信如果是中级水平的前端,认真阅读此文一定会对你有一点帮助滴。

前言

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

sbf282.com 4sbf282.com 5sbf282.com 6

突然有一天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技术的话,我这里再做一个总结吧:

JavaScript

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

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

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

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

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

② Hybrid的交互接口如何设计

③ Hybrid的Header如何设计

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

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

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

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

Andriod APP二维码:

sbf282.com 7

代码地址:

边界问题

在我们使用Hybrid技术前要注意一个边界问题,什么项目适合Hybrid什么项目不适合,这个要搞清楚,适合Hybrid的项目为:

① 有60%以上的业务为H5

② 对更新(开发效率)有一定要求的APP

不适合使用Hybrid技术的项目有以下特点:

① 只有20%不到的业务使用H5做

② 交互效果要求较高(动画多)

任何技术都有适用的场景,千万不要妄想推翻已有APP的业务用H5去替代,最后会证明那是自讨苦吃,当然如果仅仅想在APP里面嵌入新的实验性业务,这个是没问题的。

双容器

得益于近几年移动端的发展,前端早已今非昔比,从大型框架来说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容器,所以问题也就不大。

一个实际的场景

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

sbf282.com 8

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

交互约定

根据之前的学习,我们知道与Native交互有两种交互:

① URL Schema

② JavaScriptCore

而两种方式在使用上各有利弊,首先来说URL Schema是比较稳定而成熟的,如果使用上文中提到的“ajax”交互方式,会比较灵活;而从设计的角度来说JavaScriptCore似乎更加合理,但是我们在实际使用中却发现,注入的时机得不到保障。

iOS同事在实体JavaScriptCore注入时,我们的原意是在webview载入前就注入所有的Native能力,而实际情况是页面js已经执行完了才被注入,这里会导致Hybrid交互失效,如果你看到某个Hybrid平台,突然header显示不正确了,就可能是这个问题导致,所以JavaScriptCore就被我们弃用了。

JavaScript

JavaScriptCore可能导致的问题: ① 注入时机不唯一(也许是BUG) ② 刷新页面的时候,JavaScriptCore的注入在不同机型表现不一致,有些就根本不注入了,所以全部hybrid交互失效

1
2
3
JavaScriptCore可能导致的问题:
① 注入时机不唯一(也许是BUG)
② 刷新页面的时候,JavaScriptCore的注入在不同机型表现不一致,有些就根本不注入了,所以全部hybrid交互失效

如果非要使用JavaScriptCore,为了解决这一问题,我们做了一个兼容,用URL Schema的方式,在页面逻辑载入之初执行一个命令,将native的一些方式重新载入,比如:

JavaScript

_.requestHybrid({ tagname: 'injection' });

1
2
3
_.requestHybrid({
     tagname: 'injection'
});

这个能解决一些问题,但是有些初始化就马上要用到的方法可能就无力了,比如:

① 想要获取native给予的地理信息

② 想要获取native给予的用户信息(直接以变量的方式获取)

作为生产来讲,我们还是求稳,所以最终选择了URL Schema。

明白了基本的边界问题,选取了底层的交互方式,就可以开始进行初步的Hybrid设计了,但是这离一个可用于生产,可离落地的Hybrid方案还比较远。

多容器

量变到一定阶段便不再一样了,简单从携程来说,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底层的问题不是我们今天思考的重点,我们还是回到前端应用层。

演示地址

代码仓促,可能会有BUG哦:)

代码地址:

Hybrid交互设计

Hybrid的交互无非是Native调用前端页面的JS方法,或者前端页面通过JS调用Native提供的接口,两者交互的桥梁皆Webview:

sbf282.com 9

app自身可以自定义url schema,并且把自定义的url注册在调度中心, 例如

  • ctrip://wireless 打开携程App
  • weixin:// 打开微信

我们JS与Native通信一般就是创建这类URL被Native捕获处理,后续也出现了其它前端调用Native的方式,但可以做底层封装使其透明化,所以重点以及是如何进行前端与Native的交互设计。

账号体系

一般来说,一个公司的账号体系健壮灵活程度会很大程度反映出这个研发团队的整体实力:

① 统一的鉴权认证

② 短信服务图形验证码的处理

③ 子系统的权限设计、公共的用户信息导出

④ 第三方接入方案

⑤ 接入文档输出

⑥ ……

这个技术方案,有没有是一回事(说明没思维),有几套是一回事(说明比较乱,技术不统一),对外的一套做到了什么程度又是一回事,当然这个不是我们讨论的重点,而账号体系也是Hybrid设计中不可或缺的一环。

账号体系涉及了接口权限控制、资源访问控制,现在有一种方案是,前端代码不做接口鉴权,账号一块的工作全部放到native端。

多容器与前端

上述平台产品虽然有各自的问题,但是其流量优势是无可比拟的!所以很多业务方、第三方公司都会接入,对于前端来说难度便增加了不少,以百度为例:

sbf282.com 10

最初是前端代码运行在浏览器即可,而现在一套前端代码却需要运行在:

① 浏览器

② 自身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的广告),更多的是一些点击回调的响应,于是我们找到了更好的方案:

sbf282.com 11

页面基本构成

因为订单填写页一般有密度,我这里挑选相对复杂而又没有密度的产品列表页来做说明,其中框架以及业务代码已经做过抽离,不会包含敏感信息,一些优化后续会同步到开源blade框架中去。

我们这里列表页的首屏页面如下:

sbf282.com 12

简单来说组成如下:

① 框架级别UI组件UIHeader,头部组件

② 点击日期会出框架级别UI,日历组件UICalendar

③ 点击出发时段、出发汽车站、到达汽车站,皆会出框架级别UI

④ header下面的日期工具栏需要作为独立的业务模块

⑤ 列表区域可以作为独立的业务模块,但是与主业务靠太近,不太适合

⑥ 出发时段、出发汽车站、到达汽车站皆是独立的业务模块

一个页面被我们拆分成了若干个小模块,我们只需要关注模块内部的交互实现,而包括业务模块的通信,业务模块的样式,业务模块的重用,暂时有以下约定:

JavaScript

① 单个页面的样式全部写在一个文件中,比如list里面所有模块对应的是list.css ② 模块之间采用观察者模式观察数据实体变化,以数据为媒介通信 ③ 一般来说业务模块不可重用,如果有重用的模块,需要分离到common目录中,因为我们今天不考虑common重用,这块暂时不予理睬

1
2
3
① 单个页面的样式全部写在一个文件中,比如list里面所有模块对应的是list.css
② 模块之间采用观察者模式观察数据实体变化,以数据为媒介通信
③ 一般来说业务模块不可重用,如果有重用的模块,需要分离到common目录中,因为我们今天不考虑common重用,这块暂时不予理睬

这里有些朋友可能认为单个模块的CSS以及image也应该参与独立,我这里不太同意,业务页面样式粒度太细的话会给设计带来不小的麻烦,这里再以通俗的话来说:尼玛,我CSS功底一般,拆分的太细,对我来说难度太高……

JS to Native

Native在每个版本会提供一些API,前端会有一个对应的框架团队对其进行封装,释放业务接口。比如糯米对外的接口是这样的:

JavaScript

BNJS.http.get();//向业务服务器拿请求据【1.0】 1.3版本接口有扩展 BNJS.http.post();//向业务服务器提交数据【1.0】 BNJS.http.sign();//计算签名【1.0】 BNJS.http.getNA();//向NA服务器拿请求据【1.0】 1.3版本接口有扩展 BNJS.http.postNA();//向NA服务器提交数据【1.0】 BNJS.http.getCatgData();//从Native本地获取筛选数据【1.1】

1
2
3
4
5
6
BNJS.http.get();//向业务服务器拿请求据【1.0】 1.3版本接口有扩展
BNJS.http.post();//向业务服务器提交数据【1.0】
BNJS.http.sign();//计算签名【1.0】
BNJS.http.getNA();//向NA服务器拿请求据【1.0】 1.3版本接口有扩展
BNJS.http.postNA();//向NA服务器提交数据【1.0】
BNJS.http.getCatgData();//从Native本地获取筛选数据【1.1】

JavaScript

BNJSReady(function(){ BNJS.http.post({ url : '', params : { msg : '测试post', contact : '18721687903' }, onSuccess : function(res){ alert('发送post请求成功!'); }, onFail : function(res){ alert('发送post请求失败!'); } }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BNJSReady(function(){
    BNJS.http.post({
        url : 'http://cp01-testing-tuan02.cp01.baidu.com:8087/naserver/user/feedback',
        params : {
            msg : '测试post',
            contact : '18721687903'
        },
        onSuccess : function(res){
            alert('发送post请求成功!');
        },
        onFail : function(res){
            alert('发送post请求失败!');
        }
    });
});

前端框架定义了一个全局变量BNJS作为Native与前端交互的对象,只要引入了糯米提供的这个JS库,并且在糯米封装的Webview容器中,前端便获得了调用Native的能力,我揣测糯米这种设计是因为这样便于第三方团队的接入使用,手机百度有一款轻应用框架也走的这种路线:

JavaScript

clouda.mbaas.account //释放了clouda全局变量

1
clouda.mbaas.account //释放了clouda全局变量

这样做有一个前提是,Native本身已经十分稳定了,很少新增功能了,否则在直连情况下就会面临一个尴尬,因为web站点永远保持最新的,就会在一些低版本容器中调用了没有提供的Native能力而报错。

native代理请求

在H5想要做某一块老的App业务,这个APP80%以上的业务都是Native做的,这类APP在接口方面就没有考虑过H5的感受,会要求很多信息如:

① 设备号

② 地理信息

③ 网络情况

④ 系统版本

有很多H5拿不到或者不容易拿到的公共信息,因为H5做的往往是一些比较小的业务,像什么个人主页之类的不重要的业务,Server端可能不愿意提供额外的接口适配,而使用额外的接口还有可能打破他们统一的某些规则;加之native对接口有自己的一套公共处理逻辑,所以便出了Native代理H5发请求的方案,公共参数会由Native自动带上。

JavaScript

//暂时只关注hybrid调试,后续得关注三端匹配 _.requestHybrid({ tagname: 'apppost', param: { url: this.url, param: params }, callback: function (data) { scope.baseDataValidate(data, onComplete, onError); } });

1
2
3
4
5
6
7
8
9
10
11
12
//暂时只关注hybrid调试,后续得关注三端匹配
_.requestHybrid({
     tagname: 'apppost',
     param: {
         url: this.url,
         param: params
     },
     callback: function (data) {
         scope.baseDataValidate(data, onComplete, onError);
     }
});

这种方案有一些好处,接口统一,前端也不需要关注接口权限验证,但是这个会带给前端噩梦!

前端相对于native一个很大的优点,就是调试灵活,这种代理请求的方式,会限制请求只能在APP容器中生效,对前端调试造成了很大的痛苦

1
前端相对于native一个很大的优点,就是调试灵活,这种代理请求的方式,会限制请求只能在APP容器中生效,对前端调试造成了很大的痛苦

从真实的生产效果来说,也是很影响效率的,容易导致后续前端再也不愿意做那个APP的业务了,所以使用要慎重……

多容器解决方案

不好的做法

不好的这个事情其实是相对的,因为不好的做法一般是比较简单的做法,对于一次性项目或者业务比较简单的页面来说反而是好的做法,比如这里的业务逻辑可以这样写:

JavaScript

define(['AbstractView', 'list.layout.html', 'list.html', 'BusModel', 'BusStore', 'UICalendarBox', 'UILayerList', 'cUser', 'UIToast'], function (AbstractView, layoutHtml, listTpl, BusModel, BusStore, UICalendarBox, UILayerList, cUser, UIToast) { return _.inherit(AbstractView, { propertys: function ($super) { $super(); //一堆基础属性定义 //...... //交互业务逻辑 this.events = { 'click .js_pre_day': 'preAction', //点击前一天触发 'click .js_next_day': 'nextAction', //点击后一天触发 'click .js_bus_list li': 'toBooking', //点击列表项目触发 'click .js_show_calendar': 'showCalendar', //点击日期项出日历组件 'click .js_show_setoutdate': 'showSetoutDate', //筛选出发时段 'click .js_show_setstation': 'showStation', //筛选出发站 'click .js_show_arrivalstation': 'showArrivalStation', //筛选到达站 //迭代需求,增加其它频道入口 'click .js-list-tip': function () {} }; }, //初始化头部标题栏 initHeader: function (t) { }, //首次dom渲染后,初始化后续会用到的所有dom元素,以免重复获取 initElement: function () {}, showSetoutDate: function () {}, showStation: function () {}, showArrivalStation: function () {}, showCalendar: function () {}, preAction: function (e) {}, nextAction: function () {}, toBooking: function (e) {}, listInit: function () {}, bindScrollEvent: function () {}, unbindScrollEvent: function () { }, addEvent: function () { this.on('onShow', function () { //当页面渲染结束,需要做的初始化操作,比如渲染页面 this.listInit(); //...... }); this.on('onHide', function () { this.unbindScrollEvent(); }); } }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
define(['AbstractView', 'list.layout.html', 'list.html', 'BusModel', 'BusStore', 'UICalendarBox', 'UILayerList', 'cUser', 'UIToast'],
function (AbstractView, layoutHtml, listTpl, BusModel, BusStore, UICalendarBox, UILayerList, cUser, UIToast) {
    return _.inherit(AbstractView, {
        propertys: function ($super) {
            $super();
            //一堆基础属性定义
            //......
            //交互业务逻辑
            this.events = {
                'click .js_pre_day': 'preAction', //点击前一天触发
                'click .js_next_day': 'nextAction', //点击后一天触发
                'click .js_bus_list li': 'toBooking', //点击列表项目触发
                'click .js_show_calendar': 'showCalendar', //点击日期项出日历组件
                'click .js_show_setoutdate': 'showSetoutDate', //筛选出发时段
                'click .js_show_setstation': 'showStation', //筛选出发站
                'click .js_show_arrivalstation': 'showArrivalStation', //筛选到达站
                //迭代需求,增加其它频道入口
                'click .js-list-tip': function () {}
            };
        },
        //初始化头部标题栏
        initHeader: function (t) { },
        //首次dom渲染后,初始化后续会用到的所有dom元素,以免重复获取
        initElement: function () {},
        showSetoutDate: function () {},
        showStation: function () {},
        showArrivalStation: function () {},
        showCalendar: function () {},
        preAction: function (e) {},
        nextAction: function () {},
        toBooking: function (e) {},
        listInit: function () {},
        bindScrollEvent: function () {},
        unbindScrollEvent: function () { },
        addEvent: function () {
            this.on('onShow', function () {
                //当页面渲染结束,需要做的初始化操作,比如渲染页面
                this.listInit();
                //......
            });
            this.on('onHide', function () {
                this.unbindScrollEvent();
            });
        }
    });
});

根据之前的经验,如果仅仅包含这些业务逻辑,这样写代码问题不是非常大,代码量预计在800行左右,但是为了完成完整的业务逻辑,我们这里马上产生了新的需求。

API式交互

手白、糯米底层如何做我们无从得知,但我们发现调用Native API接口的方式和我们使用AJAX调用服务器端提供的接口是及其相似的:

sbf282.com 13

这里类似的微薄开放平台的接口是这样定义的:

粉丝服务(新手接入指南)

读取接口

接收消息

接收用户私信、关注、取消关注、@等消息接口

写入接口

发送消息

向用户回复私信消息接口

生成带参数的二维码

生成带参数的二维码接口

我们要做的就是通过一种方式创建ajax请求即可:

JavaScript

1
https://api.weibo.com/2/statuses/public_timeline.json

所以我在实际设计Hybrid交互模型时,是以接口为单位进行设计的,比如获取通讯录的总体交互是:

sbf282.com 14

注入cookie

前端比较通用的权限标志还是用cookie做的,所以Hybrid比较成熟的方案仍旧是注入cookie,这里的一个前提就是native&H5有一套统一的账号体系(统一的权限校验系统)。

因为H5使用的webview可以有独立的登录态,如果不加限制太过混乱难以维护,比如:

我们在qq浏览器中打开携程的网站,携程站内第三方登录可以唤起qq,然后登录成功;完了qq浏览器本来也有一个登录态,发现却没有登录,点击一键登录的时候再次唤起了qq登录。

当然,qq作为一个浏览器容器,不应该关注业务的登录,他这样做是没问题的,但是我们自己的一个H5子应用如果登录了的话,便希望将这个登录态同步到native,这里如果native去监控cookie的变化就太复杂了,通用的方案是:

Hybrid APP中,所有的登录走Native提供的登录框

1
Hybrid APP中,所有的登录走Native提供的登录框

每次打开webview native便将当前登录信息写入cookie中,由此前端就具有登录态了,登录框的唤起在接口处统一处理:

JavaScript

/* 无论成功与否皆会关闭登录框 参数包括: success 登录成功的回调 error 登录失败的回调 url 如果没有设置success,或者success执行后没有返回true,则默认跳往此url */ HybridUI.Login = function (opts) { }; //=> requestHybrid({ tagname: 'login', param: { success: function () { }, error: function () { }, url: '...' } }); //与登录接口一致,参数一致 HybridUI.logout = function () { };

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

容器判断

解决多容器的第一步是容器判断,一般来说,不同的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)与什么平台。

需求迭代

因为我这里的班次列表,最初是没有URL参数,所以根本无法产出班次列表,页面上所有组件模块都是摆设,于是这里新增一个需求:

JavaScript

当url没有出发-到达相关参数信息时,默认弹出出发城市到达城市选择框

1
当url没有出发-到达相关参数信息时,默认弹出出发城市到达城市选择框

于是,我们这里会新增一个简单的弹出层:

sbf282.com 15

这个看似简单的弹出层,背后却隐藏了一个巨大的陷阱,因为点击出发或者到达时会出城市列表,而城市列表本身就是一个比较复杂的业务:

sbf282.com 16

于是页面的组成发生了改变:

① 本身业务逻辑约800行代码

② 新增出发到达筛选弹出层

③ 出发城市页面,预计300行代码

而弹出层的新增对业务本身造成了深远的影响,本来url是不带有业务参数的,但是点击了弹出层的确定按钮,需要改变URL参数,并且刷新本身页面的数据,于是简单的一个弹出层新增直接将页面的复杂程度提升了一倍。

于是该页面代码轻轻松松破千了,后续需求迭代js代码量破2000仅仅是时间问题,到时候维护便复杂了,页面复杂无规律的DOM操作将会令你焦头烂额,这个时候组件化开发的优势便得以体现了,于是下面进入组件化开发的设计。

格式约定

交互的第一步是设计数据格式,这里分为请求数据格式与响应数据格式,参考ajax的请求模型大概是:

$.ajax(options) ⇒ XMLHttpRequest type (默认值:"GET") HTTP的请求方法(“GET”, “POST”, or other)。 url (默认值:当前url) 请求的url地址。 data (默认值:none) 请求中包含的数据,对于GET请求来说,这是包含查询字符串的url地址,如果是包含的是object的话,$.param会将其转化成string。

1
2
3
4
$.ajax(options) ⇒ XMLHttpRequest
type (默认值:"GET") HTTP的请求方法(“GET”, “POST”, or other)。
url (默认值:当前url) 请求的url地址。
data (默认值:none) 请求中包含的数据,对于GET请求来说,这是包含查询字符串的url地址,如果是包含的是object的话,$.param会将其转化成string。

所以我这边与Native约定的请求模型是:

JavaScript

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

1
2
3
4
5
6
7
8
9
requestHybrid({
  //创建一个新的webview对话框窗口
  tagname: 'hybridapi',
  //请求参数,会被Native使用
  param: {},
  //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)调用之

数据返回的格式约定是:

JavaScript

{ data: {}, errno: 0, msg: "success" }

1
2
3
4
5
{
  data: {},
  errno: 0,
  msg: "success"
}

真实的数据在data对象中,如果errno不为0的话,便需要提示msg,这里举个例子如果错误码1代表该接口需要升级app才能使用的话:

JavaScript

{ data: {}, errno: 1, msg: "APP版本过低,请升级APP版本" }

1
2
3
4
5
{
  data: {},
  errno: 1,
  msg: "APP版本过低,请升级APP版本"
}

代码实现

这里给一个简单的代码实现,真实代码在APP中会有所变化:

JavaScript

window.Hybrid = window.Hybrid || {}; var bridgePostMsg = function (url) { if ($.os.ios) { window.location = url; } else { var ifr = $('<iframe style="display: none;" src="' + url + '"/>'); $('body').append(ifr); setTimeout(function () { ifr.remove(); }, 1000) } }; var _getHybridUrl = function (params) { var k, paramStr = '', url = 'scheme://'; url += params.tagname + '?t=' + new Date().getTime(); //时间戳,防止url不起效 if (params.callback) { url += '&callback=' + params.callback; delete params.callback; } if (params.param) { paramStr = typeof params.param == 'object' ? JSON.stringify(params.param) : params.param; url += '¶m=' + encodeURIComponent(paramStr); } return url; }; var requestHybrid = function (params) { //生成唯一执行函数,执行后销毁 var tt = (new Date().getTime()); var t = 'hybrid_' + tt; var tmpFn; //处理有回调的情况 if (params.callback) { tmpFn = params.callback; params.callback = t; window.Hybrid[t] = function (data) { tmpFn(data); delete window.Hybrid[t]; } } bridgePostMsg(_getHybridUrl(params)); }; //获取版本信息,约定APP的navigator.userAgent版本包含版本信息:scheme/xx.xx.xx var getHybridInfo = function () { var platform_version = {}; var na = navigator.userAgent; var info = na.match(/scheme/d.d.d/); if (info && info[0]) { info = info[0].split('/'); if (info && info.length == 2) { platform_version.platform = info[0]; platform_version.version = info[1]; } } return platform_version; };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
window.Hybrid = window.Hybrid || {};
var bridgePostMsg = function (url) {
    if ($.os.ios) {
        window.location = url;
    } else {
        var ifr = $('<iframe style="display: none;" src="' + url + '"/>');
        $('body').append(ifr);
        setTimeout(function () {
            ifr.remove();
        }, 1000)
    }
};
var _getHybridUrl = function (params) {
    var k, paramStr = '', url = 'scheme://';
    url += params.tagname + '?t=' + new Date().getTime(); //时间戳,防止url不起效
    if (params.callback) {
        url += '&callback=' + params.callback;
        delete params.callback;
    }
    if (params.param) {
        paramStr = typeof params.param == 'object' ? JSON.stringify(params.param) : params.param;
        url += '&param=' + encodeURIComponent(paramStr);
    }
    return url;
};
var requestHybrid = function (params) {
    //生成唯一执行函数,执行后销毁
    var tt = (new Date().getTime());
    var t = 'hybrid_' + tt;
    var tmpFn;
 
    //处理有回调的情况
    if (params.callback) {
        tmpFn = params.callback;
        params.callback = t;
        window.Hybrid[t] = function (data) {
            tmpFn(data);
            delete window.Hybrid[t];
        }
    }
    bridgePostMsg(_getHybridUrl(params));
};
//获取版本信息,约定APP的navigator.userAgent版本包含版本信息:scheme/xx.xx.xx
var getHybridInfo = function () {
    var platform_version = {};
    var na = navigator.userAgent;
    var info = na.match(/scheme/d.d.d/);
 
    if (info && info[0]) {
        info = info[0].split('/');
        if (info && info.length == 2) {
            platform_version.platform = info[0];
            platform_version.version = info[1];
        }
    }
    return platform_version;
};

因为Native对于H5来是底层,框架&底层一般来说是不会关注业务实现的,所以真实业务中Native调用H5场景较少,这里不予关注了。

账号切换&注销

账户注销本没有什么注意点,但是因为H5 push了一个个webview页面,这个重新登录后这些页面怎么处理是个问题。

我们这边设计的是一旦重新登录或者注销账户,所有的webview都会被pop掉,然后再新开一个页面,就不会存在一些页面展示怪异的问题了。

前端实现

如果是页面片的开发模式,一个页面往往会有一个js文件,做的好的团队这个js文件会是一个类,通过requireJS可以轻易拿到该文件,我们这里不做无用功,直接在之前代码的基础上做,有疑问的朋友请移步该文章:

【组件化开发】前端进阶篇之如何编写可维护可升级的代码

在上文中,我们将一个个页面以组件化的方式打散了,我们这里新增一个index页面,并且新增一个按钮,点击按钮弹出一个提示:

sbf282.com 17

sbf282.com 18

sbf282.com 19 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的弹出提示的话,代码便有所不同了:

我们使用的应该是:

sbf282.com 20 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文件)中做更改:

sbf282.com 21

 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删除即可:

sbf282.com 22

sbf282.com 23

可以看到,按钮的点击已经不一样了,当然还有很多不足,比如糯米中header部分便没有做处理。

准备工作

本文由胜博发-前端发布,转载请注明来源:每个看似简单的移动页面背后往往会隐藏5个以上