葡京网投哪个正规 > 新葡亰-前端 > 【葡京网投哪个正规】浅谈Hybrid技术的设计与实现第三弹,前端进阶篇之如何编写可维护可升级的代码

原标题:【葡京网投哪个正规】浅谈Hybrid技术的设计与实现第三弹,前端进阶篇之如何编写可维护可升级的代码

浏览次数:90 时间:2019-11-30

浅谈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个以上的数据请求,其中最过复杂的当属机票与酒店的订单填写业务代码

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

葡京正网网投 1

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

葡京正网网投 2

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

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

葡京正网网投 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的业务进度:

葡京正网网投 4葡京正网网投 5葡京正网网投 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二维码:

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

葡京正网网投 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:

葡京正网网投 9

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

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

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

账号体系

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

① 统一的鉴权认证

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

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

④ 第三方接入方案

⑤ 接入文档输出

⑥ ……

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

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

多容器与前端

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

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

葡京正网网投 11

页面基本构成

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

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

葡京正网网投 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调用服务器端提供的接口是及其相似的:

葡京正网网投 13

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

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

读取接口

接收消息

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

写入接口

发送消息

向用户回复私信消息接口

生成带参数的二维码

生成带参数的二维码接口

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

JavaScript

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

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

葡京正网网投 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没有出发-到达相关参数信息时,默认弹出出发城市到达城市选择框

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

葡京正网网投 15

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

葡京正网网投 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页面,并且新增一个按钮,点击按钮弹出一个提示:

葡京正网网投 17

葡京正网网投 18

葡京正网网投 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的弹出提示的话,代码便有所不同了:

我们使用的应该是:

葡京正网网投 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文件)中做更改:

葡京正网网投 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删除即可:

葡京正网网投 22

葡京正网网投 23

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

准备工作

常用交互API

良好的交互设计是成功的第一步,在真实业务开发中有一些API一定会用到。

公共业务的设计-体系化

在Hybrid架构中(其实就算在传统的业务中也是),会存在很多公共业务,这部分公共业务很多是H5做的(比如注册、地址维护、反馈等,登录是native化了的公共业务),我们一个Hybrid架构要真正的效率高,就得把各种公共业务做好了,不然单是H5做业务,效率未必会真的比Native高多少。

底层框架完善并且统一后,便可以以规范的力量限制各业务开发,在统一的框架下开发出来的公共业务会大大的提升整体工作效率,这里以注册为例,一个公共页面一般来说得设计成这个样子:

公共业务代码,应该可以让人在URL参数上对页面进行一定定制化,这里URL参数一般要独特一些,一面被覆盖,这个设计适用于native页面

1
公共业务代码,应该可以让人在URL参数上对页面进行一定定制化,这里URL参数一般要独特一些,一面被覆盖,这个设计适用于native页面

葡京正网网投 24

URL中会包含以下参数:

① _hashead 是否有head,默认true

② _hasback 是否包含回退按钮,默认true

③ _backtxt 回退按钮的文案,默认没有,这个时候显示为回退图标

④ _title 标题

⑤ _btntxt 按钮的文案

⑥ _backurl 回退按钮点击时候的跳转,默认为空则执行history.back

⑦ _successurl 点击按钮回调成功时候的跳转,必须

只要公共页面设计为这个样子,就能满足多数业务了,在底层做一些适配,可以很轻易的一套代码同时用于native与H5,这里再举个例子:

如果我们要点击成功后去到一个native页面,如果按照我们之前的设计,我们每个Native页面皆已经URL化了的话,我们完全可以以这种方向跳转:

JavaScript

requestHybrid({ tagname: 'forward', param: { topage: 'nativeUrl', type: 'native' } });

1
2
3
4
5
6
7
requestHybrid({
     tagname: 'forward',
     param: {
         topage: 'nativeUrl',
         type: 'native'
    }
});

这个命令会生成一个这样的url的链接:

_successurl == hybrid://forward?param=%7B%22topage%22%3A%22nativeUrl%22%2C%22type%22%3A%22native%22%7D

完了,在点击回调时要执行一个H5的URL跳转:

JavaScript

window.location = _successurl

1
window.location = _successurl

而根据我们之前的hybrid规范约定,这种请求会被native拦截,于是就跳到了我们想要的native页面,整个这一套东西就是我们所谓的体系化:

葡京正网网投 25

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的实现:

葡京正网网投 26

我这里工作做的多一些,考虑了微信时候的场景,但是这里业务代码暂时只考虑糯米,对应糯米的文档:

葡京正网网投 27 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:注意,这里的适配只是简单实现,考虑多场景的话不能这样写代码!!!

于是我们在糯米中便能很好的运行了

葡京正网网投 28

总体架构

这次的代码依赖于blade骨架,包括:

① MVC模块,完成通过url获取正确的page控制器,从而通过view.js完成渲染页面的功能

② 数据请求模块,完成接口请求

全站依赖于javascript的继承功能,详情见:【一次面试】再谈javascript中的继承,如果不太了解面向对象编程,文中代码可能会有点吃力,也请各位多多了解。

总体业务架构如图:

葡京正网网投 29

框架架构图:

葡京正网网投 30.

下面分别介绍下各个模块,帮助各位在下文中能更好的了解代码,首先是基本MVC的介绍,这里请参考我这篇文章:简单的MVC介绍

跳转

跳转是Hybrid必用API之一,对前端来说有以下跳转:

① 页面内跳转,与Hybrid无关

② H5跳转Native界面

③ H5新开Webview跳转H5页面,一般为做页面动画切换

如果要使用动画,按业务来说有向前与向后两种,forward&back,所以约定如下,首先是H5跳Native某一个页面

JavaScript

//H5跳Native页面 //=>baidubus://forward?t=1446297487682¶m=%7B%22topage%22%3A%22home%22%2C%22type%22%3A%22h2n%22%2C%22data2%22%3A2%7D requestHybrid({ tagname: 'forward', param: { //要去到的页面 topage: 'home', //跳转方式,H5跳Native type: 'native', //其它参数 data2: 2 } });

1
2
3
4
5
6
7
8
9
10
11
12
13
//H5跳Native页面
//=>baidubus://forward?t=1446297487682&param=%7B%22topage%22%3A%22home%22%2C%22type%22%3A%22h2n%22%2C%22data2%22%3A2%7D
requestHybrid({
    tagname: 'forward',
    param: {
        //要去到的页面
        topage: 'home',
        //跳转方式,H5跳Native
        type: 'native',
        //其它参数
        data2: 2
    }
});

比如携程H5页面要去到酒店Native某一个页面可以这样:

JavaScript

//=>schema://forward?t=1446297653344¶m=%7B%22topage%22%3A%22hotel%2Fdetail%20%20%22%2C%22type%22%3A%22h2n%22%2C%22id%22%3A20151031%7D requestHybrid({ tagname: 'forward', param: { //要去到的页面 topage: 'hotel/detail', //跳转方式,H5跳Native type: 'native', //其它参数 id: 20151031 } });

1
2
3
4
5
6
7
8
9
10
11
12
//=>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
requestHybrid({
    tagname: 'forward',
    param: {
        //要去到的页面
        topage: 'hotel/detail',
        //跳转方式,H5跳Native
        type: 'native',
        //其它参数
        id: 20151031
    }
});

比如H5新开Webview的方式跳转H5页面便可以这样:

JavaScript

requestHybrid({ tagname: 'forward', param: { //要去到的页面,首先找到hotel频道,然后定位到detail模块 topage: 'hotel/detail ', //跳转方式,H5新开Webview跳转,最后装载H5页面 type: 'webview', //其它参数 id: 20151031 } });

1
2
3
4
5
6
7
8
9
10
11
requestHybrid({
    tagname: 'forward',
    param: {
        //要去到的页面,首先找到hotel频道,然后定位到detail模块
        topage: 'hotel/detail  ',
        //跳转方式,H5新开Webview跳转,最后装载H5页面
        type: 'webview',
        //其它参数
        id: 20151031
    }
});

back与forward一致,我们甚至会有animattype参数决定切换页面时的动画效果,真实使用时可能会封装全局方法略去tagname的细节,这时就和糯米对外释放的接口差不多了。

离线更新

根据之前的约定,Native中如果存在静态资源,也是按频道划分的:

JavaScript

webapp //根目录 ├─flight ├─hotel //酒店频道 │ │ index.html //业务入口html资源,如果不是单页应用会有多个入口 │ │ main.js //业务所有js资源打包 │ │ │ └─static //静态样式资源 │ ├─css │ ├─hybrid //存储业务定制化类Native Header图标 │ └─images ├─libs │ libs.js //框架所有js资源打包 │ └─static //框架静态资源样式文件 ├─css └─images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
webapp //根目录
├─flight
├─hotel //酒店频道
│  │  index.html //业务入口html资源,如果不是单页应用会有多个入口
│  │  main.js //业务所有js资源打包
│  │
│  └─static //静态样式资源
│      ├─css
│      ├─hybrid //存储业务定制化类Native Header图标
│      └─images
├─libs
│      libs.js //框架所有js资源打包
└─static //框架静态资源样式文件
    ├─css
    └─images

我们这里制定一个规则,native会过滤某一个规则的请求,检查本地是否有该文件,如果本地有那么就直接读取本地,比如说,我们会将这个类型的请求映射到本地:

JavaScript

//===>> file ===> flight/static/hybrid/icon-search.png

1
2
3
http://domain.com/webapp/flight/static/hybrid/icon-search.png
//===>>
file ===> flight/static/hybrid/icon-search.png

这样在浏览器中便继续读取线上文件,在native中,如果有本地资源,便读取本地资源:

葡京正网网投 31

但是我们在真实使用场景中却遇到了一些麻烦。

结语

全局控制器

其实控制器可谓是变化万千的一个对象,对于服务器端来说,控制器完成的功能是将本次请求分发到具体的代码模块,由代码模块处理后返回字符串给前端;

对于请求已经来到浏览器的前端来说,根据这次请求URL(或者其它判断条件),判断该次请求应该由哪个前端js控制器执行,这是前端控制器干的事情;

当真的这次处理逻辑进入一个具体的page后,这个page事实上也可以作为一个控制器存在……

我们这里的控制器,主要完成根据当前请求实例化View的功能,并且会提供一些view级别希望单例使用的接口:

JavaScript

define([ 'UIHeader', 'UIToast', 'UILoading', 'UIPageView', 'UIAlert' ], function (UIHeader, UIToast, UILoading, UIPageView, UIAlert) { return _.inherit({ propertys: function () { //view搜索目录 this.viewRootPath = 'views/'; //默认view this.defaultView = 'index'; //当前视图路径 this.viewId; this.viewUrl; //视图集 this.views = {}; //是否开启单页应用 // this.isOpenWebapp = _.getHybridInfo().platform == 'baidubox' ? true : false; this.isOpenWebapp = false; this.viewMapping = {}; //UIHeader需要释放出来 this.UIHeader = UIHeader; this.interface = [ 'forward', 'back', 'jump', 'showPageView', 'hidePageView', 'showLoading', 'hideLoading', 'showToast', 'hideToast', 'showMessage', 'hideMessage', 'showConfirm', 'hideConfirm', 'openWebapp', 'closeWebapp' ]; }, initialize: function (options) { this.propertys(); this.setOption(options); this.initViewPort(); this.initAppMapping(); //开启fastclick $.bindFastClick && $.bindFastClick(); }, setOption: function (options) { _.extend(this, options); }, //创建dom结构 initViewPort: function () { this.d_header = $('#headerview'); this.d_state = $('#js_page_state'); this.d_viewport = $('#main'); //实例化全局使用的header,这里好像有点不对 this.header = new this.UIHeader({ wrapper: this.d_header }); //非共享资源,这里应该引入app概念了 this.pageviews = {}; this.toast = new UIToast(); this.loading = new UILoading(); this.alert = new UIAlert(); this.confirm = new UIAlert(); }, openWebapp: function () { this.isOpenWebapp = true; }, closeWebapp: function () { this.isOpenWebapp = false; }, showPageView: function (name, _viewdata_, id) { var view = null, k, scope = this.curViewIns || this; if (!id) id = name; if (!_.isString(name)) return; // for (k in _viewdata_) { // if (_.isFunction(_viewdata_[k])) _viewdata_[k] = $.proxy(_viewdata_[k], scope); // } view = this.pageviews[id]; var arr = name.split('/'); var getViewPath = window.getViewPath || window.GetViewPath; if (!view) { view = new UIPageView({ // bug fixed by zzx viewId: arr[arr.length - 1] || name, viewPath: getViewPath ? getViewPath(name) : name, _viewdata_: _viewdata_, onHide: function () { scope.initHeader(); } }); this.pageviews[id] = view; } else { view.setViewData(_viewdata_); } view.show(); }, hidePageView: function (name) { if (name) { if (this.pageviews[name]) this.pageviews[name].hide(); } else { for (var k in this.pageviews) this.pageviews[k].hide(); } }, showLoading: function () { this.loading.show(); }, hideLoading: function () { this.loading.hide(); }, showToast: function (msg, callback) { this.toast.resetDefaultProperty(); this.toast.content = msg; if (callback) this.toast.hideAction = callback; this.toast.refresh(); this.toast.show(); }, hideToast: function () { this.toast.hide(); }, showMessage: function (param) { if (_.isString(param)) { param = { content: param }; } this.alert.resetDefaultProperty(); this.alert.setOption(param); this.alert.refresh(); this.alert.show(); }, hideMessage: function () { this.alert.hide(); }, showConfirm: function (params) { if (!params) params = {}; if (typeof params == 'string') { params = { content: params }; } this.confirm.resetDefaultProperty(); //与showMessage不一样的地方 this.confirm.btns = [ { name: '取消', className: 'cm-btns-cancel js_cancel' }, { name: '确定', className: 'cm-btns-ok js_ok' } ]; this.confirm.setOption(params); this.confirm.refresh(); this.confirm.show(); }, hideConfirm: function () { this.confirm.hide(); }, //初始化app initApp: function () { //首次加载不需要走路由控制 this.loadViewByUrl(); //后面的加载全部要经过路由处理 if (this.isOpenWebapp === true) $(window).on('popstate.app', $.proxy(this.loadViewByUrl, this)); }, loadViewByUrl: function (e) { this.hidePageView(); var url = decodeURIComponent(location.href).toLowerCase(); var viewId = this.getViewIdRule(url); viewId = viewId || this.defaultView; this.viewId = viewId; this.viewUrl = url; this.switchView(this.viewId); }, //@override getViewIdRule: function (url) { var viewId = '', hash = ''; var reg = /webapp/.+/(.+).html/; var match = url.match(reg); if (match && match[1]) viewId = match[1]; return viewId; }, //@override setUrlRule: function (viewId, param, replace, project) { var reg = /(webapp/.+/)(.+).html/; var url = window.location.href; var match = url.match(reg); var proj = project ? 'webapp/' + project : match[1]; var preUrl = '', str = '', i = 0, _k, _v; //这里这样做有点过于业务了 *bug* var keepParam = [ 'us' ], p; if (!viewId) return; if (!match || !match[1]) { preUrl = url + '/webapp/bus/' + viewId + '.html'; } else { preUrl = url.substr(0, url.indexOf(match[1])) + proj + viewId + '.html'; ; } //特定的参数将会一直带上去,渠道、来源等标志 for (i = 0; i < keepParam.length; i++) { p = keepParam[i]; if (_.getUrlParam()[p]) { if (!param) param = {}; param[p] = _.getUrlParam()[p]; } } i = 0; for (k in param) { _k = encodeURIComponent(_.removeAllSpace(k)); _v = encodeURIComponent(_.removeAllSpace(param[k])); if (i === 0) { str += '?' + _k + '=' + _v; i++; } else { str += '&' + _k + '=' + _v; } } url = preUrl + str; if (this.isOpenWebapp === false) { window.location = url; return; } if (replace) { history.replaceState('', {}, url); } else { history.pushState('', {}, url); } }, switchView: function (id) { var curView = this.views[id]; //切换前的当前view,马上会隐藏 var tmpView = this.curView; if (tmpView && tmpView != curView) { this.lastView = tmpView; } //加载view样式,权宜之计 // this.loadViewStyle(id); //如果当前view存在,则执行请onload事件 if (curView) { //如果当前要跳转的view就是当前view的话便不予处理 //这里具体处理逻辑要改************************************* if (curView == this.curView) { return; } this.curView = curView; this.curView.show(); this.lastView && this.lastView.hide(); } else { // this.showLoading(); this.loadView(id, function (View) { //每次加载结束将状态栏隐藏,这个代码要改 // this.hideLoading(); this.curView = new View({ viewId: id, refer: this.lastView ? this.lastView.viewId : null, APP: this, wrapper: this.d_viewport }); //设置网页上的view标志 this.curView.$el.attr('page-url', id); //保存至队列 this.views[id] = this.curView; this.curView.show(); this.lastView && this.lastView.hide(); }); } }, //加载view loadView: function (path, callback) { var self = this; requirejs([this.buildUrl(path)], function (View) { callback && callback.call(self, View); }); }, //override //配置可能会有的路径扩展,为Hybrid与各个渠道做适配 initAppMapping: function () { // console.log('该方法必须被重写'); }, //@override buildUrl: function (path) { var mappingPath = this.viewMapping[path]; return mappingPath ? mappingPath : this.viewRootPath + '/' + path + '/'

  • path; }, //此处需要一个更新逻辑,比如在index view再点击到index view不会有反应,下次改************************** forward: function (viewId, param, replace) { if (!viewId) return; viewId = viewId.toLowerCase(); this.setUrlRule(viewId, param, replace); this.loadViewByUrl(); }, jump: function (path, param, replace) { var viewId; var project; if (!path) { return; } path = path.toLowerCase().split('/'); if (path.length <= 0) { return; } viewId = path.pop(); project = path.length === 1 ? path.join('') + '/' : path.join(''); this.setUrlRule(viewId, param, replace, project); this.loadViewByUrl(); }, back: function (viewId, param, replace) { if (viewId) { this.forward(viewId, param, replace) } else { if (window.history.length == 1) { this.forward(this.defaultView, param, replace) } else { history.back(); } } } }); }); abstract.app
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
define([
  'UIHeader',
  'UIToast',
  'UILoading',
  'UIPageView',
  'UIAlert'
], function (UIHeader, UIToast, UILoading, UIPageView, UIAlert) {
 
    return _.inherit({
        propertys: function () {
            //view搜索目录
            this.viewRootPath = 'views/';
 
            //默认view
            this.defaultView = 'index';
 
            //当前视图路径
            this.viewId;
            this.viewUrl;
 
            //视图集
            this.views = {};
 
            //是否开启单页应用
            //      this.isOpenWebapp = _.getHybridInfo().platform == 'baidubox' ? true : false;
            this.isOpenWebapp = false;
 
            this.viewMapping = {};
 
            //UIHeader需要释放出来
            this.UIHeader = UIHeader;
 
            this.interface = [
                'forward',
                'back',
                'jump',
                'showPageView',
                'hidePageView',
                'showLoading',
                'hideLoading',
                'showToast',
                'hideToast',
                'showMessage',
                'hideMessage',
                'showConfirm',
                'hideConfirm',
                'openWebapp',
                'closeWebapp'
            ];
 
        },
 
        initialize: function (options) {
            this.propertys();
            this.setOption(options);
            this.initViewPort();
            this.initAppMapping();
 
            //开启fastclick
            $.bindFastClick && $.bindFastClick();
 
        },
 
        setOption: function (options) {
            _.extend(this, options);
        },
 
        //创建dom结构
        initViewPort: function () {
 
            this.d_header = $('#headerview');
            this.d_state = $('#js_page_state');
            this.d_viewport = $('#main');
 
            //实例化全局使用的header,这里好像有点不对
            this.header = new this.UIHeader({
                wrapper: this.d_header
            });
 
            //非共享资源,这里应该引入app概念了
            this.pageviews = {};
            this.toast = new UIToast();
            this.loading = new UILoading();
            this.alert = new UIAlert();
            this.confirm = new UIAlert();
        },
 
        openWebapp: function () {
            this.isOpenWebapp = true;
        },
 
        closeWebapp: function () {
            this.isOpenWebapp = false;
        },
 
        showPageView: function (name, _viewdata_, id) {
            var view = null, k, scope = this.curViewIns || this;
            if (!id) id = name;
            if (!_.isString(name)) return;
            //    for (k in _viewdata_) {
            //      if (_.isFunction(_viewdata_[k])) _viewdata_[k] = $.proxy(_viewdata_[k], scope);
            //    }
            view = this.pageviews[id];
            var arr = name.split('/');
            var getViewPath = window.getViewPath || window.GetViewPath;
            if (!view) {
                view = new UIPageView({
                    // bug fixed by zzx
                    viewId: arr[arr.length - 1] || name,
                    viewPath: getViewPath ? getViewPath(name) : name,
                    _viewdata_: _viewdata_,
                    onHide: function () {
                        scope.initHeader();
                    }
                });
                this.pageviews[id] = view;
            } else {
                view.setViewData(_viewdata_);
            }
            view.show();
 
        },
 
        hidePageView: function (name) {
            if (name) {
                if (this.pageviews[name]) this.pageviews[name].hide();
            } else {
                for (var k in this.pageviews) this.pageviews[k].hide();
            }
        },
 
        showLoading: function () {
            this.loading.show();
        },
 
        hideLoading: function () {
            this.loading.hide();
        },
 
        showToast: function (msg, callback) {
            this.toast.resetDefaultProperty();
            this.toast.content = msg;
            if (callback) this.toast.hideAction = callback;
            this.toast.refresh();
            this.toast.show();
        },
 
        hideToast: function () {
            this.toast.hide();
        },
 
        showMessage: function (param) {
            if (_.isString(param)) {
                param = { content: param };
            }
 
            this.alert.resetDefaultProperty();
            this.alert.setOption(param);
            this.alert.refresh();
            this.alert.show();
        },
 
        hideMessage: function () {
            this.alert.hide();
        },
 
        showConfirm: function (params) {
            if (!params) params = {};
            if (typeof params == 'string') {
                params = {
                    content: params
                };
            }
 
            this.confirm.resetDefaultProperty();
 
            //与showMessage不一样的地方
            this.confirm.btns = [
              { name: '取消', className: 'cm-btns-cancel js_cancel' },
              { name: '确定', className: 'cm-btns-ok js_ok' }
            ];
            this.confirm.setOption(params);
            this.confirm.refresh();
            this.confirm.show();
        },
 
        hideConfirm: function () {
            this.confirm.hide();
        },
 
        //初始化app
        initApp: function () {
 
            //首次加载不需要走路由控制
            this.loadViewByUrl();
 
            //后面的加载全部要经过路由处理
            if (this.isOpenWebapp === true)
                $(window).on('popstate.app', $.proxy(this.loadViewByUrl, this));
 
        },
 
        loadViewByUrl: function (e) {
            this.hidePageView();
 
            var url = decodeURIComponent(location.href).toLowerCase();
            var viewId = this.getViewIdRule(url);
 
            viewId = viewId || this.defaultView;
            this.viewId = viewId;
            this.viewUrl = url;
            this.switchView(this.viewId);
 
        },
 
        //@override
        getViewIdRule: function (url) {
            var viewId = '', hash = '';
            var reg = /webapp/.+/(.+).html/;
 
            var match = url.match(reg);
            if (match && match[1]) viewId = match[1];
 
            return viewId;
        },
 
        //@override
        setUrlRule: function (viewId, param, replace, project) {
            var reg = /(webapp/.+/)(.+).html/;
            var url = window.location.href;
            var match = url.match(reg);
            var proj = project ? 'webapp/' + project : match[1];
            var preUrl = '', str = '', i = 0, _k, _v;
            //这里这样做有点过于业务了 *bug*
            var keepParam = [
              'us'
            ], p;
            if (!viewId) return;
            if (!match || !match[1]) {
                preUrl = url + '/webapp/bus/' + viewId + '.html';
            } else {
                preUrl = url.substr(0, url.indexOf(match[1])) + proj + viewId + '.html'; ;
            }
 
            //特定的参数将会一直带上去,渠道、来源等标志
            for (i = 0; i < keepParam.length; i++) {
                p = keepParam[i];
                if (_.getUrlParam()[p]) {
                    if (!param) param = {};
                    param[p] = _.getUrlParam()[p];
                }
            }
 
            i = 0;
 
            for (k in param) {
                _k = encodeURIComponent(_.removeAllSpace(k));
                _v = encodeURIComponent(_.removeAllSpace(param[k]));
                if (i === 0) {
                    str += '?' + _k + '=' + _v;
                    i++;
                } else {
                    str += '&' + _k + '=' + _v;
                }
            }
 
            url = preUrl + str;
 
            if (this.isOpenWebapp === false) {
                window.location = url;
                return;
            }
 
            if (replace) {
                history.replaceState('', {}, url);
            } else {
                history.pushState('', {}, url);
            }
 
        },
 
        switchView: function (id) {
 
            var curView = this.views[id];
 
            //切换前的当前view,马上会隐藏
            var tmpView = this.curView;
 
            if (tmpView && tmpView != curView) {
                this.lastView = tmpView;
            }
 
            //加载view样式,权宜之计
            //      this.loadViewStyle(id);
 
            //如果当前view存在,则执行请onload事件
            if (curView) {
 
                //如果当前要跳转的view就是当前view的话便不予处理
                //这里具体处理逻辑要改*************************************
                if (curView == this.curView) {
                    return;
                }
 
                this.curView = curView;
                this.curView.show();
                this.lastView && this.lastView.hide();
            } else {
 
                //        this.showLoading();
                this.loadView(id, function (View) {
                    //每次加载结束将状态栏隐藏,这个代码要改
                    //          this.hideLoading();
 
                    this.curView = new View({
                        viewId: id,
                        refer: this.lastView ? this.lastView.viewId : null,
                        APP: this,
                        wrapper: this.d_viewport
                    });
 
                    //设置网页上的view标志
                    this.curView.$el.attr('page-url', id);
 
                    //保存至队列
                    this.views[id] = this.curView;
 
                    this.curView.show();
                    this.lastView && this.lastView.hide();
 
                });
            }
        },
 
        //加载view
        loadView: function (path, callback) {
            var self = this;
            requirejs([this.buildUrl(path)], function (View) {
                callback && callback.call(self, View);
            });
        },
 
        //override
        //配置可能会有的路径扩展,为Hybrid与各个渠道做适配
        initAppMapping: function () {
            //            console.log('该方法必须被重写');
        },
 
        //@override
        buildUrl: function (path) {
            var mappingPath = this.viewMapping[path];
            return mappingPath ? mappingPath : this.viewRootPath + '/' + path + '/' + path;
        },
 
        //此处需要一个更新逻辑,比如在index view再点击到index view不会有反应,下次改**************************
        forward: function (viewId, param, replace) {
            if (!viewId) return;
            viewId = viewId.toLowerCase();
 
            this.setUrlRule(viewId, param, replace);
            this.loadViewByUrl();
        },
        jump: function (path, param, replace) {
            var viewId;
            var project;
            if (!path) {
                return;
            }
            path = path.toLowerCase().split('/');
            if (path.length <= 0) {
                return;
            }
            viewId = path.pop();
            project = path.length === 1 ? path.join('') + '/' : path.join('');
            this.setUrlRule(viewId, param, replace, project);
            this.loadViewByUrl();
        },
        back: function (viewId, param, replace) {
            if (viewId) {
                this.forward(viewId, param, replace)
            } else {
                if (window.history.length == 1) {
                    this.forward(this.defaultView, param, replace)
                } else {
                    history.back();
                }
            }
        }
 
    });
 
});
 
abstract.app

这里属于框架控制器层面的代码,与今天的主题不是非常相关,有兴趣的朋友可以详细读读。

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是不允许重复的):

JavaScript

//Native以及前端框架会对特殊tagname的标识做默认回调,如果未注册callback,或者点击回调callback无返回则执行默认方法 // back前端默认执行History.back,如果不可后退则回到指定URL,Native如果检测到不可后退则返回Naive大首页 // home前端默认返回指定URL,Native默认返回大首页 this.header.set({ left: [ { //如果出现value字段,则默认不使用icon tagname: 'back', value: '回退', //如果设置了lefticon或者righticon,则显示icon //native会提供常用图标icon映射,如果找不到,便会去当前业务频道专用目录获取图标 lefticon: 'back', callback: function () { } } ], right: [ { //默认icon为tagname,这里为icon tagname: 'search', callback: function () { } }, //自定义图标 { tagname: 'me', //会去hotel频道存储静态header图标资源目录搜寻该图标,没有便使用默认图标 icon: 'hotel/me.png', callback: function () { } } ], title: 'title', //显示主标题,子标题的场景 title: ['title', 'subtitle'], //定制化title title: { value: 'title', //标题右边图标 righticon: 'down', //也可以设置lefticon //标题类型,默认为空,设置的话需要特殊处理 //type: 'tabs', //点击标题时的回调,默认为空 callback: function () { } } });

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
//Native以及前端框架会对特殊tagname的标识做默认回调,如果未注册callback,或者点击回调callback无返回则执行默认方法
// back前端默认执行History.back,如果不可后退则回到指定URL,Native如果检测到不可后退则返回Naive大首页
// home前端默认返回指定URL,Native默认返回大首页
this.header.set({
    left: [
        {
            //如果出现value字段,则默认不使用icon
            tagname: 'back',
            value: '回退',
            //如果设置了lefticon或者righticon,则显示icon
            //native会提供常用图标icon映射,如果找不到,便会去当前业务频道专用目录获取图标
            lefticon: 'back',
            callback: function () { }
        }
    ],
    right: [
        {
            //默认icon为tagname,这里为icon
            tagname: 'search',
            callback: function () { }
        },
    //自定义图标
        {
        tagname: 'me',
        //会去hotel频道存储静态header图标资源目录搜寻该图标,没有便使用默认图标
        icon: 'hotel/me.png',
        callback: function () { }
    }
    ],
    title: 'title',
    //显示主标题,子标题的场景
    title: ['title', 'subtitle'],
 
    //定制化title
    title: {
        value: 'title',
        //标题右边图标
        righticon: 'down', //也可以设置lefticon
        //标题类型,默认为空,设置的话需要特殊处理
        //type: 'tabs',
        //点击标题时的回调,默认为空
        callback: function () { }
    }
});

因为Header左边一般来说只有一个按钮,所以其对象可以使用这种形式:

JavaScript

this.header.set({ back: function () { }, title: '' }); //语法糖=> this.header.set({ left: [{ tagname: 'back', callback: function(){} }], title: '', });

1
2
3
4
5
6
7
8
9
10
11
12
this.header.set({
    back: function () { },
    title: ''
});
//语法糖=>
this.header.set({
    left: [{
        tagname: 'back',
        callback: function(){}
    }],
    title: '',
});

为完成Native端的实现,这里会新增两个接口,向Native注册事件,以及注销事件:

JavaScript

var registerHybridCallback = function (ns, name, callback) { if(!window.Hybrid[ns]) window.Hybrid[ns] = {}; window.Hybrid[ns][name] = callback; }; var unRegisterHybridCallback = function (ns) { if(!window.Hybrid[ns]) return; delete window.Hybrid[ns]; };

1
2
3
4
5
6
7
8
9
var registerHybridCallback = function (ns, name, callback) {
  if(!window.Hybrid[ns]) window.Hybrid[ns] = {};
  window.Hybrid[ns][name] = callback;
};
 
var unRegisterHybridCallback = function (ns) {
  if(!window.Hybrid[ns]) return;
  delete window.Hybrid[ns];
};

Native Header组件的实现:

JavaScript

define([], function () { 'use strict'; return _.inherit({ propertys: function () { this.left = []; this.right = []; this.title = {}; this.view = null; this.hybridEventFlag = 'Header_Event'; }, //全部更新 set: function (opts) { if (!opts) return; var left = []; var right = []; var title = {}; var tmp = {}; //语法糖适配 if (opts.back) { tmp = { tagname: 'back' }; if (typeof opts.back == 'string') tmp.value = opts.back; else if (typeof opts.back == 'function') tmp.callback = opts.back; else if (typeof opts.back == 'object') _.extend(tmp, opts.back); left.push(tmp); } else { if (opts.left) left = opts.left; } //右边按钮必须保持数据一致性 if (typeof opts.right == 'object' && opts.right.length) right = opts.right if (typeof opts.title == 'string') { title.title = opts.title; } else if (_.isArray(opts.title) && opts.title.length > 1) { title.title = opts.title[0]; title.subtitle = opts.title[1]; } else if (typeof opts.title == 'object') { _.extend(title, opts.title); } this.left = left; this.right = right; this.title = title; this.view = opts.view; this.registerEvents(); _.requestHybrid({ tagname: 'updateheader', param: { left: this.left, right: this.right, title: this.title } }); }, //注册事件,将事件存于本地 registerEvents: function () { _.unRegisterHybridCallback(this.hybridEventFlag); this._addEvent(this.left); this._addEvent(this.right); this._addEvent(this.title); }, _addEvent: function (data) { if (!_.isArray(data)) data = [data]; var i, len, tmp, fn, tagname; var t = 'header_' + (new Date().getTime()); for (i = 0, len = data.length; i < len; i++) { tmp = data[i]; tagname = tmp.tagname || ''; if (tmp.callback) { fn = $.proxy(tmp.callback, this.view); tmp.callback = t; _.registerHeaderCallback(this.hybridEventFlag, t + '_' + tagname, fn); } } }, //显示header show: function () { _.requestHybrid({ tagname: 'showheader' }); }, //隐藏header hide: function () { _.requestHybrid({ tagname: 'hideheader', param: { animate: true } }); }, //只更新title,不重置事件,不对header其它地方造成变化,仅仅最简单的header能如此操作 update: function (title) { _.requestHybrid({ tagname: 'updateheadertitle', param: { title: 'aaaaa' } }); }, initialize: function () { this.propertys(); } }); }); Native Header组件的封装

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
define([], function () {
    'use strict';
 
    return _.inherit({
 
        propertys: function () {
 
            this.left = [];
            this.right = [];
            this.title = {};
            this.view = null;
 
            this.hybridEventFlag = 'Header_Event';
 
        },
 
        //全部更新
        set: function (opts) {
            if (!opts) return;
 
            var left = [];
            var right = [];
            var title = {};
            var tmp = {};
 
            //语法糖适配
            if (opts.back) {
                tmp = { tagname: 'back' };
                if (typeof opts.back == 'string') tmp.value = opts.back;
                else if (typeof opts.back == 'function') tmp.callback = opts.back;
                else if (typeof opts.back == 'object') _.extend(tmp, opts.back);
                left.push(tmp);
            } else {
                if (opts.left) left = opts.left;
            }
 
            //右边按钮必须保持数据一致性
            if (typeof opts.right == 'object' && opts.right.length) right = opts.right
 
            if (typeof opts.title == 'string') {
                title.title = opts.title;
            } else if (_.isArray(opts.title) && opts.title.length > 1) {
                title.title = opts.title[0];
                title.subtitle = opts.title[1];
            } else if (typeof opts.title == 'object') {
                _.extend(title, opts.title);
            }
 
            this.left = left;
            this.right = right;
            this.title = title;
            this.view = opts.view;
 
            this.registerEvents();
 
            _.requestHybrid({
                tagname: 'updateheader',
                param: {
                    left: this.left,
                    right: this.right,
                    title: this.title
                }
            });
 
        },
 
        //注册事件,将事件存于本地
        registerEvents: function () {
            _.unRegisterHybridCallback(this.hybridEventFlag);
            this._addEvent(this.left);
            this._addEvent(this.right);
            this._addEvent(this.title);
        },
 
        _addEvent: function (data) {
            if (!_.isArray(data)) data = [data];
            var i, len, tmp, fn, tagname;
            var t = 'header_' + (new Date().getTime());
 
            for (i = 0, len = data.length; i < len; i++) {
                tmp = data[i];
                tagname = tmp.tagname || '';
                if (tmp.callback) {
                    fn = $.proxy(tmp.callback, this.view);
                    tmp.callback = t;
                    _.registerHeaderCallback(this.hybridEventFlag, t + '_' + tagname, fn);
                }
            }
        },
 
        //显示header
        show: function () {
            _.requestHybrid({
                tagname: 'showheader'
            });
        },
 
        //隐藏header
        hide: function () {
            _.requestHybrid({
                tagname: 'hideheader',
                param: {
                    animate: true
                }
            });
        },
 
        //只更新title,不重置事件,不对header其它地方造成变化,仅仅最简单的header能如此操作
        update: function (title) {
            _.requestHybrid({
                tagname: 'updateheadertitle',
                param: {
                    title: 'aaaaa'
                }
            });
        },
 
        initialize: function () {
            this.propertys();
        }
    });
 
});
 
Native Header组件的封装

增量的粒度

其实,我们最开始做增量设计的时候就考虑了很多问题,但是真实业务的时候往往因为时间的压迫,做出来的东西就会很简陋,这个只能慢慢迭代,而我们所有的缓存都会考虑两个问题:

① 如何存储&读取缓存

② 如何更新缓存

浏览器的缓存读取更新是比较单纯的:

浏览器只需要自己能读到最新的缓存即可

1
浏览器只需要自己能读到最新的缓存即可

而APP的话,会存在最新发布的APP希望读到离线包,而老APP不希望读到增量包的情况(老的APP下载下来增量包压根不支持),更加复杂的情况是想对某个版本做定向修复,那么就需要定向发增量包了,这让情况变得复杂,而复杂即错误,我们往往可以以简单的约定,解决复杂的场景。

思考以下场景:

我们的APP要发一个新的版本了,我们把最初一版的静态资源给打了进去,完了审核中的时候,我们老版本APP突然有一个临时需求要上线,我知道这听起来很有一些扯淡,但这种扯淡的事情却真实的发生了,这个时候我们如果打了增量包的话,那么最新的APP在审核期间也会拉到这次代码,但也许这不是我们所期望的,于是有了以下与native的约定:

Native请求增量更新的时候带上版本号,并且强迫约定iOS与Android的大版本号一致,比如iOS为2.1.0Android这个版本修复BUG可以是2.1.1但不能是2.2.0

1
Native请求增量更新的时候带上版本号,并且强迫约定iOS与Android的大版本号一致,比如iOS为2.1.0Android这个版本修复BUG可以是2.1.1但不能是2.2.0

然后在服务器端配置一个较为复杂的版本映射表:

JavaScript

## 附录一 // 每个app所需的项目配置 const APP_CONFIG = [ 'surgery' => [ // 包名 'channel' => 'd2d', // 主项目频道名 'dependencies' => ['blade', 'static', 'user'], // 依赖的频道 'version' => [ // 各个版本对应的增量包范围,取范围内版本号最大的增量包 '2.0.x' => ['gte' => '1.0.0', 'lt' => '1.1.0'], '2.2.x' => ['gte' => '1.1.0', 'lt' => '1.2.0'] ], 'version_i' => [ // ios需特殊配置的某版本 ], 'version_a' => [ // Android需特殊配置的某版本 ] ] ];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 附录一  
// 每个app所需的项目配置
const APP_CONFIG = [
   'surgery' => [        // 包名
        'channel' => 'd2d',      // 主项目频道名
        'dependencies' => ['blade', 'static', 'user'],    // 依赖的频道
        'version' => [   // 各个版本对应的增量包范围,取范围内版本号最大的增量包
            '2.0.x' => ['gte' => '1.0.0', 'lt' => '1.1.0'],    
            '2.2.x' => ['gte' => '1.1.0', 'lt' => '1.2.0']
        ],
        'version_i' => [    // ios需特殊配置的某版本
 
        ],
        'version_a' => [    // Android需特殊配置的某版本
 
        ]
    ]
];

这里解决了APP版本的读取限制,完了我们便需要关心增量的到达率与更新率,我们也会担心我们的APP读到错误的文件。

代码地址

页面基类

这里的核心是页面级别的处理,这里会做比较多的介绍,首先我们为所有的业务级View提供了一个继承的View:

JavaScript

define([], function () { 'use strict'; return _.inherit({ _propertys: function () { this.APP = this.APP || window.APP; var i = 0, len = 0, k; if (this.APP && this.APP.interface) { for (i = 0, len = this.APP.interface.length; i < len; i++) { k = this.APP.interface[i]; if (k == 'showPageView') continue; if (_.isFunction(this.APP[k])) { this[k] = $.proxy(this.APP[k], this.APP); } else this[k] = this.APP[k]; } } this.header = this.APP.header; }, showPageView: function (name, _viewdata, id) { this.APP.curViewIns = this; this.APP.showPageView(name, _viewdata, id) }, propertys: function () { //这里设置UI的根节点所处包裹层 this.wrapper = $('#main'); this.id = _.uniqueId('page-view-'); this.classname = ''; this.viewId = null; this.refer = null; //模板字符串,各个组件不同,现在加入预编译机制 this.template = ''; //事件机制 this.events = {}; //自定义事件 //此处需要注意mask 绑定事件前后问题,考虑scroll.radio插件类型的mask应用,考虑组件通信 this.eventArr = {}; //初始状态为实例化 this.status = 'init'; this._propertys(); }, getViewModel: function () { //假如有datamodel的话,便直接返回,不然便重写,这里基本为了兼容 if (_.isObject(this.datamodel)) return this.datamodel; return {}; }, //子类事件绑定若想保留父级的,应该使用该方法 addEvents: function (events) { if (_.isObject(events)) _.extend(this.events, events); }, on: function (type, fn, insert) { if (!this.eventArr[type]) this.eventArr[type] = []; //头部插入 if (insert) { this.eventArr[type].splice(0, 0, fn); } else { this.eventArr[type].push(fn); } }, off: function (type, fn) { if (!this.eventArr[type]) return; if (fn) { this.eventArr[type] = _.without(this.eventArr[type], fn); } else { this.eventArr[type] = []; } }, trigger: function (type) { var _slice = Array.prototype.slice; var args = _slice.call(arguments, 1); var events = this.eventArr; var results = [], i, l; if (events[type]) { for (i = 0, l = events[type].length; i < l; i++) { results[results.length] = events[type][i].apply(this, args); } } return results; }, createRoot: function (html) { //如果存在style节点,并且style节点不存在的时候需要处理 if (this.style && !$('#page_' + this.viewId)[0]) { $('head').append($('<style id="page_' + this.viewId + '" class="page-style">' + this.style + '</style>')) } //如果具有fake节点,需要移除 $('#fake-page').remove(); //UI的根节点 this.$el = $('<div class="cm-view page-' + this.viewId + ' ' + this.classname + '" style="display: none; " id="' + this.id + '">' + html + '</div>'); if (this.wrapper.find('.cm-view')[0]) { this.wrapper.append(this.$el); } else { this.wrapper.html('').append(this.$el); } }, _isAddEvent: function (key) { if (key == 'onCreate' || key == 'onPreShow' || key == 'onShow' || key == 'onRefresh' || key == 'onHide') return true; return false; }, setOption: function (options) { //这里可以写成switch,开始没有想到有这么多分支 for (var k in options) { if (k == 'events') { _.extend(this[k], options[k]); continue; } else if (this._isAddEvent(k)) { this.on(k, options[k]) continue; } this[k] = options[k]; } // _.extend(this, options); }, initialize: function (opts) { //这种默认属性 this.propertys(); //根据参数重置属性 this.setOption(opts); //检测不合理属性,修正为正确数据 this.resetPropery(); this.addEvent(); this.create(); this.initElement(); window.sss = this; }, $: function (selector) { return this.$el.find(selector); }, //提供属性重置功能,对属性做检查 resetPropery: function () { }, //各事件注册点,用于被继承override addEvent: function () { }, create: function () { this.trigger('onPreCreate'); //如果没有传入模板,说明html结构已经存在 this.createRoot(this.render()); this.status = 'create'; this.trigger('onCreate'); }, //实例化需要用到到dom元素 initElement: function () { }, render: function (callback) { var data = this.getViewModel() || {}; var html = this.template; if (!this.template) return ''; //引入预编译机制 if (_.isFunction(this.template)) { html = this.template(data); } else { html = _.template(this.template)(data); } typeof callback == 'function' && callback.call(this); return html; }, refresh: function (needRecreate) { this.resetPropery(); if (needRecreate) { this.create(); } else { this.$el.html(this.render()); } this.initElement(); if (this.status != 'hide') this.show(); this.trigger('onRefresh'); }, /** * @description 组件显示方法,首次显示会将ui对象实际由内存插入包裹层 * @method initialize * @param {Object} opts */ show: function () { this.trigger('onPreShow'); // //如果包含就不要乱搞了 // if (!$.contains(this.wrapper[0], this.$el[0])) { // //如果需要清空容器的话便清空 // if (this.needEmptyWrapper) this.wrapper.html(''); // this.wrapper.append(this.$el); // } this.$el.show(); this.status = 'show'; this.bindEvents(); this.initHeader(); this.trigger('onShow'); }, initHeader: function () { }, hide: function () { if (!this.$el || this.status !== 'show') return; this.trigger('onPreHide'); this.$el.hide(); this.status = 'hide'; this.unBindEvents(); this.trigger('onHide'); }, destroy: function () { this.status = 'destroy'; this.unBindEvents(); this.$root.remove(); this.trigger('onDestroy'); delete this; }, bindEvents: function () { var events = this.events; if (!(events || (events = _.result(this, 'events')))) return this; this.unBindEvents(); // 解析event参数的正则 var delegateEventSplitter = /^(S+)s*(.*)$/; var key, method, match, eventName, selector; // 做简单的字符串数据解析 for (key in events) { method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; if (!method) continue; match = key.match(delegateEventSplitter); eventName = match[1], selector = match[2]; method = _.bind(method, this); eventName += '.delegateUIEvents' + this.id; if (selector === '') { this.$el.on(eventName, method); } else { this.$el.on(eventName, selector, method); } } return this; }, unBindEvents: function () { this.$el.off('.delegateUIEvents' + this.id); return this; }, getParam: function (key) { return _.getUrlParam(window.location.href, key) }, renderTpl: function (tpl, data) { if (!_.isFunction(tpl)) tpl = _.template(tpl); return tpl(data); } }); }); abstract.view

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
define([], function () {
    'use strict';
 
    return _.inherit({
 
        _propertys: function () {
            this.APP = this.APP || window.APP;
            var i = 0, len = 0, k;
            if (this.APP && this.APP.interface) {
                for (i = 0, len = this.APP.interface.length; i < len; i++) {
                    k = this.APP.interface[i];
                    if (k == 'showPageView') continue;
 
                    if (_.isFunction(this.APP[k])) {
                        this[k] = $.proxy(this.APP[k], this.APP);
                    }
                    else this[k] = this.APP[k];
                }
            }
 
            this.header = this.APP.header;
        },
 
        showPageView: function (name, _viewdata, id) {
            this.APP.curViewIns = this;
            this.APP.showPageView(name, _viewdata, id)
        },
        propertys: function () {
            //这里设置UI的根节点所处包裹层
            this.wrapper = $('#main');
            this.id = _.uniqueId('page-view-');
            this.classname = '';
 
            this.viewId = null;
            this.refer = null;
 
            //模板字符串,各个组件不同,现在加入预编译机制
            this.template = '';
            //事件机制
            this.events = {};
 
            //自定义事件
            //此处需要注意mask 绑定事件前后问题,考虑scroll.radio插件类型的mask应用,考虑组件通信
            this.eventArr = {};
 
            //初始状态为实例化
            this.status = 'init';
 
            this._propertys();
        },
 
        getViewModel: function () {
            //假如有datamodel的话,便直接返回,不然便重写,这里基本为了兼容
            if (_.isObject(this.datamodel)) return this.datamodel;
            return {};
        },
 
        //子类事件绑定若想保留父级的,应该使用该方法
        addEvents: function (events) {
            if (_.isObject(events)) _.extend(this.events, events);
        },
 
        on: function (type, fn, insert) {
            if (!this.eventArr[type]) this.eventArr[type] = [];
 
            //头部插入
            if (insert) {
                this.eventArr[type].splice(0, 0, fn);
            } else {
                this.eventArr[type].push(fn);
            }
        },
 
        off: function (type, fn) {
            if (!this.eventArr[type]) return;
            if (fn) {
                this.eventArr[type] = _.without(this.eventArr[type], fn);
            } else {
                this.eventArr[type] = [];
            }
        },
 
        trigger: function (type) {
            var _slice = Array.prototype.slice;
            var args = _slice.call(arguments, 1);
            var events = this.eventArr;
            var results = [], i, l;
 
            if (events[type]) {
                for (i = 0, l = events[type].length; i < l; i++) {
                    results[results.length] = events[type][i].apply(this, args);
                }
            }
            return results;
        },
 
        createRoot: function (html) {
 
            //如果存在style节点,并且style节点不存在的时候需要处理
            if (this.style && !$('#page_' + this.viewId)[0]) {
                $('head').append($('<style id="page_' + this.viewId + '" class="page-style">' + this.style + '</style>'))
            }
 
            //如果具有fake节点,需要移除
            $('#fake-page').remove();
 
            //UI的根节点
            this.$el = $('<div class="cm-view page-' + this.viewId + ' ' + this.classname + '" style="display: none; " id="' + this.id + '">' + html + '</div>');
            if (this.wrapper.find('.cm-view')[0]) {
                this.wrapper.append(this.$el);
            } else {
                this.wrapper.html('').append(this.$el);
            }
 
        },
 
        _isAddEvent: function (key) {
            if (key == 'onCreate' || key == 'onPreShow' || key == 'onShow' || key == 'onRefresh' || key == 'onHide')
                return true;
            return false;
        },
 
        setOption: function (options) {
            //这里可以写成switch,开始没有想到有这么多分支
            for (var k in options) {
                if (k == 'events') {
                    _.extend(this[k], options[k]);
                    continue;
                } else if (this._isAddEvent(k)) {
                    this.on(k, options[k])
                    continue;
                }
                this[k] = options[k];
            }
            //      _.extend(this, options);
        },
 
        initialize: function (opts) {
            //这种默认属性
            this.propertys();
            //根据参数重置属性
            this.setOption(opts);
            //检测不合理属性,修正为正确数据
            this.resetPropery();
 
            this.addEvent();
            this.create();
 
            this.initElement();
 
            window.sss = this;
 
        },
 
        $: function (selector) {
            return this.$el.find(selector);
        },
 
        //提供属性重置功能,对属性做检查
        resetPropery: function () { },
 
        //各事件注册点,用于被继承override
        addEvent: function () {
        },
 
        create: function () {
            this.trigger('onPreCreate');
            //如果没有传入模板,说明html结构已经存在
            this.createRoot(this.render());
 
            this.status = 'create';
            this.trigger('onCreate');
        },
 
        //实例化需要用到到dom元素
        initElement: function () { },
 
        render: function (callback) {
            var data = this.getViewModel() || {};
            var html = this.template;
            if (!this.template) return '';
            //引入预编译机制
            if (_.isFunction(this.template)) {
                html = this.template(data);
            } else {
                html = _.template(this.template)(data);
            }
            typeof callback == 'function' && callback.call(this);
            return html;
        },
 
        refresh: function (needRecreate) {
            this.resetPropery();
            if (needRecreate) {
                this.create();
            } else {
                this.$el.html(this.render());
            }
            this.initElement();
            if (this.status != 'hide') this.show();
            this.trigger('onRefresh');
        },
 
        /**
        * @description 组件显示方法,首次显示会将ui对象实际由内存插入包裹层
        * @method initialize
        * @param {Object} opts
        */
        show: function () {
            this.trigger('onPreShow');
            //      //如果包含就不要乱搞了
            //      if (!$.contains(this.wrapper[0], this.$el[0])) {
            //        //如果需要清空容器的话便清空
            //        if (this.needEmptyWrapper) this.wrapper.html('');
            //        this.wrapper.append(this.$el);
            //      }
 
            this.$el.show();
            this.status = 'show';
 
            this.bindEvents();
 
            this.initHeader();
            this.trigger('onShow');
        },
 
        initHeader: function () { },
 
        hide: function () {
            if (!this.$el || this.status !== 'show') return;
 
            this.trigger('onPreHide');
            this.$el.hide();
 
            this.status = 'hide';
            this.unBindEvents();
            this.trigger('onHide');
        },
 
        destroy: function () {
            this.status = 'destroy';
            this.unBindEvents();
            this.$root.remove();
            this.trigger('onDestroy');
            delete this;
        },
 
        bindEvents: function () {
            var events = this.events;
 
            if (!(events || (events = _.result(this, 'events')))) return this;
            this.unBindEvents();
 
            // 解析event参数的正则
            var delegateEventSplitter = /^(S+)s*(.*)$/;
            var key, method, match, eventName, selector;
 
            // 做简单的字符串数据解析
            for (key in events) {
                method = events[key];
                if (!_.isFunction(method)) method = this[events[key]];
                if (!method) continue;
 
                match = key.match(delegateEventSplitter);
                eventName = match[1], selector = match[2];
                method = _.bind(method, this);
                eventName += '.delegateUIEvents' + this.id;
 
                if (selector === '') {
                    this.$el.on(eventName, method);
                } else {
                    this.$el.on(eventName, selector, method);
                }
            }
 
            return this;
        },
 
        unBindEvents: function () {
            this.$el.off('.delegateUIEvents' + this.id);
            return this;
        },
 
        getParam: function (key) {
            return _.getUrlParam(window.location.href, key)
        },
 
        renderTpl: function (tpl, data) {
            if (!_.isFunction(tpl)) tpl = _.template(tpl);
            return tpl(data);
        }
 
    });
 
});
 
abstract.view

一个Page级别的View会有以下几个关键属性&方法:

① template,html字符串,不包含请求的基础模块,会构成页面的html骨架层

② events,所有的DOM事件定义处,以事件代理的方式定义,所以不必担心执行顺序

③ addEvent,用于页面级别各个阶段的监控事件注册点,一般来说用户只需要关注很少几个事件,比如:

JavaScript

//写法 addEvent: function () { //页面渲染结束,并显示时候触发的事件 this.on('onShow', function () { }); //离开页面,页面隐藏时候触发的事件 this.on('onHide', function () { }); }

1
2
3
4
5
6
7
8
9
//写法
addEvent: function () {
   //页面渲染结束,并显示时候触发的事件
    this.on('onShow', function () {
    });
    //离开页面,页面隐藏时候触发的事件
    this.on('onHide', function () {
    });
}

一个页面的基本写法:

JavaScript

define(['AbstractView'], function (AbstractView) { return _.inherit(AbstractView, { propertys: function ($super) { $super(); //一堆基础属性定义 //...... //交互业务逻辑 this.events = { 'click .js_pre_day': 'preAction' }; }, preAction: function (e) { }, addEvent: function () { this.on('onShow', function () { //当页面渲染结束,需要做的初始化操作,比如渲染页面 //...... }); this.on('onHide', function () { }); } }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
define(['AbstractView'], function (AbstractView) {
    return _.inherit(AbstractView, {
        propertys: function ($super) {
            $super();
            //一堆基础属性定义
            //......
            //交互业务逻辑
            this.events = {
                'click .js_pre_day': 'preAction'
            };
        },
        preAction: function (e) { },
        addEvent: function () {
            this.on('onShow', function () {
                //当页面渲染结束,需要做的初始化操作,比如渲染页面
                //......
            });
            this.on('onHide', function () {
            });
        }
    });
});

只要按照这种规则写,便能展示页面,并且具备DOM交互事件。

请求类

虽然get类请求可以用jsonp的方式绕过跨域问题,但是post请求却是真正的拦路虎,为了安全性服务器设置cors会仅仅针对几个域名,Hybrid内嵌静态资源是通过file的方式读取,这种场景使用cors就不好使了,所以每个请求需要经过Native做一层代理发出去。

葡京正网网投 32

这个使用场景与Header组件一致,前端框架层必须做到对业务透明化,业务事实上不必关心这个请求是由浏览器发出还是由Native发出:

JavaScript

HybridGet = function (url, param, callback) { }; HybridPost = function (url, param, callback) { };

1
2
3
4
HybridGet = function (url, param, callback) {
};
HybridPost = function (url, param, callback) {
};

真实的业务场景,会将之封装到数据请求模块,在底层做适配,在H5站点下使用ajax请求,在Native内嵌时使用代理发出,与Native的约定为:

JavaScript

requestHybrid({ tagname: 'ajax', param: { url: 'hotel/detail', param: {}, //默认为get type: 'post' }, //响应后的回调 callback: function (data) { } });

1
2
3
4
5
6
7
8
9
10
11
requestHybrid({
    tagname: 'ajax',
    param: {
        url: 'hotel/detail',
        param: {},
        //默认为get
        type: 'post'
    },
    //响应后的回调
    callback: function (data) { }
});

更新率

我们有时候想要的是一旦增量包发布,用户拿着手机就马上能看到最新的内容了,而这样需要app调用增量包的频率增高,所以我们是设置每30分钟检查一次更新。

demo地址

测试糯米时请扫描第二个二维码:

葡京正网网投 33 

这里抛出了前端多Webview容器会遇到的一些问题,并提出了一个解决思路,后续可能会有更加完整解决方案与demo出来,希望对各位有用,若是有已经涉及到这块业务的朋友可以私下交流下。

文中是我个人的一些开发经验,希望对各位有用,也希望各位 多多支持讨论 ,指...

页面模块类

所谓页面模块类,便是用于拆分一个页面为单个组件模块所用类,这里有这些约定:

JavaScript

① 一个模块类实例一定会依赖一个Page的基类实例 ② 模块类实例通过this.view可以访问到依赖类的一切资源 ③ 模块类实例与模块之间通过数据entity做通信

1
2
3
① 一个模块类实例一定会依赖一个Page的基类实例
② 模块类实例通过this.view可以访问到依赖类的一切资源
③ 模块类实例与模块之间通过数据entity做通信

这里代码可以再优化,但不是我们这里关注的重点:

JavaScript

define([], function () { 'use strict'; return _.inherit({ propertys: function () { //这里设置UI的根节点所处包裹层,必须设置 this.$el = null; //用于定位dom的选择器 this.selector = ''; //每个moduleView必须有一个父view,页面级容器 this.view = null; //模板字符串,各个组件不同,现在加入预编译机制 this.template = ''; //事件机制 this.events = {}; //实体model,跨模块通信的桥梁 this.entity = null; }, setOption: function (options) { //这里可以写成switch,开始没有想到有这么多分支 for (var k in options) { if (k == 'events') { _.extend(this[k], options[k]); continue; } this[k] = options[k]; } // _.extend(this, options); }, //@override initData: function () { }, //如果传入了dom便 initWrapper: function (el) { if (el && el[0]) { this.$el = el; return; } this.$el = this.view.$(this.selector); }, initialize: function (opts) { //这种默认属性 this.propertys(); //根据参数重置属性 this.setOption(opts); this.initData(); this.initWithoutRender(); }, //处理dom已经存在,不需要渲染的情况 initWithoutRender: function () { if (this.template) return; var scope = this; this.view.on('onShow', function () { scope.initWrapper(); if (!scope.$el[0]) return; //如果没有父view则不能继续 if (!scope.view) return; scope.initElement(); scope.bindEvents(); }); }, $: function (selector) { return this.$el.find(selector); }, //实例化需要用到到dom元素 initElement: function () { }, //@override //收集来自各方的实体组成view渲染需要的数据,需要重写 getViewModel: function () { throw '必须重写'; }, _render: function (callback) { var data = this.getViewModel() || {}; var html = this.template; if (!this.template) return ''; //引入预编译机制 if (_.isFunction(this.template)) { html = this.template(data); } else { html = _.template(this.template)(data); } typeof callback == 'function' && callback.call(this); return html; }, //渲染时必须传入dom映射 render: function () { this.initWrapper(); if (!this.$el[0]) return; //如果没有父view则不能继续 if (!this.view) return; var html = this._render(); this.$el.html(html); this.initElement(); this.bindEvents(); }, bindEvents: function () { var events = this.events; if (!(events || (events = _.result(this, 'events')))) return this; this.unBindEvents(); // 解析event参数的正则 var delegateEventSplitter = /^(S+)s*(.*)$/; var key, method, match, eventName, selector; // 做简单的字符串数据解析 for (key in events) { method = events[key]; if (!_.isFunction(method)) method = this[events[key]]; if (!method) continue; match = key.match(delegateEventSplitter); eventName = match[1], selector = match[2]; method = _.bind(method, this); eventName += '.delegateUIEvents' + this.id; if (selector === '') { this.$el.on(eventName, method); } else { this.$el.on(eventName, selector, method); } } return this; }, unBindEvents: function () { this.$el.off('.delegateUIEvents' + this.id); return this; } }); }); module.view

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
define([], function () {
    'use strict';
 
    return _.inherit({
 
        propertys: function () {
            //这里设置UI的根节点所处包裹层,必须设置
            this.$el = null;
 
            //用于定位dom的选择器
            this.selector = '';
 
            //每个moduleView必须有一个父view,页面级容器
            this.view = null;
 
            //模板字符串,各个组件不同,现在加入预编译机制
            this.template = '';
 
            //事件机制
            this.events = {};
 
            //实体model,跨模块通信的桥梁
            this.entity = null;
        },
 
        setOption: function (options) {
            //这里可以写成switch,开始没有想到有这么多分支
            for (var k in options) {
                if (k == 'events') {
                    _.extend(this[k], options[k]);
                    continue;
                }
                this[k] = options[k];
            }
            //      _.extend(this, options);
        },
 
        //@override
        initData: function () {
        },
 
        //如果传入了dom便
        initWrapper: function (el) {
            if (el && el[0]) {
                this.$el = el;
                return;
            }
            this.$el = this.view.$(this.selector);
        },
 
        initialize: function (opts) {
 
            //这种默认属性
            this.propertys();
            //根据参数重置属性
            this.setOption(opts);
            this.initData();
 
            this.initWithoutRender();
 
        },
 
        //处理dom已经存在,不需要渲染的情况
        initWithoutRender: function () {
            if (this.template) return;
            var scope = this;
            this.view.on('onShow', function () {
                scope.initWrapper();
                if (!scope.$el[0]) return;
                //如果没有父view则不能继续
                if (!scope.view) return;
                scope.initElement();
                scope.bindEvents();
            });
        },
 
        $: function (selector) {
            return this.$el.find(selector);
        },
 
        //实例化需要用到到dom元素
        initElement: function () { },
 
        //@override
        //收集来自各方的实体组成view渲染需要的数据,需要重写
        getViewModel: function () {
            throw '必须重写';
        },
 
        _render: function (callback) {
            var data = this.getViewModel() || {};
            var html = this.template;
            if (!this.template) return '';
            //引入预编译机制
            if (_.isFunction(this.template)) {
                html = this.template(data);
            } else {
                html = _.template(this.template)(data);
            }
            typeof callback == 'function' && callback.call(this);
            return html;
        },
 
        //渲染时必须传入dom映射
        render: function () {
            this.initWrapper();
            if (!this.$el[0]) return;
 
            //如果没有父view则不能继续
            if (!this.view) return;
 
            var html = this._render();
            this.$el.html(html);
            this.initElement();
            this.bindEvents();
 
        },
 
        bindEvents: function () {
            var events = this.events;
 
            if (!(events || (events = _.result(this, 'events')))) return this;
            this.unBindEvents();
 
            // 解析event参数的正则
            var delegateEventSplitter = /^(S+)s*(.*)$/;
            var key, method, match, eventName, selector;
 
            // 做简单的字符串数据解析
            for (key in events) {
                method = events[key];
                if (!_.isFunction(method)) method = this[events[key]];
                if (!method) continue;
 
                match = key.match(delegateEventSplitter);
                eventName = match[1], selector = match[2];
                method = _.bind(method, this);
                eventName += '.delegateUIEvents' + this.id;
 
                if (selector === '') {
                    this.$el.on(eventName, method);
                } else {
                    this.$el.on(eventName, selector, method);
                }
            }
 
            return this;
        },
 
        unBindEvents: function () {
            this.$el.off('.delegateUIEvents' + this.id);
            return this;
        }
    });
 
});
 
module.view

常用NativeUI组件

最后,Native会提供几个常用的Native级别的UI,比如loading加载层,比如toast消息框:

JavaScript

var HybridUI = {}; HybridUI.showLoading(); //=> requestHybrid({ tagname: 'showLoading' }); HybridUI.showToast({ title: '111', //几秒后自动关闭提示框,-1需要点击才会关闭 hidesec: 3, //弹出层关闭时的回调 callback: function () { } }); //=> requestHybrid({ tagname: 'showToast', param: { title: '111', hidesec: 3, callback: function () { } } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var HybridUI = {};
HybridUI.showLoading();
//=>
requestHybrid({
    tagname: 'showLoading'
});
 
HybridUI.showToast({
    title: '111',
    //几秒后自动关闭提示框,-1需要点击才会关闭
    hidesec: 3,
    //弹出层关闭时的回调
    callback: function () { }
});
//=>
requestHybrid({
    tagname: 'showToast',
    param: {
        title: '111',
        hidesec: 3,
        callback: function () { }
    }
});

Native UI与前端UI不容易打通,所以在真实业务开发过程中,一般只会使用几个关键的Native UI。

正确读取

这里可能有点杞人忧天,因为Native程序不是自己手把手开发的,总是担心APP在正在拉取增量包时,或者正在解压时,读取了静态文件,这样会不会读取错误呢,后面想了想,便继续采用了之前的md5打包的方式,将落地的html中需要的文件打包为md5引用,如果落地页下载下来后,读不到本地文件就自己会去拉取线上资源咯。

数据实体类

这里的数据实体对应着,MVC中的Model,因为之前已经使用model用作了数据请求相关的命名,这里便使用Entity做该工作:

JavaScript

define([], function () { /* 一些原则: init方法时,不可引起其它字段update */ var Entity = _.inherit({ initialize: function (opts) { this.propertys(); this.setOption(opts); }, propertys: function () { //只取页面展示需要数据 this.data = {}; //局部数据改变对应的响应程序,暂定为一个方法 //可以是一个类的实例,如果是实例必须有render方法 this.controllers = {}; this.scope = null; }, subscribe: function (namespace, callback, scope) { if (typeof namespace === 'function') { scope = callback; callback = namespace; namespace = 'update'; } if (!namespace || !callback) return; if (scope) callback = $.proxy(callback, scope); if (!this.controllers[namespace]) this.controllers[namespace] = []; this.controllers[namespace].push(callback); }, unsubscribe: function (namespace) { if (!namespace) this.controllers = {}; if (this.controllers[namespace]) this.controllers[namespace] = []; }, publish: function (namespace, data) { if (!namespace) return; if (!this.controllers[namespace]) return; var arr = this.controllers[namespace]; var i, len = arr.length; for (i = 0; i < len; i++) { arr[i](data); } }, setOption: function (opts) { for (var k in opts) { this[k] = opts[k]; } }, //首次初始化时,需要矫正数据,比如做服务器适配 //@override handleData: function () { }, //一般用于首次根据服务器数据源填充数据 initData: function (data) { var k; if (!data) return; //如果默认数据没有被覆盖可能有误 for (k in this.data) { if (data[k]) this.data[k] = data[k]; } this.handleData(); this.publish('init', this.get()); }, //验证data的有效性,如果无效的话,不应该进行以下逻辑,并且应该报警 //@override validateData: function () { return true; }, //获取数据前,可以进行格式化 //@override formatData: function (data) { return data; }, //获取数据 get: function () { if (!this.validateData()) { //需要log return {}; } return this.formatData(this.data); }, //数据跟新后需要做的动作,执行对应的controller改变dom //@override update: function (key) { key = key || 'update'; var data = this.get(); this.publish(key, data); } }); return Entity; }); abstract.entity

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
define([], function () {
    /*
    一些原则:
    init方法时,不可引起其它字段update
    */
    var Entity = _.inherit({
        initialize: function (opts) {
            this.propertys();
            this.setOption(opts);
        },
 
        propertys: function () {
            //只取页面展示需要数据
            this.data = {};
 
            //局部数据改变对应的响应程序,暂定为一个方法
            //可以是一个类的实例,如果是实例必须有render方法
            this.controllers = {};
 
            this.scope = null;
 
        },
 
        subscribe: function (namespace, callback, scope) {
            if (typeof namespace === 'function') {
                scope = callback;
                callback = namespace;
                namespace = 'update';
            }
            if (!namespace || !callback) return;
            if (scope) callback = $.proxy(callback, scope);
            if (!this.controllers[namespace]) this.controllers[namespace] = [];
            this.controllers[namespace].push(callback);
        },
 
        unsubscribe: function (namespace) {
            if (!namespace) this.controllers = {};
            if (this.controllers[namespace]) this.controllers[namespace] = [];
        },
 
        publish: function (namespace, data) {
            if (!namespace) return;
            if (!this.controllers[namespace]) return;
            var arr = this.controllers[namespace];
            var i, len = arr.length;
            for (i = 0; i < len; i++) {
                arr[i](data);
            }
        },
 
        setOption: function (opts) {
            for (var k in opts) {
                this[k] = opts[k];
            }
        },
 
        //首次初始化时,需要矫正数据,比如做服务器适配
        //@override
        handleData: function () { },
 
        //一般用于首次根据服务器数据源填充数据
        initData: function (data) {
            var k;
            if (!data) return;
 
            //如果默认数据没有被覆盖可能有误
            for (k in this.data) {
                if (data[k]) this.data[k] = data[k];
            }
 
            this.handleData();
            this.publish('init', this.get());
        },
 
        //验证data的有效性,如果无效的话,不应该进行以下逻辑,并且应该报警
        //@override
        validateData: function () {
            return true;
        },
 
        //获取数据前,可以进行格式化
        //@override
        formatData: function (data) {
            return data;
        },
 
        //获取数据
        get: function () {
            if (!this.validateData()) {
                //需要log
                return {};
            }
            return this.formatData(this.data);
        },
 
        //数据跟新后需要做的动作,执行对应的controller改变dom
        //@override
        update: function (key) {
            key = key || 'update';
            var data = this.get();
            this.publish(key, data);
        }
 
    });
 
    return Entity;
});
 
abstract.entity

这里的数据实体会以实例的方式注入给模块类实例,他的工作是起一个中枢左右,完成模块之间的通信,反正非常重要就是了

账号系统的设计

根据上面的设计,我们约定在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请求接口,如果没有登录则在底层唤起登录框(需要前端框架支持)

简单的登录登出接口实现:

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 () {
};

账号信息获取

在实际的业务开发中,判断用户是否登录、获取用户基本信息的需求比比皆是,所以这里必须保证Hybrid开发模式与H5开发模式保持统一,否则需要在业务代码中做很多无谓的判断,我们在前端框架会封装一个User模块,主要接口包括:

JavaScript

1 var User = {}; 2 User.isLogin = function () { }; 3 User.getInfo = function () { };

1
2
3
1 var User = {};
2 User.isLogin = function () { };
3 User.getInfo = function () { };

这个代码的底层实现分为前端实现,Native实现,首先是前端的做法是:

当前端页面载入后,会做一次异步请求,请求用户相关数据,如果是登录状态便能获取数据存于localstorage中,这里一定不能存取敏感信息

前端使用localstorage的话需要考虑极端情况下使用内存变量的方式替换localstorage的实现,否则会出现不可使用的情况,而后续的访问皆是使用localstorage中的数据做判断依据,以下情况需要清理localstorage的账号数据:

① 系统登出

② 访问接口提示需要登录

③ 调用登录接口

这种模式多用于单页应用,非单页应用一般会在每次刷新页面先清空账号信息再异步拉取,但是如果当前页面马上就需要判断用户登录数据的话,便不可靠了;处于Hybrid容器中时,因为Native本身就保存了用户信息,封装的接口直接由Native获取即可,这块比较靠谱。

调试

一个Hybrid项目,要最大限度的符合前端的开发习惯,并且要提供可调试方案

1
一个Hybrid项目,要最大限度的符合前端的开发习惯,并且要提供可调试方案

我们之前说过直接将所有请求用native发出有一个最大的问题就是调试不方便,而正确的hybrid的开发应该是有70%以上的时间,纯业务开发者不需要关心native联调,当所有业务开发结束后再内嵌简单调一下即可。

因为调试时候需要读取测试环境资源,需要server端qa接口有个全局开关,关闭所有的增量读取

1
因为调试时候需要读取测试环境资源,需要server端qa接口有个全局开关,关闭所有的增量读取

关于代理调试的方法已经很多人介绍过了,我这里不再多说,说一些native中的调试方案吧,其实很多人都知道。

其它

数据请求统一使用abstract.model,数据前端缓存使用abstract.store,这里因为目标是做页面拆分,请求模块不是关键,各位可以把这段代码看层一个简单的ajax即可:

JavaScript

this.model.setParam({}); this.model.execute(function (data) { });

1
2
3
this.model.setParam({});
this.model.execute(function (data) {
});

Hybrid的资源

iOS

首先,你需要拥有一台Mac机,然后打开safari;在偏好设置中将开发模式打开:

葡京正网网投 34

然后打开模拟器,即可开始调试咯:

葡京正网网投 35

业务入口

最后简单说下业务入口文件:

JavaScript

(function () { var project = './'; var viewRoot = 'pages'; require.config({ paths: { //BUS相关模板根目录 IndexPath: project + 'pages/index', ListPath: project + 'pages/list', BusStore: project + 'model/bus.store', BusModel: project + 'model/bus.model' } }); require(['AbstractApp', 'UIHeader'], function (APP, UIHeader) { window.APP = new APP({ UIHeader: UIHeader, viewRootPath: viewRoot }); window.APP.initApp(); }); })();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function () {
    var project = './';
    var viewRoot = 'pages';
    require.config({
        paths: {
            //BUS相关模板根目录
            IndexPath: project + 'pages/index',
            ListPath: project + 'pages/list',
 
            BusStore: project + 'model/bus.store',
            BusModel: project + 'model/bus.model'
        }
    });
    require(['AbstractApp', 'UIHeader'], function (APP, UIHeader) {
        window.APP = new APP({
            UIHeader: UIHeader,
            viewRootPath: viewRoot
        });
        window.APP.initApp();
    });
})();

很简单的代码,指定了下require的path配置,最后我们看看入口页面的调用:

<!doctype html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui" /> <meta content="yes" name="apple-mobile-web-app-capable" /> <meta content="black" name="apple-mobile-web-app-status-bar-style" /> <meta name="format-detection" content="telephone=no" /> <link href="../static/css/global.css" rel="stylesheet" type="text/css" /> <title>班次列表</title> </head> <body> <div id="headerview"> <div class="cm-header"> <h1 class="cm-page-title js_title"> 正在加载... </h1> </div> </div> <div class="cm-page-wrap"> <div class="cm-state" id="js_page_state"> </div> <article class="cm-page" id="main"> </article> </div> <script type="text/javascript" src="../blade/libs/zepto.js"></script> <script src="../blade/libs/fastclick.js" type="text/javascript"></script> <script type="text/javascript" src="../blade/libs/underscore.js"></script> <script src="../blade/libs/underscore.extend.js" type="text/javascript"></script> <script type="text/javascript" src="../blade/libs/require.js"></script> <script type="text/javascript" src="../blade/common.js"></script> <script type="text/javascript" src="main.js"></script> </body> </html> list.html list.html

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
<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui" />
  <meta content="yes" name="apple-mobile-web-app-capable" />
  <meta content="black" name="apple-mobile-web-app-status-bar-style" />
  <meta name="format-detection" content="telephone=no" />
  <link href="../static/css/global.css" rel="stylesheet" type="text/css" />
  <title>班次列表</title>
</head>
<body>
  <div id="headerview">
    <div class="cm-header">
      <h1 class="cm-page-title js_title">
        正在加载...
      </h1>
    </div>
  </div>
  <div class="cm-page-wrap">
    <div class="cm-state" id="js_page_state">
    </div>
    <article class="cm-page" id="main">
    </article>
  </div>
  <script type="text/javascript" src="../blade/libs/zepto.js"></script>
  <script src="../blade/libs/fastclick.js" type="text/javascript"></script>
  <script type="text/javascript" src="../blade/libs/underscore.js"></script>
  <script src="../blade/libs/underscore.extend.js" type="text/javascript"></script>
  <script type="text/javascript" src="../blade/libs/require.js"></script>
  <script type="text/javascript" src="../blade/common.js"></script>
  <script type="text/javascript" src="main.js"></script>
</body>
</html>
 
list.html
 
list.html

webapp ├─blade //框架目录 │ ├─data │ ├─libs │ ├─mvc │ └─ui ├─bus │ ├─model //数据请求模块,完全可以使用zepto ajax替换 │ └─pages │ ├─booking │ ├─index │ └─list //demo代码模块 └─static

1
2
3
4
5
6
7
8
9
10
11
12
13
webapp
├─blade //框架目录
│  ├─data
│  ├─libs
│  ├─mvc
│  └─ui
├─bus
│  ├─model //数据请求模块,完全可以使用zepto ajax替换
│  └─pages
│      ├─booking
│      ├─index
│      └─list //demo代码模块
└─static

接下来,让我们真实的开始拆分页面吧。

目录结构

Hybrid技术既然是将静态资源存于Native,那么就需要目录设计,经过之前的经验,目录结构一般以2层目录划分:

葡京正网网投 36

如果我们有两个频道酒店与机票,那么目录结构是这样的:

webapp //根目录 ├─flight ├─hotel //酒店频道 │ │ index.html //业务入口html资源,如果不是单页应用会有多个入口 │ │ main.js //业务所有js资源打包 │ │ │ └─static //静态样式资源 │ ├─css │ ├─hybrid //存储业务定制化类Native Header图标 │ └─images ├─libs │ libs.js //框架所有js资源打包 │ └─static ├─css └─images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
webapp //根目录
├─flight
├─hotel //酒店频道
│  │  index.html //业务入口html资源,如果不是单页应用会有多个入口
│  │  main.js //业务所有js资源打包
│  │
│  └─static //静态样式资源
│      ├─css
│      ├─hybrid //存储业务定制化类Native Header图标
│      └─images
├─libs
│      libs.js //框架所有js资源打包
└─static
    ├─css
    └─images

最初设计的forward跳转中的topage参数规则是:频道/具体页面=>channel/page,其余资源会由index.html这个入口文件带出。

Android

Android需要能FQ的chrome,然后输入chrome://inspect/#devices即可,前提是native同事为你打开调试模式,当然Android也可以使用模拟器啦,但是Android的真机表现过于不一样,还是建议使用真机测试。

组件式编程

增量机制

真实的增量机制需要服务器端的配合,我这里只能简单描述,Native端会有维护一个版本映射表:

JavaScript

{ flight: 1.0.0, hotel: 1.0.0, libs: 1.0.0, static: 1.0.0 }

1
2
3
4
5
6
{
  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修复。

一些坑点

骨架设计

首先,我们进行最简单的骨架设计,这里依次是其js代码与模板代码:

JavaScript

define(['AbstractView', 'text!ListPath/list.css', 'text!ListPath/tpl.layout.html'], function (AbstractView, style, layoutHtml) { return _.inherit(AbstractView, { propertys: function ($super) { $super(); this.style = style; this.template = layoutHtml; }, initHeader: function (name) { var title = '班次列表'; this.header.set({ view: this, title: title }); }, addEvent: function () { this.on('onShow', function () { console.log('页面渲染结束'); }); } }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
define(['AbstractView', 'text!ListPath/list.css', 'text!ListPath/tpl.layout.html'], function (AbstractView, style, layoutHtml) {
    return _.inherit(AbstractView, {
        propertys: function ($super) {
            $super();
            this.style = style;
            this.template = layoutHtml;
        },
 
        initHeader: function (name) {
            var title = '班次列表';
            this.header.set({
                view: this,
                title: title
            });
        },
 
        addEvent: function () {
            this.on('onShow', function () {
                console.log('页面渲染结束');
            });
        }
    });
});

<div class="calendar-bar-wrapper js_calendar_wrapper"> 日历工具条模块 </div> <div class="none-data js_none_data" style="display: none;"> 当前暂无班次可预订</div> <div class="js_list_wrapper"> 列表模块 </div> <div class="js_list_loading" style="display: none; text-align: center; padding: 10px 0;"> 正在加载...</div> <ul class="bus-tabs list-filter"> <li class="tabs-item js_show_setoutdate"> <div class="line"> <i class="icon-time"></i>出发时段<i class="icon-sec"></i></div> <div class="line js_day_sec"> 全天</div> </li> <li class="tabs-item js_show_setstation"> <div class="line"> <i class="icon-circle icon-setout "></i>出发汽车站<i class="icon-sec"></i></div> <div class="line js_start_sec"> 全部车站</div> </li> <li class="tabs-item js_show_arrivalstation"> <div class="line"> <i class="icon-circle icon-arrival "></i>到达汽车站<i class="icon-sec"></i></div> <div class="line js_arrival_sec"> 全部车站</div> </li> </ul> tpl.layout

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
<div class="calendar-bar-wrapper js_calendar_wrapper">
    日历工具条模块
</div>
<div class="none-data js_none_data" style="display: none;">
    当前暂无班次可预订</div>
<div class="js_list_wrapper">
    列表模块
</div>
<div class="js_list_loading" style="display: none; text-align: center; padding: 10px 0;">
    正在加载...</div>
<ul class="bus-tabs list-filter">
    <li class="tabs-item js_show_setoutdate">
        <div class="line">
            <i class="icon-time"></i>出发时段<i class="icon-sec"></i></div>
        <div class="line js_day_sec">
            全天</div>
    </li>
    <li class="tabs-item js_show_setstation">
        <div class="line">
            <i class="icon-circle icon-setout "></i>出发汽车站<i class="icon-sec"></i></div>
        <div class="line js_start_sec">
            全部车站</div>
    </li>
    <li class="tabs-item js_show_arrivalstation">
        <div class="line">
            <i class="icon-circle icon-arrival "></i>到达汽车站<i class="icon-sec"></i></div>
        <div class="line js_arrival_sec">
            全部车站</div>
    </li>
</ul>
 
tpl.layout

页面展示如图:

葡京正网网投 37

结语

github上代码会持续更新,现在界面反正不太好看,大家多多包涵吧,这里是一些效果图:

葡京正网网投 38葡京正网网投 39葡京正网网投 40

Hybrid方案是快速迭代项目,快速占领市场的神器,希望此文能对准备接触Hybrid技术的朋友提供一些帮助,并且再次感谢明月同学的配合。

 

1 赞 4 收藏 评论

葡京正网网投 41

不要命就用swift

苹果官方出了swift,于是我们iOS团队好事者尝试了感觉不错,便迅速在团队内部推广了起来,而我们OC本身的体量本来就有10多万行代码量,我们都知道一个道理:

重构一时爽,项目火葬场

1
重构一时爽,项目火葬场

而重构过程中肯定又会遇到一些历史问题,或者一些第三方库,代码总会有一点尿不尽一点冗余,而不知道swift是官方有问题还是怎么回事,每次稍微多一些改动就需要编译一个多小时!!!!你没看错,是要编译一个多小时。

一次,我的小伙伴在打游戏,被我揪着说了两句,他说他在编译,我尼玛很不屑的骂了他,后面开始调iOS时,编译了2小时!!!从那以后看见他打游戏我一点脾气都没有了!!!

这种编译的感觉,就像吃坏了肚子,在厕所蹲了半天却什么也没拉出来一样!!!所以,不要命就全部换成swift吧。

如果有一定历史包袱的业务,或者新业务,最好不要全面使用新技术,不成熟的技术,如果有什么不可逆的坑,那么会连一点退路都没有了。

1
如果有一定历史包袱的业务,或者新业务,最好不要全面使用新技术,不成熟的技术,如果有什么不可逆的坑,那么会连一点退路都没有了。

日历工具栏的实现

这里要做的第一步是将日历工具栏模块实现,以数据为先的思考,我们先实现了一个与日历业务有关的数据实体:

JavaScript

define(['AbstractEntity'], function (AbstractEntity) { var Entity = _.inherit(AbstractEntity, { propertys: function ($super) { $super(); var n = new Date(); var curTime = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime(); this.data = { date: curTime, title: '当前日期' }; }, set: function (date) { if (!date) return; if (_.isDate(date)) date = date.getTime(); if (typeof date === 'string') date = parseInt(date); this.data.date = date; this.update(); }, getDateStr: function () { var date = new Date(); date.setTime(this.data.date); var dateDetail = _.dateUtil.getDetail(date); var name = dateDetail.year + '-' + dateDetail.month + '-' + dateDetail.day + ' ' + dateDetail.weekday + (dateDetail.day1 ? '(' + dateDetail.day1 + ')' : ''); return name; }, nextDay: function () { this.set(this.getDate() + 86400000); return true; }, getDate: function () { return parseInt(this.data.date); }, //是否能够再往前一天 canPreDay: function () { var n = new Date(); var curTime = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime(); //如果当前日期已经是第一天,则不可预订 if (curTime <= this.getDate() - 86400000) { return true; } return false; }, preDay: function () { if (!this.canPreDay()) return false; this.set(this.getDate() - 86400000); return true; } }); return Entity; }); en.date

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
58
59
60
61
62
define(['AbstractEntity'], function (AbstractEntity) {
 
    var Entity = _.inherit(AbstractEntity, {
        propertys: function ($super) {
            $super();
            var n = new Date();
            var curTime = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime();
            this.data = {
                date: curTime,
                title: '当前日期'
            };
        },
 
        set: function (date) {
            if (!date) return;
            if (_.isDate(date)) date = date.getTime();
            if (typeof date === 'string') date = parseInt(date);
            this.data.date = date;
            this.update();
        },
 
        getDateStr: function () {
            var date = new Date();
            date.setTime(this.data.date);
            var dateDetail = _.dateUtil.getDetail(date);
            var name = dateDetail.year + '-' + dateDetail.month + '-' + dateDetail.day + ' ' + dateDetail.weekday + (dateDetail.day1 ? '(' + dateDetail.day1 + ')' : '');
            return name;
        },
 
        nextDay: function () {
            this.set(this.getDate() + 86400000);
            return true;
        },
 
        getDate: function () {
            return parseInt(this.data.date);
        },
 
        //是否能够再往前一天
        canPreDay: function () {
            var n = new Date();
            var curTime = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime();
 
            //如果当前日期已经是第一天,则不可预订
            if (curTime <= this.getDate() - 86400000) {
                return true;
            }
            return false;
        },
 
        preDay: function () {
            if (!this.canPreDay()) return false;
            this.set(this.getDate() - 86400000);
            return true;
        }
 
    });
 
    return Entity;
});
 
en.date

里面完成日期工具栏所有相关数据操作,并且不包含实际的业务逻辑。

然后这里开始设计日期工具栏的模块View:

JavaScript

define(['ModuleView', 'UICalendarBox', 'text!ListPath/tpl.calendar.bar.html'], function (ModuleView, UICalendarBox, tpl) { return _.inherit(ModuleView, { //此处若是要使用model,处实例化时候一定要保证entity的存在,如果不存在便是业务BUG initData: function () { this.template = tpl; this.events = { 'click .js_pre_day': 'preAction', 'click .js_next_day': 'nextAction', 'click .js_show_calendar': 'showCalendar' }; //初始化时候需要执行的回调 this.dateEntity.subscribe('init', this.render, this); this.dateEntity.subscribe(this.render, this); }, initDate: function () { var t = new Date().getTime(); //默认情况下获取当前日期,也有过了18.00就设置为第二天日期 //当时一旦url上有startdatetime参数的话,便需要使用之 if (_.getUrlParam().startdatetime) t = _.getUrlParam().startdatetime; this.dateEntity.initData({ date: t }); }, getViewModel: function () { var data = this.dateEntity.get(); data.formatStr = this.dateEntity.getDateStr(); data.canPreDay = this.dateEntity.canPreDay(); return data; }, preAction: function () { if (this.dateEntity.preDay()) return; this.view.showToast('前一天不可预订'); }, nextAction: function () { this.dateEntity.nextDay(); }, showCalendar: function () { var scope = this, endDate = new Date(); var secDate = new Date(); secDate.setTime(this.dateEntity.getDate()); endDate.setTime(new Date().getTime() + 2592000000); if (!this.calendar) { this.calendar = new UICalendarBox({ endTime: endDate, selectDate: secDate, onItemClick: function (date, el, e) { scope.dateEntity.set(date); this.hide(); } }); } else { this.calendar.calendar.selectDate = secDate; this.calendar.calendar.refresh(); } this.calendar.show(); } }); }); mod.date

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
define(['ModuleView', 'UICalendarBox', 'text!ListPath/tpl.calendar.bar.html'], function (ModuleView, UICalendarBox, tpl) {
    return _.inherit(ModuleView, {
 
        //此处若是要使用model,处实例化时候一定要保证entity的存在,如果不存在便是业务BUG
        initData: function () {
 
            this.template = tpl;
            this.events = {
                'click .js_pre_day': 'preAction',
                'click .js_next_day': 'nextAction',
                'click .js_show_calendar': 'showCalendar'
            };
 
            //初始化时候需要执行的回调
            this.dateEntity.subscribe('init', this.render, this);
            this.dateEntity.subscribe(this.render, this);
 
        },
 
        initDate: function () {
            var t = new Date().getTime();
            //默认情况下获取当前日期,也有过了18.00就设置为第二天日期
            //当时一旦url上有startdatetime参数的话,便需要使用之
            if (_.getUrlParam().startdatetime) t = _.getUrlParam().startdatetime;
            this.dateEntity.initData({
                date: t
            });
        },
 
        getViewModel: function () {
            var data = this.dateEntity.get();
            data.formatStr = this.dateEntity.getDateStr();
            data.canPreDay = this.dateEntity.canPreDay();
            return data;
        },
 
        preAction: function () {
            if (this.dateEntity.preDay()) return;
            this.view.showToast('前一天不可预订');
        },
 
        nextAction: function () {
            this.dateEntity.nextDay();
        },
 
        showCalendar: function () {
            var scope = this, endDate = new Date();
            var secDate = new Date();
            secDate.setTime(this.dateEntity.getDate());
 
            endDate.setTime(new Date().getTime() + 2592000000);
 
            if (!this.calendar) {
                this.calendar = new UICalendarBox({
                    endTime: endDate,
                    selectDate: secDate,
                    onItemClick: function (date, el, e) {
                        scope.dateEntity.set(date);
                        this.hide();
                    }
                });
            } else {
                this.calendar.calendar.selectDate = secDate;
                this.calendar.calendar.refresh();
            }
            this.calendar.show();
        }
 
    });
 
});
 
mod.date

这个组件模块干了几个事情:

① 首先,dateEntity实体需要由list.js这个主view注入

② 这里为dateEntity注册了两个数据响应事件:

JavaScript

this.dateEntity.subscribe('init', this.render, this); this.dateEntity.subscribe(this.render, this);

1
2
this.dateEntity.subscribe('init', this.render, this);
this.dateEntity.subscribe(this.render, this);

render方法继承至基类,使用template与数据生成html,其中数据产生必须重写父类一个方法:

JavaScript

getViewModel: function () { var data = this.dateEntity.get(); data.formatStr = this.dateEntity.getDateStr(); data.canPreDay = this.dateEntity.canPreDay(); return data; },

1
2
3
4
5
6
getViewModel: function () {
    var data = this.dateEntity.get();
    data.formatStr = this.dateEntity.getDateStr();
    data.canPreDay = this.dateEntity.canPreDay();
    return data;
},

因为这里的日历数据,默认取当前时间,但是url参数可能传递日期参数,所以定义了一个数据初始化方法:

JavaScript

initDate: function () { var t = new Date().getTime(); //默认情况下获取当前日期,也有过了18.00就设置为第二天日期 //当时一旦url上有startdatetime参数的话,便需要使用之 if (_.getUrlParam().startdatetime) t = _.getUrlParam().startdatetime; this.dateEntity.initData({ date: t }); },

1
2
3
4
5
6
7
8
9
initDate: function () {
    var t = new Date().getTime();
    //默认情况下获取当前日期,也有过了18.00就设置为第二天日期
    //当时一旦url上有startdatetime参数的话,便需要使用之
    if (_.getUrlParam().startdatetime) t = _.getUrlParam().startdatetime;
    this.dateEntity.initData({
        date: t
    });
},

该方法在主页面渲染结束后会第一时间调用,这个时候日历工具栏便渲染出来,其中日历组件的使用便不予理睬了,主控制器的代码改变如下:

JavaScript

define([ 'AbstractView', 'text!ListPath/list.css', 'ListPath/en.date', 'ListPath/mod.date', 'text!ListPath/tpl.layout.html' ], function ( AbstractView, style, DateEntity, DateModule, layoutHtml ) { return _.inherit(AbstractView, { _initEntity: function () { this.dateEntity = new DateEntity(); }, _initModule: function () { this.dateModule = new DateModule({ view: this, selector: '.js_calendar_wrapper', dateEntity: this.dateEntity }); }, propertys: function ($super) { $super(); this._initEntity(); this._initModule(); this.style = style; this.template = layoutHtml; }, initHeader: function (name) { var title = '班次列表'; this.header.set({ view: this, title: title }); }, addEvent: function () { this.on('onShow', function () { //初始化date数据 this.dateModule.initDate(); }); } }); }); list.js

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
58
59
60
61
62
63
64
define([
    'AbstractView',
    'text!ListPath/list.css',
 
    'ListPath/en.date',
 
    'ListPath/mod.date',
 
    'text!ListPath/tpl.layout.html'
], function (
    AbstractView,
    style,
 
    DateEntity,
 
    DateModule,
 
    layoutHtml
) {
    return _.inherit(AbstractView, {
 
        _initEntity: function () {
            this.dateEntity = new DateEntity();
        },
 
        _initModule: function () {
            this.dateModule = new DateModule({
                view: this,
                selector: '.js_calendar_wrapper',
                dateEntity: this.dateEntity
            });
        },
 
        propertys: function ($super) {
            $super();
 
            this._initEntity();
            this._initModule();
 
            this.style = style;
            this.template = layoutHtml;
        },
 
        initHeader: function (name) {
            var title = '班次列表';
            this.header.set({
                view: this,
                title: title
            });
        },
 
        addEvent: function () {
            this.on('onShow', function () {
 
                //初始化date数据
                this.dateModule.initDate();
 
            });
        }
    });
 
});
 
list.js

JavaScript

_initEntity: function () { this.dateEntity = new DateEntity(); }, _initModule: function () { this.dateModule = new DateModule({ view: this, selector: '.js_calendar_wrapper', dateEntity: this.dateEntity }); },

1
2
3
4
5
6
7
8
9
10
11
_initEntity: function () {
    this.dateEntity = new DateEntity();
},
 
_initModule: function () {
    this.dateModule = new DateModule({
        view: this,
        selector: '.js_calendar_wrapper',
        dateEntity: this.dateEntity
    });
},

JavaScript

addEvent: function () { this.on('onShow', function () { //初始化date数据 this.dateModule.initDate(); }); }

1
2
3
4
5
6
7
addEvent: function () {
    this.on('onShow', function () {
        //初始化date数据
        this.dateModule.initDate();
 
    });
}

于是,整个界面变成了这个样子:

葡京正网网投 42

这里是对应的日历工具模板文件tpl.calendar.html:

<ul class="bus-tabs calendar-bar"> <li class="tabs-item js_pre_day <%=!canPreDay ? 'disabled' : '' %>">前一天</li> <li class="tabs-item js_show_calendar" style="-webkit-flex: 2; flex: 2;"><%=formatStr %></li> <li class="tabs-item js_next_day">后一天</li> </ul>

1
2
3
4
5
<ul class="bus-tabs calendar-bar">
    <li class="tabs-item  js_pre_day <%=!canPreDay ? 'disabled' : '' %>">前一天</li>
    <li class="tabs-item js_show_calendar" style="-webkit-flex: 2; flex: 2;"><%=formatStr %></li>
    <li class="tabs-item js_next_day">后一天</li>
</ul>

iOS静态资源缓存

Android有一个全局开关,控制静态资源部读取缓存,但是iOS中研究了好久,都没有找到这个开关,而他读取缓存又特别厉害,所以所有的请求资源在有增量包的情况下,最好加上时间戳或者md5

搜索工具栏的实现

我们现在的页面,就算不传任何URL参数,已经能渲染出部分页面了,但是下面出发站汽车等业务数据必须等待班次列表数据请求结束才能替换数据,但是这些数据如果没有出发城市和到达城市是不能发起请求的,所以这里先实现搜索工具栏功能:

在出发城市或者到达城市不存在的话便弹出搜索工具栏,引导用户选择城市,这里新增弹出层需要在主页面控制器(检测主控制器)中使用一个UI组件:

JavaScript

define([ 'AbstractView', 'text!ListPath/list.css', 'ListPath/en.date', 'ListPath/mod.date', 'text!ListPath/tpl.layout.html', 'text!ListPath/tpl.search.box.html', 'UIScrollLayer' ], function ( AbstractView, style, DateEntity, DateModule, layoutHtml, searchBoxHtml, UIScrollLayer ) { return _.inherit(AbstractView, { _initEntity: function () { this.dateEntity = new DateEntity(); }, _initModule: function () { this.dateModule = new DateModule({ view: this, selector: '.js_calendar_wrapper', dateEntity: this.dateEntity }); }, propertys: function ($super) { $super(); this._initEntity(); this._initModule(); this.style = style; this.template = layoutHtml; }, initHeader: function (name) { var title = '班次列表'; this.header.set({ view: this, title: title, back: function () { console.log('回退'); }, right: [ { tagname: 'search-bar', value: '搜索', callback: function () { console.log('弹出搜索框'); this.showSearchBox(); } } ] }); }, //搜索工具弹出层 showSearchBox: function () { var scope = this; if (!this.searchBox) { this.searchBox = new UIScrollLayer({ title: '请选择搜索条件', html: searchBoxHtml, events: { 'click .js-start': function () { }, 'click .js-arrive': function () { }, 'click .js_search_list': function () { console.log('查询列表'); } } }); } this.searchBox.show(); }, addEvent: function () { this.on('onShow', function () { //初始化date数据 this.dateModule.initDate(); //这里判断是否需要弹出搜索弹出层 if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) { this.showSearchBox(); return; } }); } }); }); list.js

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
define([
    'AbstractView',
    'text!ListPath/list.css',
 
    'ListPath/en.date',
 
    'ListPath/mod.date',
 
    'text!ListPath/tpl.layout.html',
    'text!ListPath/tpl.search.box.html',
    'UIScrollLayer'
], function (
    AbstractView,
    style,
 
    DateEntity,
 
    DateModule,
 
    layoutHtml,
    searchBoxHtml,
    UIScrollLayer
) {
    return _.inherit(AbstractView, {
 
        _initEntity: function () {
            this.dateEntity = new DateEntity();
        },
 
        _initModule: function () {
            this.dateModule = new DateModule({
                view: this,
                selector: '.js_calendar_wrapper',
                dateEntity: this.dateEntity
            });
        },
 
        propertys: function ($super) {
            $super();
 
            this._initEntity();
            this._initModule();
 
            this.style = style;
            this.template = layoutHtml;
        },
 
        initHeader: function (name) {
            var title = '班次列表';
            this.header.set({
                view: this,
                title: title,
                back: function () {
                    console.log('回退');
                },
                right: [
                    {
                        tagname: 'search-bar',
                        value: '搜索',
                        callback: function () {
                            console.log('弹出搜索框');
                            this.showSearchBox();
                        }
                    }
                ]
            });
        },
 
        //搜索工具弹出层
        showSearchBox: function () {
            var scope = this;
            if (!this.searchBox) {
                this.searchBox = new UIScrollLayer({
                    title: '请选择搜索条件',
                    html: searchBoxHtml,
                    events: {
                        'click .js-start': function () {
 
                        },
                        'click .js-arrive': function () {
 
                        },
                        'click .js_search_list': function () {
 
                            console.log('查询列表');
                        }
                    }
                });
            }
            this.searchBox.show();
        },
 
        addEvent: function () {
            this.on('onShow', function () {
                //初始化date数据
                this.dateModule.initDate();
 
                //这里判断是否需要弹出搜索弹出层
                if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) {
                    this.showSearchBox();
                    return;
                }
 
            });
        }
    });
 
});
 
list.js

对应搜索弹出层html模板:

<div class="c-row search-line" data-flag="start"> <div class="c-span3"> 出发</div> <div class="c-span9 js-start search-line-txt"> 请选择出发地</div> </div> <div class="c-row search-line" data-flag="arrive"> <div class="c-span3"> 到达</div> <div class="c-span9 js-arrive search-line-txt"> 请选择到达地</div> </div> <div class="c-row " data-flag="arrive"> <span class="btn-primary full-width js_search_list">查询</span> </div> tpl.search.box.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="c-row search-line" data-flag="start">
    <div class="c-span3">
        出发</div>
    <div class="c-span9 js-start search-line-txt">
        请选择出发地</div>
</div>
<div class="c-row search-line" data-flag="arrive">
    <div class="c-span3">
        到达</div>
    <div class="c-span9 js-arrive search-line-txt">
        请选择到达地</div>
</div>
<div class="c-row " data-flag="arrive">
    <span class="btn-primary full-width js_search_list">查询</span>
</div>
 
tpl.search.box.html

这里核心代码是:

JavaScript

//搜索工具弹出层 showSearchBox: function () { var scope = this; if (!this.searchBox) { this.searchBox = new UIScrollLayer({ title: '请选择搜索条件', html: searchBoxHtml, events: { 'click .js-start': function () { }, 'click .js-arrive': function () { }, 'click .js_search_list': function () { console.log('查询列表'); } } }); } this.searchBox.show(); },

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//搜索工具弹出层
showSearchBox: function () {
    var scope = this;
    if (!this.searchBox) {
        this.searchBox = new UIScrollLayer({
            title: '请选择搜索条件',
            html: searchBoxHtml,
            events: {
                'click .js-start': function () {
 
                },
                'click .js-arrive': function () {
 
                },
                'click .js_search_list': function () {
 
                    console.log('查询列表');
                }
            }
        });
    }
    this.searchBox.show();
},

于是当URL什么参数都没有的时候,就会弹出这个搜索框

葡京正网网投 43

这里也迎来了一个难点,因为城市列表事实上应该是一个独立的可访问的页面,但是这里是想用弹出层的方式调用他,所以我在APP层实现了一个方法可以用弹出层的方式调起一个独立的页面。

JavaScript

注意: 这里city城市列表未完全采用组件化的方式开发,有兴趣的朋友可以自己尝试着开发

1
2
注意:
这里city城市列表未完全采用组件化的方式开发,有兴趣的朋友可以自己尝试着开发

这里有一个不同的地方是,因为我们点击查询的时候才会做实体数据更新,这里是单纯的做DOM操作了,这里不设置数据实体一个原因就是:

这个搜索弹出层是一个页面级DOM之外的部分,数据实体变化一般只应该影响Page级别的DOM,除非真的有两个页面级View会公用一个数据实体。

JavaScript

define([ 'AbstractView', 'text!ListPath/list.css', 'ListPath/en.date', 'ListPath/mod.date', 'text!ListPath/tpl.layout.html', 'text!ListPath/tpl.search.box.html', 'UIScrollLayer' ], function ( AbstractView, style, DateEntity, DateModule, layoutHtml, searchBoxHtml, UIScrollLayer ) { return _.inherit(AbstractView, { _initEntity: function () { this.dateEntity = new DateEntity(); }, _initModule: function () { this.dateModule = new DateModule({ view: this, selector: '.js_calendar_wrapper', dateEntity: this.dateEntity }); }, propertys: function ($super) { $super(); this._initEntity(); this._initModule(); this.style = style; this.template = layoutHtml; //主控制器业务属性 this.urlData = { start: {}, end: {} }; }, initHeader: function (name) { var title = '班次列表'; this.header.set({ view: this, title: title, back: function () { console.log('回退'); }, right: [ { tagname: 'search-bar', value: '搜索', callback: function () { console.log('弹出搜索框'); this.showSearchBox(); } } ] }); }, //搜索工具弹出层 showSearchBox: function () { var scope = this; if (!this.searchBox) { this.searchBox = new UIScrollLayer({ title: '请选择搜索条件', html: searchBoxHtml, events: { 'click .js-start': function (e) { scope._showCityView('start', $(e.currentTarget)); }, 'click .js-arrive': function (e) { scope._showCityView('end', $(e.currentTarget)); }, 'click .js_search_list': function () { var param = {}; if (!scope.urlData.start.id) { scope.showToast('请先选择出发城市'); return; } if (!scope.urlData.end.id) { scope.showToast('请先选择到达城市'); return; } //这里一定会有出发城市与到达城市等数据 param.startcityid = scope.urlData.start.id; param.arrivalcityid = scope.urlData.end.id; param.startdatetime = scope.dateEntity.getDate(); param.startname = scope.urlData.start.name; param.arrivename = scope.urlData.end.name; if (scope.urlData.start.station) { param.startstationid = scope.urlData.start.station } if (scope.urlData.end.station) { param.arrivalstationid = end_station } scope.forward('list', param); this.hide(); } } }); } this.searchBox.show(); }, _showCityView: function (key, el) { var scope = this; if (key == 'end') { //因为到达车站会依赖出发车站的数据,所以这里得先做判断 if (!this.urlData.start.id) { this.showToast('请先选择出发城市'); return; } } this.showPageView('city', { flag: key, startId: this.urlData.start.id, type: this.urlData.start.type, onCityItemClick: function (id, name, station, type) { scope.urlData[key] = {}; scope.urlData[key]['id'] = id; scope.urlData[key]['type'] = type; scope.urlData[key]['name'] = name; if (station) scope.urlData[key]['name'] = station; el.text(name); scope.hidePageView(); }, onBackAction: function () { scope.hidePageView(); } }); }, addEvent: function () { this.on('onShow', function () { //初始化date数据 this.dateModule.initDate(); //这里判断是否需要弹出搜索弹出层 if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) { this.showSearchBox(); return; } }); } }); }); list.js

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
define([
    'AbstractView',
    'text!ListPath/list.css',
 
    'ListPath/en.date',
 
    'ListPath/mod.date',
 
    'text!ListPath/tpl.layout.html',
    'text!ListPath/tpl.search.box.html',
    'UIScrollLayer'
], function (
    AbstractView,
    style,
 
    DateEntity,
 
    DateModule,
 
    layoutHtml,
    searchBoxHtml,
    UIScrollLayer
) {
    return _.inherit(AbstractView, {
 
        _initEntity: function () {
            this.dateEntity = new DateEntity();
 
        },
 
        _initModule: function () {
            this.dateModule = new DateModule({
                view: this,
                selector: '.js_calendar_wrapper',
                dateEntity: this.dateEntity
            });
 
        },
 
        propertys: function ($super) {
            $super();
 
            this._initEntity();
            this._initModule();
 
            this.style = style;
            this.template = layoutHtml;
 
            //主控制器业务属性
            this.urlData = {
                start: {},
                end: {}
            };
 
        },
 
        initHeader: function (name) {
            var title = '班次列表';
            this.header.set({
                view: this,
                title: title,
                back: function () {
                    console.log('回退');
                },
                right: [
                    {
                        tagname: 'search-bar',
                        value: '搜索',
                        callback: function () {
                            console.log('弹出搜索框');
                            this.showSearchBox();
                        }
                    }
                ]
            });
        },
 
        //搜索工具弹出层
        showSearchBox: function () {
            var scope = this;
            if (!this.searchBox) {
                this.searchBox = new UIScrollLayer({
                    title: '请选择搜索条件',
                    html: searchBoxHtml,
                    events: {
                        'click .js-start': function (e) {
                            scope._showCityView('start', $(e.currentTarget));
                        },
                        'click .js-arrive': function (e) {
                            scope._showCityView('end', $(e.currentTarget));
                        },
                        'click .js_search_list': function () {
                            var param = {};
 
                            if (!scope.urlData.start.id) {
                                scope.showToast('请先选择出发城市');
                                return;
                            }
 
                            if (!scope.urlData.end.id) {
                                scope.showToast('请先选择到达城市');
                                return;
                            }
 
                            //这里一定会有出发城市与到达城市等数据
                            param.startcityid = scope.urlData.start.id;
                            param.arrivalcityid = scope.urlData.end.id;
                            param.startdatetime = scope.dateEntity.getDate();
                            param.startname = scope.urlData.start.name;
                            param.arrivename = scope.urlData.end.name;
 
                            if (scope.urlData.start.station) {
                                param.startstationid = scope.urlData.start.station
                            }
 
                            if (scope.urlData.end.station) {
                                param.arrivalstationid = end_station
                            }
 
                            scope.forward('list', param);
                            this.hide();
                        }
                    }
                });
            }
            this.searchBox.show();
        },
 
        _showCityView: function (key, el) {
            var scope = this;
 
            if (key == 'end') {
                //因为到达车站会依赖出发车站的数据,所以这里得先做判断
                if (!this.urlData.start.id) {
                    this.showToast('请先选择出发城市');
                    return;
                }
            }
 
            this.showPageView('city', {
                flag: key,
                startId: this.urlData.start.id,
                type: this.urlData.start.type,
                onCityItemClick: function (id, name, station, type) {
                    scope.urlData[key] = {};
                    scope.urlData[key]['id'] = id;
                    scope.urlData[key]['type'] = type;
                    scope.urlData[key]['name'] = name;
                    if (station) scope.urlData[key]['name'] = station;
                    el.text(name);
                    scope.hidePageView();
                },
                onBackAction: function () {
                    scope.hidePageView();
                }
            });
        },
 
        addEvent: function () {
            this.on('onShow', function () {
                //初始化date数据
                this.dateModule.initDate();
 
                //这里判断是否需要弹出搜索弹出层
                if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) {
                    this.showSearchBox();
                    return;
                }
 
            });
        }
    });
 
});
 
list.js

搜索功能完成后,我们这里便可以进入真正的数据请求功能渲染列表了。

Android webview兼容

Android webview的表现不佳,闪屏等问题比较多,遇到的几个问题有:

① 使用hybrid命令(比如跳转),如果点击快了的话,Android因为响应慢要开两个新页面,需要对连续点击做冻结

② 4.4以下低版本不能捕获js回调,意思是Android拿不到js的返回值,一些特殊的功能就做不了,比如back容错

③ ……

其余模块

在实现数据请求之前,我按照日期模块的方式将下面三个模块的功能也一并完成了,这里唯一不同的是,这些模块的DOM已经存在,我们不需要渲染了,完成后的代码大概是这样的:

JavaScript

define([ 'AbstractView', 'text!ListPath/list.css', 'ListPath/en.station', 'ListPath/en.date', 'ListPath/en.time', 'ListPath/mod.date', 'ListPath/mod.time', 'ListPath/mod.setout', 'ListPath/mod.arrive', 'text!ListPath/tpl.layout.html', 'text!ListPath/tpl.search.box.html', 'UIScrollLayer' ], function ( AbstractView, style, StationEntity, DateEntity, TimeEntity, DateModule, TimeModule, SetoutModule, ArriveModule, layoutHtml, searchBoxHtml, UIScrollLayer ) { return _.inherit(AbstractView, { _initEntity: function () { this.dateEntity = new DateEntity(); this.timeEntity = new TimeEntity(); this.timeEntity.subscribe('init', this.renderTime, this); this.timeEntity.subscribe(this.renderTime, this); this.setoutEntity = new StationEntity(); this.setoutEntity.subscribe('init', this.renderSetout, this); this.setoutEntity.subscribe(this.renderSetout, this); this.arriveEntity = new StationEntity(); this.arriveEntity.subscribe('init', this.renderArrive, this); this.arriveEntity.subscribe(this.renderArrive, this); }, _initModule: function () { this.dateModule = new DateModule({ view: this, selector: '.js_calendar_wrapper', dateEntity: this.dateEntity }); this.timeModule = new TimeModule({ view: this, selector: '.js_show_setoutdate', timeEntity: this.timeEntity }); this.setOutModule = new SetoutModule({ view: this, selector: '.js_show_setstation', setoutEntity: this.setoutEntity }); this.arriveModule = new ArriveModule({ view: this, selector: '.js_show_arrivalstation', arriveEntity: this.arriveEntity }); }, propertys: function ($super) { $super(); this._initEntity(); this._initModule(); this.style = style; this.template = layoutHtml; //主控制器业务属性 this.urlData = { start: {}, end: {} }; }, initHeader: function (name) { var title = '班次列表'; this.header.set({ view: this, title: title, back: function () { console.log('回退'); }, right: [ { tagname: 'search-bar', value: '搜索', callback: function () { console.log('弹出搜索框'); this.showSearchBox(); } } ] }); }, initElement: function () { this.d_list_wrapper = this.$('.js_list_wrapper'); this.d_none_data = this.$('.js_none_data'); this.d_js_show_setoutdate = this.$('.js_show_setoutdate'); this.d_js_show_setstation = this.$('.js_show_setstation'); this.d_js_show_arrivalstation = this.$('.js_show_arrivalstation'); this.d_js_list_loading = this.$('.js_list_loading'); this.d_js_tabs = this.$('.js_tabs'); this.d_js_day_sec = this.$('.js_day_sec'); this.d_js_start_sec = this.$('.js_start_sec'); this.d_js_arrival_sec = this.$('.js_arrival_sec'); }, //搜索工具弹出层 showSearchBox: function () { var scope = this; if (!this.searchBox) { this.searchBox = new UIScrollLayer({ title: '请选择搜索条件', html: searchBoxHtml, events: { 'click .js-start': function (e) { scope._showCityView('start', $(e.currentTarget)); }, 'click .js-arrive': function (e) { scope._showCityView('end', $(e.currentTarget)); }, 'click .js_search_list': function () { var param = {}; if (!scope.urlData.start.id) { scope.showToast('请先选择出发城市'); return; } if (!scope.urlData.end.id) { scope.showToast('请先选择到达城市'); return; } //这里一定会有出发城市与到达城市等数据 param.startcityid = scope.urlData.start.id; param.arrivalcityid = scope.urlData.end.id; param.startdatetime = scope.dateEntity.getDate(); param.startname = scope.urlData.start.name; param.arrivename = scope.urlData.end.name; if (scope.urlData.start.station) { param.startstationid = scope.urlData.start.station } if (scope.urlData.end.station) { param.arrivalstationid = end_station } scope.forward('list', param); this.hide(); } } }); } this.searchBox.show(); }, _showCityView: function (key, el) { var scope = this; if (key == 'end') { //因为到达车站会依赖出发车站的数据,所以这里得先做判断 if (!this.urlData.start.id) { this.showToast('请先选择出发城市'); return; } } this.showPageView('city', { flag: key, startId: this.urlData.start.id, type: this.urlData.start.type, onCityItemClick: function (id, name, station, type) { scope.urlData[key] = {}; scope.urlData[key]['id'] = id; scope.urlData[key]['type'] = type; scope.urlData[key]['name'] = name; if (station) scope.urlData[key]['name'] = station; el.text(name); scope.hidePageView(); }, onBackAction: function () { scope.hidePageView(); } }); }, //初始化出发车站,该数据会随着数据加载结束而变化 //如果url具有出发站名称以及id,需要特殊处理 initSetoutEntity: function () { var data = {}; if (_.getUrlParam().startstationid) { //出发车站可能并没有传,兼容老代码 data.name = _.getUrlParam().startname || '全部车站'; data.id = _.getUrlParam().startstationid; } this.setoutEntity.initData(data, data.id); }, //初始化到达站 initArriveEntity: function () { var data = {}; if (_.getUrlParam().arrivalstationid) { //出发车站可能并没有传,兼容老代码 data.name = _.getUrlParam().arrivename || '全部车站'; data.id = _.getUrlParam().arrivalstationid; } this.arriveEntity.initData(data, data.id); }, //时段只有变化时候才具有显示状态 renderTime: function () { var name = this.timeEntity.getName(); this.d_js_day_sec.html(name); }, renderSetout: function () { var name = this.setoutEntity.getName(); this.d_js_start_sec.html(name); }, renderArrive: function () { var name = this.arriveEntity.getName(); this.d_js_arrival_sec.html(name); }, addEvent: function () { this.on('onShow', function () { //初始化date数据 this.dateModule.initDate(); //这里判断是否需要弹出搜索弹出层 if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) { this.showSearchBox(); return; } //初始化时段选择 this.timeEntity.initData(); this.initSetoutEntity(); this.initArriveEntity(); }); } }); }); list.js

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
define([
    'AbstractView',
    'text!ListPath/list.css',
 
    'ListPath/en.station',
    'ListPath/en.date',
    'ListPath/en.time',
 
    'ListPath/mod.date',
    'ListPath/mod.time',
    'ListPath/mod.setout',
    'ListPath/mod.arrive',
 
    'text!ListPath/tpl.layout.html',
    'text!ListPath/tpl.search.box.html',
    'UIScrollLayer'
], function (
    AbstractView,
    style,
 
    StationEntity,
    DateEntity,
    TimeEntity,
 
    DateModule,
    TimeModule,
    SetoutModule,
    ArriveModule,
 
    layoutHtml,
    searchBoxHtml,
    UIScrollLayer
) {
    return _.inherit(AbstractView, {
 
        _initEntity: function () {
            this.dateEntity = new DateEntity();
 
            this.timeEntity = new TimeEntity();
            this.timeEntity.subscribe('init', this.renderTime, this);
            this.timeEntity.subscribe(this.renderTime, this);
 
            this.setoutEntity = new StationEntity();
            this.setoutEntity.subscribe('init', this.renderSetout, this);
            this.setoutEntity.subscribe(this.renderSetout, this);
 
            this.arriveEntity = new StationEntity();
            this.arriveEntity.subscribe('init', this.renderArrive, this);
            this.arriveEntity.subscribe(this.renderArrive, this);
 
        },
 
        _initModule: function () {
            this.dateModule = new DateModule({
                view: this,
                selector: '.js_calendar_wrapper',
                dateEntity: this.dateEntity
            });
 
            this.timeModule = new TimeModule({
                view: this,
                selector: '.js_show_setoutdate',
                timeEntity: this.timeEntity
            });
 
            this.setOutModule = new SetoutModule({
                view: this,
                selector: '.js_show_setstation',
                setoutEntity: this.setoutEntity
            });
 
            this.arriveModule = new ArriveModule({
                view: this,
                selector: '.js_show_arrivalstation',
                arriveEntity: this.arriveEntity
            });
 
        },
 
        propertys: function ($super) {
            $super();
 
            this._initEntity();
            this._initModule();
 
            this.style = style;
            this.template = layoutHtml;
 
            //主控制器业务属性
            this.urlData = {
                start: {},
                end: {}
            };
 
        },
 
        initHeader: function (name) {
            var title = '班次列表';
            this.header.set({
                view: this,
                title: title,
                back: function () {
                    console.log('回退');
                },
                right: [
                    {
                        tagname: 'search-bar',
                        value: '搜索',
                        callback: function () {
                            console.log('弹出搜索框');
                            this.showSearchBox();
                        }
                    }
                ]
            });
        },
 
        initElement: function () {
            this.d_list_wrapper = this.$('.js_list_wrapper');
            this.d_none_data = this.$('.js_none_data');
 
            this.d_js_show_setoutdate = this.$('.js_show_setoutdate');
            this.d_js_show_setstation = this.$('.js_show_setstation');
            this.d_js_show_arrivalstation = this.$('.js_show_arrivalstation');
            this.d_js_list_loading = this.$('.js_list_loading');
            this.d_js_tabs = this.$('.js_tabs');
 
            this.d_js_day_sec = this.$('.js_day_sec');
            this.d_js_start_sec = this.$('.js_start_sec');
            this.d_js_arrival_sec = this.$('.js_arrival_sec');
        },
 
        //搜索工具弹出层
        showSearchBox: function () {
            var scope = this;
            if (!this.searchBox) {
                this.searchBox = new UIScrollLayer({
                    title: '请选择搜索条件',
                    html: searchBoxHtml,
                    events: {
                        'click .js-start': function (e) {
                            scope._showCityView('start', $(e.currentTarget));
                        },
                        'click .js-arrive': function (e) {
                            scope._showCityView('end', $(e.currentTarget));
                        },
                        'click .js_search_list': function () {
                            var param = {};
 
                            if (!scope.urlData.start.id) {
                                scope.showToast('请先选择出发城市');
                                return;
                            }
 
                            if (!scope.urlData.end.id) {
                                scope.showToast('请先选择到达城市');
                                return;
                            }
 
                            //这里一定会有出发城市与到达城市等数据
                            param.startcityid = scope.urlData.start.id;
                            param.arrivalcityid = scope.urlData.end.id;
                            param.startdatetime = scope.dateEntity.getDate();
                            param.startname = scope.urlData.start.name;
                            param.arrivename = scope.urlData.end.name;
 
                            if (scope.urlData.start.station) {
                                param.startstationid = scope.urlData.start.station
                            }
 
                            if (scope.urlData.end.station) {
                                param.arrivalstationid = end_station
                            }
 
                            scope.forward('list', param);
                            this.hide();
                        }
                    }
                });
            }
            this.searchBox.show();
        },
 
        _showCityView: function (key, el) {
            var scope = this;
 
            if (key == 'end') {
                //因为到达车站会依赖出发车站的数据,所以这里得先做判断
                if (!this.urlData.start.id) {
                    this.showToast('请先选择出发城市');
                    return;
                }
            }
 
            this.showPageView('city', {
                flag: key,
                startId: this.urlData.start.id,
                type: this.urlData.start.type,
                onCityItemClick: function (id, name, station, type) {
                    scope.urlData[key] = {};
                    scope.urlData[key]['id'] = id;
                    scope.urlData[key]['type'] = type;
                    scope.urlData[key]['name'] = name;
                    if (station) scope.urlData[key]['name'] = station;
                    el.text(name);
                    scope.hidePageView();
                },
                onBackAction: function () {
                    scope.hidePageView();
                }
            });
        },
 
        //初始化出发车站,该数据会随着数据加载结束而变化
        //如果url具有出发站名称以及id,需要特殊处理
        initSetoutEntity: function () {
            var data = {};
            if (_.getUrlParam().startstationid) {
                //出发车站可能并没有传,兼容老代码
                data.name = _.getUrlParam().startname || '全部车站';
                data.id = _.getUrlParam().startstationid;
            }
 
            this.setoutEntity.initData(data, data.id);
        },
 
        //初始化到达站
        initArriveEntity: function () {
 
            var data = {};
            if (_.getUrlParam().arrivalstationid) {
                //出发车站可能并没有传,兼容老代码
                data.name = _.getUrlParam().arrivename || '全部车站';
                data.id = _.getUrlParam().arrivalstationid;
            }
 
            this.arriveEntity.initData(data, data.id);
        },
 
        //时段只有变化时候才具有显示状态
        renderTime: function () {
            var name = this.timeEntity.getName();
            this.d_js_day_sec.html(name);
        },
 
        renderSetout: function () {
            var name = this.setoutEntity.getName();
            this.d_js_start_sec.html(name);
        },
 
        renderArrive: function () {
            var name = this.arriveEntity.getName();
            this.d_js_arrival_sec.html(name);
        },
 
        addEvent: function () {
            this.on('onShow', function () {
                //初始化date数据
                this.dateModule.initDate();
 
                //这里判断是否需要弹出搜索弹出层
                if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) {
                    this.showSearchBox();
                    return;
                }
 
                //初始化时段选择
                this.timeEntity.initData();
                this.initSetoutEntity();
                this.initArriveEntity();
 
            });
        }
    });
 
});
 
list.js

这个时候整个逻辑结构大概出来了:

葡京正网网投 44

JavaScript

注意: 因为该文耗时过长,导致我现在体力有点虚脱,所以这里的代码不一定最优

1
2
注意:
因为该文耗时过长,导致我现在体力有点虚脱,所以这里的代码不一定最优

最后功能:

葡京正网网投 45

到此,demo结束了,最后形成的目录:

葡京正网网投 46

一个js便可以拆分成这么多的小组件模块,如果是更加复杂的页面,这里的文件会很多,比如订单填写页的组件模块是这里的三倍。

一些小特性

为了让H5的表现更加像native我们会约定一些小的特性,这种特性不适合通用架构,但是有了会更有亮点。

组件化的优缺点

组件化带来的几个优点十分明显:

JavaScript

① 组件化拆分,使得主控制业务逻辑清晰简单 ② 各个业务组件模块功能相对独立,可维护性可测试性大大提升 ③ 组件之间可以任意组合,有一定可重用性 ④ 增删模块不会怕打断骨头连着筋 ⑤ 一个业务模块所需代码全部在一个目录,比较好操作(有点凑数嫌疑)

1
2
3
4
5
① 组件化拆分,使得主控制业务逻辑清晰简单
② 各个业务组件模块功能相对独立,可维护性可测试性大大提升
③ 组件之间可以任意组合,有一定可重用性
④ 增删模块不会怕打断骨头连着筋
⑤ 一个业务模块所需代码全部在一个目录,比较好操作(有点凑数嫌疑)

回退更新

我们在hybrid中的跳转,事实上每次都是新开一个webview,当A->B的时候,事实上A只是被隐藏了,当B点击返回的时候,便直接将A展示了出来,而A不会做任何更新,对前端来说是无感知的。

事实上,这个是一种优化,为了解决这种问题我们做了一个下拉刷新的特性:

JavaScript

_.requestHybrid({ tagname: 'headerrefresh', param: { //下拉时候展示的文案 title: '123' }, //下拉后执行的回调,强暴点就全部刷新 callback: function(data) { location.reload(); } });

1
2
3
4
5
6
7
8
9
10
11
_.requestHybrid({
    tagname: 'headerrefresh',
    param: {
         //下拉时候展示的文案
         title: '123'
     },
     //下拉后执行的回调,强暴点就全部刷新
     callback: function(data) {
         location.reload();
     }
});

但,这个总没有自动刷新来的舒服,于是我们在页面第一次加载的时候约定了这些事件:

JavaScript

// 注册页面加载事件 _.requestHybrid({ tagname: 'onwebviewshow', callback: function () { } }); // 注册页面影藏事件 _.requestHybrid({ tagname: 'onwebviewhide', callback: function () { scope.loopFlag = false; clearTimeout(scope.t); } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注册页面加载事件
  _.requestHybrid({
      tagname: 'onwebviewshow',
      callback: function () {
        
      }
  });
// 注册页面影藏事件
_.requestHybrid({
     tagname: 'onwebviewhide',
     callback: function () {
         scope.loopFlag = false;
         clearTimeout(scope.t);
     }
});

在webview展示的时候触发,和在webview隐藏的时候触发,这样用户便可以做自动数据刷新了,但是局部刷新要做到什么程度就要看开发的时间安排了,技术好时间多自然体验好。

缺点

事实上,组件化不会带来什么不足,对于不了解的朋友可能会认为代码复杂度有所增加,其实不这样做代码才真正叫一个难呢!

真正的美中不足的要挑一个毛病的话,这种分拆可能会比单个文件代码量稍大

header-搜索

根据我们之前的约定,header是比较中规中矩的,但是由于产品和视觉强迫,我们实现了一个不一样的header,最开始虽然不太乐意,做完了后感觉还行……

葡京正网网投 47

这块工作量主要是native的,我们只需要约定即可:

JavaScript

this.header.set({ view: this, //左边按钮 left: [], //右边按钮 right: [{ tagname: 'cancel', value: '取消', callback: function () { this.back(); } }], //searchbox定制 title: { //特殊tagname tagname: 'searchbox', //标题,该数据为默认文本框文字 title: '取消', //没有文字时候的占位提示 placeholder: '搜索医院、科室、医生和病症', //是否默认进入页面获取焦点 focus: true, //文本框相关具有的回调事件 //data为一个json串 //editingdidbegin 为点击或者文本框获取焦点时候触发的事件 //editingdidend 为文本框失去焦点触发的事件 //editingchanged 为文本框数据改变时候触发的事件 type: '', data: '' //真实数据 }, callback: function(data) { var _data = JSON.parse(data); if (_data.type == 'editingdidend' && this.keyword != $.trim(_data.data)) { this.keyword = $.trim(_data.data); this.reloadList(); } } });

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
this.header.set({
    view: this,
     //左边按钮
     left: [],
    //右边按钮
     right: [{
         tagname: 'cancel',
        value: '取消',
         callback: function () {
            this.back();
        }
    }],
    //searchbox定制
     title: {
         //特殊tagname
         tagname: 'searchbox',
        //标题,该数据为默认文本框文字
         title: '取消',
         //没有文字时候的占位提示
        placeholder: '搜索医院、科室、医生和病症',
         //是否默认进入页面获取焦点
        focus: true,
         //文本框相关具有的回调事件
         //data为一个json串
         //editingdidbegin 为点击或者文本框获取焦点时候触发的事件
        //editingdidend 为文本框失去焦点触发的事件
         //editingchanged 为文本框数据改变时候触发的事件
         type: '',
        data: '' //真实数据
     },
     callback: function(data) {
         var _data = JSON.parse(data);
         if (_data.type == 'editingdidend' && this.keyword != $.trim(_data.data)) {
             this.keyword = $.trim(_data.data);
            this.reloadList();
         }
     }
});

从性能优化角度看组件化

无论什么前端优化,最后的瓶颈一定是在请求量上做文章:压缩、缓存、仅仅做首屏渲染、将jQuery缓存zepto……

说都会说,但是很多场景由不得你那样做,项目足够复杂,而UI又提供给了不同团队使用的话,有一天前端做了一次UI优化,而如何将这次UI优化反应到线上才是考验架构设计的时候,如果是不好的设计的话,想将这次优化推上线,会发生两个事情:

① 业务团队大改代码

② 框架资源(js&css)膨胀

这种头疼的问题是一般人做优化考虑不到的,而业务团队不会因为你的更新而去修改代码,所以一般会以代码膨胀为代价将这次优化强推上线,那往往会让情况更加复杂:

新老代码融合,半年后你根本不知道哪些代码可以删,哪些代码可以留,很大时候这个问题会体现在具有公共特性的CSS中 如果你的CSS同时服务于多个团队,而各个团队的框架版本不一致,那么UI升级对你来说可能是一个噩梦! 如果你想做第三轮的UI升级,那还是算了吧……

事实上,我评价一个前端是否足够厉害,往往就会从这里考虑:

当一个项目足够复杂后,你私下做好了优化,但是你的优化代码不能无缝的让业务团队使用,而需要业务团队做很多改变,你如何解决这种问题

很多前端做一个优化,便是重新做了一个东西,刚开始肯定比线上的好,但半年后,那个代码质量还未必有以前的好呢,所以我们这里应该解决的是:

如何设计一个机制,让业务团队以最小的修改,而可以用上新的UI(样式、特性),而不会增加CSS(JS)体积 这个可能是组件化真正要解决的事情!

理想情况下,一个H5的资源组成情况是这样的:

① 公共核心CSS文件(200行左右)

② 框架核心文件(包含框架核心和第三方库)

③ UI组件(有很多独立的UI组件组成,每个UI组件又包含完整的HTML&CSS)

④ 公共业务模块(提供业务级别公共服务,比如登录、城市列表等业务相关功能)

⑤ 业务频道一个页面,也就是我们这里的list页的代码

因为框架核心一般来说是不经常改变的,就算改变也是对表现层透明的,UI采用增量与预加载机制,这样做会对后续样式升级,UI升级有莫大的好处,而业务组件化后本身要做什么滚动加载也是轻而易举

好的前端架构设计应该满足不停的UI升级需求,而不增加业务团队下载量

结语

希望此文能对准备接触Hybrid技术的朋友提供一些帮助,关于Hybrid的系列这里是最后一篇实战类文章介绍,这里是demo期间的一些效果图,后续git库的代码会再做整理:

葡京正网网投 48

葡京正网网投 49

葡京正网网投 50

结语

本文就如何分解复杂的前端页面提出了一些自己的想法,并且给予了实现,希望对各位有所帮助。

落地项目

真实落地的业务为医联通,有兴趣的朋友试试:

葡京正网网投 51

葡京正网网投 52

关于合并

前端代码有分拆就有合并,因为最终一个完整的页面需要所有资源才能运行,但考虑到此文已经很长了,关于合并一块的工作留待下文分析吧

推动感悟

从项目调研到项目落地再到最近一些的优化,已经花了三个月时间了,要做好一件事是不容易的,而且我们这个还涉及到持续优化,和配套业务比如:

① passport

② 钱包业务

③ 反馈业务

…..

等同步制作,很多工作的意义,或者作用,是非技术同事看不到的,但是如果我们不坚持做下去,迫于业务压力或者自我松懈放纵,那么就什么也没有了,我们要推动一件事情,不可能一站出来就说,嘿,小样,我们这个不错,你拿去用吧,这样人家会猜疑你的,我们一定是要先做一定demo让人有一定初步印象,再强制或者偷偷再某一个生产业务试用,一方面将技术依赖弄进去,一方面要告诉其他同事,看看嘛,也没有引起多大问题嘛,呵呵。

做事难,推动难,难在坚持,难在携手共进,这里面是需要信念的,在此尤其感谢团队3个伙伴的无私付出(杨杨、文文、文文)。

后续,我们在持续推动hybrid建设的同时,会尝试React Native,找寻更好的更适合自己的解决方案。

1 赞 收藏 评论

葡京正网网投 53

关于代码

为了方便各位理解组件化开发的思想,我这里写了一个完整的demo帮助各位分析,由于精力有限,代码难免会有BUG,各位多多包涵:

可能会浏览的代码:

2 赞 9 收藏 1 评论

葡京正网网投 54

本文由葡京网投哪个正规发布于新葡亰-前端,转载请注明出处:【葡京网投哪个正规】浅谈Hybrid技术的设计与实现第三弹,前端进阶篇之如何编写可维护可升级的代码

关键词:

上一篇:浅谈Hybrid技术的设计与实现第三弹,浅谈Hybrid技术的设计与实现

下一篇:没有了