淺談Hybrid技術的設計與實現
前言
隨著移動浪潮的興起,各種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的業務進度:
突然有一天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技術的話,我這里再做一個總結吧:
Hybrid開發效率高、跨平臺、底層本
Hybrid從業務開發上講,沒有版本問題,有BUG能及時修復
Hybrid是有缺點的,Hybrid體驗就肯定比不上Native,所以使用有其場景,但是對于需要快速試錯、快速占領市場的團隊來說,Hybrid一定是不二的選擇,團隊生存下來后還是需要做體驗更好的原生APP。
好了,上面扯了那么多沒用的東西,今天的目的其實是為大家介紹Hybrid的一些設計知識,如果你認真閱讀此文,可能在以下方面對你有所幫助:
① Hybrid中Native與前端各自的工作是什么
② Hybrid的交互接口如何設計
③ Hybrid的Header如何設計
④ Hybrid的如何設計目錄結構以及增量機制如何實現
⑤ 資源緩存策略,白屏問題......
文中是我個人的一些開發經驗,希望對各位有用,也希望各位多多支持討論,指出文中不足以及提出您的一些建議。
然后文中Andriod相關代碼由我的同事明月提供,這里特別感謝明月同學對我的支持,這里掃描二維碼可以下載APP進行測試:
Andriod APP二維碼:
代碼地址:
https://github.com/yexiaochai/hybrid
#p#
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提供的各種能力,整體架構是這樣的:
真實業務開發時,Native除了會關注登錄模塊之外還會封裝支付等重要模塊,這里視業務而定。
Hybrid交互設計
Hybrid的交互無非是Native調用前端頁面的JS方法,或者前端頁面通過JS調用Native提供的接口,兩者交互的橋梁皆Webview:
app自身可以自定義url schema,并且把自定義的url注冊在調度中心, 例如
-
ctrip://wireless 打開攜程App
-
weixin:// 打開微信
我們JS與Native通信一般就是創建這類URL被Native捕獲處理,后續也出現了其它前端調用Native的方式,但可以做底層封裝使其透明化,所以重點以及是如何進行前端與Native的交互設計。
#p#
JS to Native
Native在每個版本會提供一些API,前端會有一個對應的框架團隊對其進行封裝,釋放業務接口。比如糯米對外的接口是這樣的:
- 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】
- 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的能力,我揣測糯米這種設計是因為這樣便于第三方團隊的接入使用,手機百度有一款輕應用框架也走的這種路線:
clouda.mbaas.account //釋放了clouda全局變量
這樣做有一個前提是,Native本身已經十分穩定了,很少新增功能了,否則在直連情況下就會面臨一個尷尬,因為web站點永遠保持最新的,就會在一些低版本容器中調用了沒有提供的Native能力而報錯。
API式交互
手白、糯米底層如何做我們無從得知,但我們發現調用Native API接口的方式和我們使用AJAX調用服務器端提供的接口是及其相似的:
這里類似的微薄開放平臺的接口是這樣定義的:
粉絲服務(新手接入指南) |
||
---|---|---|
讀取接口 |
接收用戶私信、關注、取消關注、@等消息接口 |
|
寫入接口 |
向用戶回復私信消息接口 |
|
生成帶參數的二維碼接口 |
我們要做的就是通過一種方式創建ajax請求即可:
https://api.weibo.com/2/statuses/public_timeline.json
所以我在實際設計Hybrid交互模型時,是以接口為單位進行設計的,比如獲取通訊錄的總體交互是:
格式約定
交互的第一步是設計數據格式,這里分為請求數據格式與響應數據格式,參考ajax的請求模型大概是:
$.ajax(options) ⇒ XMLHttpRequest
type (默認值:"GET") HTTP的請求方法(“GET”, “POST”, or other)。
url (默認值:當前url) 請求的url地址。
data (默認值:none) 請求中包含的數據,對于GET請求來說,這是包含查詢字符串的url地址,如果是包含的是object的話,$.param會將其轉化成string。
所以我這邊與Native約定的請求模型是:
- 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)調用之
數據返回的格式約定是:
{
data: {},
errno: 0,
msg: "success"
}
真實的數據在data對象中,如果errno不為0的話,便需要提示msg,這里舉個例子如果錯誤碼1代表該接口需要升級app才能使用的話:
{
data: {},
errno: 1,
msg: "APP版本過低,請升級APP版本"
}
代碼實現
這里給一個簡單的代碼實現,真實代碼在APP中會有所變化:
- 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;
- };
因為Native對于H5來是底層,框架&底層一般來說是不會關注業務實現的,所以真實業務中Native調用H5場景較少,這里不予關注了。
#p#
常用交互API
良好的交互設計是成功的第一步,在真實業務開發中有一些API一定會用到。
跳轉
跳轉是Hybrid必用API之一,對前端來說有以下跳轉:
① 頁面內跳轉,與Hybrid無關
② H5跳轉Native界面
③ H5新開Webview跳轉H5頁面,一般為做頁面動畫切換
如果要使用動畫,按業務來說有向前與向后兩種,forward&back,所以約定如下,首先是H5跳Native某一個頁面
- //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
- }
- });
比如攜程H5頁面要去到酒店Native某一個頁面可以這樣:
- //=>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
- }
- });
比如H5新開Webview的方式跳轉H5頁面便可以這樣:
- requestHybrid({
- tagname: 'forward',
- param: {
- //要去到的頁面,首先找到hotel頻道,然后定位到detail模塊
- topage: 'hotel/detail ',
- //跳轉方式,H5新開Webview跳轉,最后裝載H5頁面
- type: 'webview',
- //其它參數
- id: 20151031
- }
- });
back與forward一致,我們甚至會有animattype參數決定切換頁面時的動畫效果,真實使用時可能會封裝全局方法略去tagname的細節,這時就和糯米對外釋放的接口差不多了。
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是不允許重復的):
- //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左邊一般來說只有一個按鈕,所以其對象可以使用這種形式:
- this.header.set({
- back: function () { },
- title: ''
- });
- //語法糖=>
- this.header.set({
- left: [{
- tagname: 'back',
- callback: function(){}
- }],
- title: '',
- });
為完成Native端的實現,這里會新增兩個接口,向Native注冊事件,以及注銷事件:
- 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組件的實現:
- 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組件的封裝
#p#
請求類
雖然get類請求可以用jsonp的方式繞過跨域問題,但是post請求卻是真正的攔路虎,為了安全性服務器設置cors會僅僅針對幾個域 名,Hybrid內嵌靜態資源是通過file的方式讀取,這種場景使用cors就不好使了,所以每個請求需要經過Native做一層代理發出去。
這個使用場景與Header組件一致,前端框架層必須做到對業務透明化,業務事實上不必關心這個請求是由瀏覽器發出還是由Native發出:
1 HybridGet = function (url, param, callback) {
2 };
3 HybridPost = function (url, param, callback) {
4 };
真實的業務場景,會將之封裝到數據請求模塊,在底層做適配,在H5站點下使用ajax請求,在Native內嵌時使用代理發出,與Native的約定為:
- requestHybrid({
- tagname: 'ajax',
- param: {
- url: 'hotel/detail',
- param: {},
- //默認為get
- type: 'post'
- },
- //響應后的回調
- callback: function (data) { }
- });
常用NativeUI組件
最后,Native會提供幾個常用的Native級別的UI,比如loading加載層,比如toast消息框:
- 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。
賬號系統的設計
根據上面的設計,我們約定在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請求接口,如果沒有登錄則在底層喚起登錄框(需要前端框架支持)
簡單的登錄登出接口實現:
- /*
- 無論成功與否皆會關閉登錄框
- 參數包括:
- 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模塊,主要接口包括:
1 var User = {};
2 User.isLogin = function () { };
3 User.getInfo = function () { };
這個代碼的底層實現分為前端實現,Native實現,首先是前端的做法是:
當前端頁面載入后,會做一次異步請求,請求用戶相關數據,如果是登錄狀態便能獲取數據存于localstorage中,這里一定不能存取敏感信息
前端使用localstorage的話需要考慮極端情況下使用內存變量的方式替換localstorage的實現,否則會出現不可使用的情況,而后續的訪問皆是使用localstorage中的數據做判斷依據,以下情況需要清理localstorage的賬號數據:
① 系統登出
② 訪問接口提示需要登錄
③ 調用登錄接口
這種模式多用于單頁應用,非單頁應用一般會在每次刷新頁面先清空賬號信息再異步拉取,但是如果當前頁面馬上就需要判斷用戶登錄數據的話,便不可靠了;處于Hybrid容器中時,因為Native本身就保存了用戶信息,封裝的接口直接由Native獲取即可,這塊比較靠譜。
#p#
Hybrid的資源
目錄結構
Hybrid技術既然是將靜態資源存于Native,那么就需要目錄設計,經過之前的經驗,目錄結構一般以2層目錄劃分:
如果我們有兩個頻道酒店與機票,那么目錄結構是這樣的:
- 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這個入口文件帶出。
增量機制
真實的增量機制需要服務器端的配合,我這里只能簡單描述,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 |
當APP啟動時,APP會讀取版本信息,這里發現hotel的本地版本號比線上的小,便會下載md5對應的zip文件,然后解壓之并且替換整個 hotel文件,本次增量結束,因為所有的版本文件不會重復,APP回滾時可用回到任意想去的版本,也可以對任意版本做BUG修復。
結語
github上代碼會持續更新,現在界面反正不太好看,大家多多包涵吧,這里是一些效果圖:
Hybrid方案是快速迭代項目,快速占領市場的神器,希望此文能對準備接觸Hybrid技術的朋友提供一些幫助,并且再次感謝明月同學的配合。