浅谈Hybrid技术的设计与实现(转)
2015-11-04 12:27
344 查看
前言
随着移动浪潮的兴起,各种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的业务进度:
1 define([], function () { 2 'use strict'; 3 4 return _.inherit({ 5 6 propertys: function () { 7 8 this.left = []; 9 this.right = []; 10 this.title = {}; 11 this.view = null; 12 13 this.hybridEventFlag = 'Header_Event'; 14 15 }, 16 17 //全部更新 18 set: function (opts) { 19 if (!opts) return; 20 21 var left = []; 22 var right = []; 23 var title = {}; 24 var tmp = {}; 25 26 //语法糖适配 27 if (opts.back) { 28 tmp = { tagname: 'back' }; 29 if (typeof opts.back == 'string') tmp.value = opts.back; 30 else if (typeof opts.back == 'function') tmp.callback = opts.back; 31 else if (typeof opts.back == 'object') _.extend(tmp, opts.back); 32 left.push(tmp); 33 } else { 34 if (opts.left) left = opts.left; 35 } 36 37 //右边按钮必须保持数据一致性 38 if (typeof opts.right == 'object' && opts.right.length) right = opts.right 39 40 if (typeof opts.title == 'string') { 41 title.title = opts.title; 42 } else if (_.isArray(opts.title) && opts.title.length > 1) { 43 title.title = opts.title[0]; 44 title.subtitle = opts.title[1]; 45 } else if (typeof opts.title == 'object') { 46 _.extend(title, opts.title); 47 } 48 49 this.left = left; 50 this.right = right; 51 this.title = title; 52 this.view = opts.view; 53 54 this.registerEvents(); 55 56 _.requestHybrid({ 57 tagname: 'updateheader', 58 param: { 59 left: this.left, 60 right: this.right, 61 title: this.title 62 } 63 }); 64 65 }, 66 67 //注册事件,将事件存于本地 68 registerEvents: function () { 69 _.unRegisterHybridCallback(this.hybridEventFlag); 70 this._addEvent(this.left); 71 this._addEvent(this.right); 72 this._addEvent(this.title); 73 }, 74 75 _addEvent: function (data) { 76 if (!_.isArray(data)) data = [data]; 77 var i, len, tmp, fn, tagname; 78 var t = 'header_' + (new Date().getTime()); 79 80 for (i = 0, len = data.length; i < len; i++) { 81 tmp = data[i]; 82 tagname = tmp.tagname || ''; 83 if (tmp.callback) { 84 fn = $.proxy(tmp.callback, this.view); 85 tmp.callback = t; 86 _.registerHeaderCallback(this.hybridEventFlag, t + '_' + tagname, fn); 87 } 88 } 89 }, 90 91 //显示header 92 show: function () { 93 _.requestHybrid({ 94 tagname: 'showheader' 95 }); 96 }, 97 98 //隐藏header 99 hide: function () { 100 _.requestHybrid({ 101 tagname: 'hideheader', 102 param: { 103 animate: true 104 } 105 }); 106 }, 107 108 //只更新title,不重置事件,不对header其它地方造成变化,仅仅最简单的header能如此操作 109 update: function (title) { 110 _.requestHybrid({ 111 tagname: 'updateheadertitle', 112 param: { 113 title: 'aaaaa' 114 } 115 }); 116 }, 117 118 initialize: function () { 119 this.propertys(); 120 } 121 }); 122 123 });
请求类
虽然get类请求可以用jsonp的方式绕过跨域问题,但是post请求却是真正的拦路虎,为了安全性服务器设置cors会仅仅针对几个域名,Hybrid内嵌静态资源是通过file的方式读取,这种场景使用cors就不好使了,所以每个请求需要经过Native做一层代理发出去。![](http://images2015.cnblogs.com/blog/294743/201511/294743-20151101171145060-1918906295.jpg)
这个使用场景与Header组件一致,前端框架层必须做到对业务透明化,业务事实上不必关心这个请求是由浏览器发出还是由Native发出:
1 HybridGet = function (url, param, callback) { 2 }; 3 HybridPost = function (url, param, callback) { 4 };
真实的业务场景,会将之封装到数据请求模块,在底层做适配,在H5站点下使用ajax请求,在Native内嵌时使用代理发出,与Native的约定为:
1 requestHybrid({ 2 tagname: 'ajax', 3 param: { 4 url: 'hotel/detail', 5 param: {}, 6 //默认为get 7 type: 'post' 8 }, 9 //响应后的回调 10 callback: function (data) { } 11 });
常用NativeUI组件
最后,Native会提供几个常用的Native级别的UI,比如loading加载层,比如toast消息框:1 var HybridUI = {}; 2 HybridUI.showLoading(); 3 //=> 4 requestHybrid({ 5 tagname: 'showLoading' 6 }); 7 8 HybridUI.showToast({ 9 title: '111', 10 //几秒后自动关闭提示框,-1需要点击才会关闭 11 hidesec: 3, 12 //弹出层关闭时的回调 13 callback: function () { } 14 }); 15 //=> 16 requestHybrid({ 17 tagname: 'showToast', 18 param: { 19 title: '111', 20 hidesec: 3, 21 callback: function () { } 22 } 23 });
Native UI与前端UI不容易打通,所以在真实业务开发过程中,一般只会使用几个关键的Native UI。
账号系统的设计
根据上面的设计,我们约定在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请求接口,如果没有登录则在底层唤起登录框(需要前端框架支持)
简单的登录登出接口实现:
1 /* 2 无论成功与否皆会关闭登录框 3 参数包括: 4 success 登录成功的回调 5 error 登录失败的回调 6 url 如果没有设置success,或者success执行后没有返回true,则默认跳往此url 7 */ 8 HybridUI.Login = function (opts) { 9 }; 10 //=> 11 requestHybrid({ 12 tagname: 'login', 13 param: { 14 success: function () { }, 15 error: function () { }, 16 url: '...' 17 } 18 }); 19 //与登录接口一致,参数一致 20 HybridUI.logout = function () { 21 };
账号信息获取
在实际的业务开发中,判断用户是否登录、获取用户基本信息的需求比比皆是,所以这里必须保证Hybrid开发模式与H5开发模式保持统一,否则需要在业务代码中做很多无谓的判断,我们在前端框架会封装一个User模块,主要接口包括:
1 var User = {}; 2 User.isLogin = function () { }; 3 User.getInfo = function () { };
这个代码的底层实现分为前端实现,Native实现,首先是前端的做法是:
当前端页面载入后,会做一次异步请求,请求用户相关数据,如果是登录状态便能获取数据存于localstorage中,这里一定不能存取敏感信息
前端使用localstorage的话需要考虑极端情况下使用内存变量的方式替换localstorage的实现,否则会出现不可使用的情况,而后续的访问皆是使用localstorage中的数据做判断依据,以下情况需要清理localstorage的账号数据:
① 系统登出
② 访问接口提示需要登录
③ 调用登录接口
这种模式多用于单页应用,非单页应用一般会在每次刷新页面先清空账号信息再异步拉取,但是如果当前页面马上就需要判断用户登录数据的话,便不可靠了;处于Hybrid容器中时,因为Native本身就保存了用户信息,封装的接口直接由Native获取即可,这块比较靠谱。
Hybrid的资源
目录结构
Hybrid技术既然是将静态资源存于Native,那么就需要目录设计,经过之前的经验,目录结构一般以2层目录划分:![](http://images2015.cnblogs.com/blog/294743/201511/294743-20151101180515607-593893838.jpg)
如果我们有两个频道酒店与机票,那么目录结构是这样的:
1 webapp //根目录 2 ├─flight 3 ├─hotel //酒店频道 4 │ │ index.html //业务入口html资源,如果不是单页应用会有多个入口 5 │ │ main.js //业务所有js资源打包 6 │ │ 7 │ └─static //静态样式资源 8 │ ├─css 9 │ ├─hybrid //存储业务定制化类Native Header图标 10 │ └─images 11 ├─libs 12 │ libs.js //框架所有js资源打包 13 │ 14 └─static 15 ├─css 16 └─images
最初设计的forward跳转中的topage参数规则是:频道/具体页面=>channel/page,其余资源会由index.html这个入口文件带出。
增量机制
真实的增量机制需要服务器端的配合,我这里只能简单描述,Native端会有维护一个版本映射表:{ 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 |
结语
github上代码会持续更新,现在界面反正不太好看,大家多多包涵吧,这里是一些效果图:![](http://images2015.cnblogs.com/blog/294743/201511/294743-20151102215014711-1524394368.jpg)
![](http://images2015.cnblogs.com/blog/294743/201511/294743-20151102215019305-1241290616.jpg)
![](http://images2015.cnblogs.com/blog/294743/201511/294743-20151102215023633-618187909.jpg)
Hybrid方案是快速迭代项目,快速占领市场的神器,希望此文能对准备接触Hybrid技术的朋友提供一些帮助,并且再次感谢明月同学的配合。
http://www.cnblogs.com/yexiaochai/p/4921635.html
相关文章推荐
- 1006. Sign In and Sign Out (25)
- 【转】STM32定时器输出比较模式中的疑惑
- C中printf 参数汇总
- [OpenJudge-NOI]乘积最大的拆分
- js返回上一页并刷新代码整理
- 1005. Spell It Right (20)
- [OpenJudge-NOI]不定方程求解 扩展欧几里德
- 这个世界上所有的人,并不是个个都有过你拥有的那些优越条件(转)
- 给Java开发者的Scala教程
- 1005. Spell It Right (20)
- MySQL常用命令
- [OpenJudge-NOI]余数相同问题 暴力
- demux的基础知识
- 英语能力
- MySQL命令行导出数据库
- jsp编程中session的用法实例分析
- Unity读取Excel数据
- Unity 3d性能优化技巧
- 给Java开发者的Scala教程
- PLSQL-Developer数据库连接工具使用方法