前戲
自己從事iOS開發(fā)多年,單在電商領(lǐng)域瞭郑,從GMall全球購再到河貍家app辜御,已經(jīng)開發(fā)了多款電商app,也想將自己的一些經(jīng)驗分享給大家屈张,希望能夠幫助到正在面臨困難的團隊及同行業(yè)的技術(shù)朋友們我抠。
一 ?如何架構(gòu)一款hybrid app
以下是做hybrid相關(guān)工作的總結(jié):
為什么要hybrid?
首先要看場景:假如是客戶端更新實時性要求高的袜茧,必然要考慮前端工作好一點,這是基礎(chǔ)的要求瓣窄,否則就將計算盡量集中到云端笛厦,譬如一些游戲3D、GPU運算要求高的俺夕;
并且電商平臺都存在很多運營活動裳凸,那么h5頁面與native頁面的交互則十分頻繁且緊密,需要更新的頻率很快劝贸。
那么從普遍的技術(shù)選擇角度:有react-native或者cordova姨谷、以及基于webview的H5+native等
此時自己造個輪子呢,還是利用成熟的開源進行組合映九,這就要團隊技術(shù)leader根據(jù)團隊現(xiàn)狀的不同去選擇不同的技術(shù)架構(gòu)梦湘;
之所以需要hybrid還得看它的優(yōu)勢:
而它有什么優(yōu)勢呢?
1 從開發(fā)效率角度:開發(fā)快,
2.從是否利于版本更新頻繁角度:更新版本快
3.從人力成本角度: 解決開發(fā)人力成本
4. 從可移植性角度:跨平臺, 方便遷移or內(nèi)嵌到其他項目
5.對業(yè)務(wù)幫助角度:對營銷策略有利件甥,便于維護
那么如何架構(gòu)一款Hybrid的app捌议?
1 JSBridge (runtime機制, JavaScriptCore(iOS,但與安卓不統(tǒng)一))
2 Native與H5 API ( Header組件, 其他native組件, 路由, Device API, 網(wǎng)絡(luò)請求)
3 調(diào)試
4 資源 ( 打包, 緩存, 增量更新, 資源訪問機制)
接下來我們聊一聊如何建立交互協(xié)議引有;
二 ?協(xié)議方案與問題:
首先不同語言之間如何建立良好的通訊協(xié)議是個問題瓣颅,那么iOS native實現(xiàn)與js交互有幾種方案呢?
1.通過UIWebview的webView:shouldStartLoadWithRequest:navigationType:截取請求的URL譬正,再通過特殊字符串宫补,重定向;
這種方式的不好維護曾我,蛋疼啊粉怕,字符串無限疊加啊抒巢;且不直觀;(這也是河貍家最早的交互設(shè)計斋荞,帶來很多坑點)
2.XMLHttpRequest bridge
JS 端使用 XMLHttpRequest 發(fā)起了一個請求:execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true); ,請求的地址是 /!gap_exec虐秦;
并把請求的數(shù)據(jù)放在了請求的 header 里面平酿,見這句代碼:execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());凤优。
而在 Objective-C 端使用一個 NSURLProtocol 的子類來檢查每個請求,如果地址是/!gap_exec的話蜈彼,則認為是 Cordova 通信的請求筑辨,直接攔截,攔截后就可以通過分析請求的數(shù)據(jù)幸逆,分發(fā)到不同的插件類棍辕;
3.可以通過JavaScriptCore框架的JSContext注入方法來實現(xiàn);
但這里還需要結(jié)合業(yè)務(wù)一起去看还绘,一般平臺都有iOS與安卓兩端楚昭,如何能夠讓iOS與安卓統(tǒng)一一套協(xié)議,這樣極力減少開發(fā)與溝通成本拍顷,這成架構(gòu)設(shè)計中的一個問題抚太。
那么我們來看看安卓非常好用的一種方式,從而誕生了第三種方案:
建立一個jsbrige昔案,js中注入native對象與方法尿贫,js通過window直接操作native對象、及方法踏揣;
沿著這個思路庆亡,iOS是否能實現(xiàn)呢?一個好的設(shè)想捞稿!
很棒的是,果然是有辦法的娱局,但是需要做一些改造BЦ!仁卷!
? 需要制定一套oc與js雙方協(xié)議,校驗協(xié)議丰介,讓oc與js能夠識別對象/方法/參數(shù)带膀;
? oc與js通信使用webViewstringByEvaluatingJavaScriptFromString:js,將oc的對象注入給js橙垢,并暴露方法/參數(shù)給js垛叨,注入oc對象;
? iframe bridge
? 在JS端創(chuàng)建一個透明的iframe柜某,設(shè)置這個 ifame 的 src 為自定義的協(xié)議嗽元,而 ifame 的 src 更改時,UIWebView會先回調(diào)其 delegate 的webView:shouldStartLoadWithRequest:navigationType:將js信息拋給oc喂击,是一個json串(包含注入對象剂癌,方法,參數(shù))翰绊;
? 符合協(xié)議的通信信息拋給oc的解析器去做事件分發(fā)佩谷;
通過調(diào)研與尋找,發(fā)現(xiàn)了EasyJSWebView類庫辞做,確實是個好東西,接下來就對這個類庫以河貍家app工程為例做下核心源碼分析寡具;(請結(jié)合demo代碼)
三 EasyJSWebView核心類介紹:
首先初始化EasyJSWebView秤茅,并設(shè)置EasyJSWebViewProxyDelegate為delegate,核心環(huán)節(jié)都在delegate的回調(diào)里童叠;
EasyJSWebViewProxyDelegate的關(guān)鍵回調(diào)如何實現(xiàn)呢框喳?
核心代碼:
oc跟js通信由于采用的stringByEvaluatingJavaScriptFromString方法可以將javascript嵌入html頁面中;
而stringByEvaluatingJavaScriptFromString需要在js加載完成以后才可調(diào)用厦坛,
則需要在webViewDidFinishLoad: 回調(diào)中調(diào)用注入方法injectJavascript()五垮;
(注意此處:原demo有個bug,injectJavascript()是放在webViewDidStartLoad:(UIWebView *)webView方法中執(zhí)行的杜秸,會造成h5內(nèi)部跳轉(zhuǎn)新頁面后就注入無效了放仗;河貍家app做了下改造,將它放在webViewDidFinishLoad回調(diào)中執(zhí)行的撬碟;)
(后續(xù)會以demo形式開源诞挨,但這里會造成js注入延遲,如果html想要在一開始加載就要對native做一些事呢蛤,這里就會出現(xiàn)問題惶傻,也是這個設(shè)計的缺點之一);
四 如何實現(xiàn)注入
那到底注入過程是怎么執(zhí)行的呢其障?
在上圖中的96-121行代碼利用到了runtime運行時機制:
這部分做的事情就是將對象银室,方法組成的數(shù)組拼接字符串,“\”是html中的轉(zhuǎn)義需要,最后結(jié)果是這樣的一個結(jié)構(gòu) EasyJS.inject(\對象蜈敢,[方法數(shù)組])
河貍家為例子就是EasyJS.inject("HLJJavaScript", ["jsShowSomething:", "loginBlock", "resetPasswordBlock", ?"setShare:"]);
但是要看Objective-C 跟 JS 通信辜荠,會先調(diào)用,stringByEvaluatingJavaScriptFromString執(zhí)行js扶认;
對INJECT_JS 這段js進行深入源碼解析
1.EasyJS對象的內(nèi)容
當js執(zhí)行這段js侨拦,會在window創(chuàng)建 EasyJS對象,EasyJS對象有call ?/inject /invokeCallback 三個方法 ?辐宾,一個__callbacks對象狱从;
我們從下往上看;
解析inject方法:
window[obj] = {}; 左邊的意思是window中增加一個obj key叠纹,增加申明一個obj native對象季研;
var jsObj = window[obj]; 將它賦給jsObj對象
for (var i = 0, l = methods.length; i < l; i++){
//methods是個我們的方法數(shù)組
注意for循環(huán)中做的事情:
首先簡化下結(jié)構(gòu) function{}(),function{}表示這是個方法誉察,而加上()表示執(zhí)行此方法与涡,如果不加()就是單單申明方法但不執(zhí)行;
也就是說這個for循環(huán)幾次就會調(diào)用執(zhí)行幾次fuction()方法的意思持偏;
那么看fuction中具體做什么事:
var method = methods[i]; 表示得到數(shù)組中的一個方法
var jsMethod = method.replace(new RegExp(":", "g"), "”); 這就是正則表達式 驼卖,做一些字符替換
好,接著看:
注意左邊鸿秆,jsObj[jsMethod]酌畜,而jsObj是咱們上面創(chuàng)建的一個對象,還記得吧卿叽。對象是個空的桥胞,還沒有任何值;
[jsMethod] 表示往jsObj對象里添加設(shè)置方法考婴,方法名由jsMethod來決定贩虾,是個動態(tài)創(chuàng)建方法;
那么觀察整個函數(shù)inject: function() ?無非就創(chuàng)建了一個對象沥阱,并且往對象中添加了方法缎罢;
好,此時我們又看到:
return EasyJS.call(obj, method, Array.prototype.slice.call(arguments));
3. EasyJS的call方法解析
EasyJS.call考杉,是EasyJS底下的call方法啦屁使;
第一個對象 ,第二個方法 奔则,第三個Array.prototype.slice.call(arguments)蛮寂;
解釋下Array.prototype.slice.call(arguments);
例子:
var a={length:2,0:'first',1:'second'};
Array.prototype.slice.call(a);
//得到結(jié)果: ?["first", "second"]
Array.prototype.slice.call(arguments)能將具有l(wèi)ength屬性的對象轉(zhuǎn)成數(shù)組易茬;
其實就是把key 0和1 的值"first", "second"取出來酬蹋,拼裝成一個數(shù)組及老;
也就是說Array.prototype.slice.call(arguments) ?這個就是個數(shù)組;
Array.prototype:就是Array的原型范抓,Array.prototype.slice這句就是訪問Array的內(nèi)置方法骄恶,因為Array是類名,而不是對象名匕垫,所以不能直接用Array.slice 僧鲁,而要用Array.prototype.slice
解釋下函數(shù)體:
第一部分:
var formattedArgs = [];//申明創(chuàng)建一個數(shù)組
第二部分:
for (var i = 0, l = args.length; i < l; i++) //循環(huán)遍歷數(shù)組
if (typeof args[i] == "function"){如果數(shù)組元素的類型是方法,也就是說args[i]類型是個方法的話就進入
formattedArgs.push("f”);//formattedArgs是個數(shù)組象泵,push表示添加到數(shù)組中相當于我們的add
var cbID = "__cb" + (+new Date);//就是獲取一個時間戳
EasyJS.__callbacks[cbID] = args[i];
formattedArgs.push(cbID);
我們來看EasyJS.__callbacks[cbID] ?//__callbacks這玩意不就是EasyJs對象頂部的聲明的一個對象嘛寞秃?
之前里面沒有任何屬性與方法
那么EasyJS.__callbacks[cbID] = args[i];這個的意思就是往__callbacks對象中添加名叫cbID的屬性與方法;值為args[i];
而剛剛if判斷args[i]類型是方法偶惠,也就是說往__callbacks中增加了方法
第三部分
var argStr = (formattedArgs.length > 0 ? ":" + encodeURIComponent(formattedArgs.join(":")) : "");
formattedArgs.join(":") 首先這是個數(shù)組 調(diào)用數(shù)組的join()方法
返回值是個字符串春寿,也就是說這個方法就是將formattedArgs的每個元素用:分隔開拼接成一個字符串
encodeURIComponent() 函數(shù)可把字符串作為 URI 組件進行編碼。
返回例如:%2C%2F%3F%3A%40%26%3D%2B%24%23
也就是argStr是一個字符串忽孽;
第四部分:
iframe bridge
在 JS 端創(chuàng)建一個透明的 iframe绑改,設(shè)置這個 ifame 的 src 為自定義的協(xié)議,而 ifame 的 src 更改時兄一,UIWebView 會先回調(diào)其 delegate 的webView:shouldStartLoadWithRequest:navigationType:方法如下
var iframe = document.createElement("IFRAME”);//就是創(chuàng)建iframe對象厘线,iframe是html中標簽,
iframe.setAttribute("src", "easy-js:" + obj + ":" + encodeURIComponent(functionName) + argStr);
"easy-js:" + obj + ":" + encodeURIComponent(functionName)+argStr ?//設(shè)置src
document.documentElement.appendChild(iframe);//標簽是要加到html的頁面上的出革,html會加載這個js造壮,只要加載js就會觸發(fā)我們webview的should 和webfinsh等等代理方法
iframe.parentNode.removeChild(iframe);// 使用完之后記得刪除
iframe = null;
五 shouldStartLoadWithRequest函數(shù)解析
當執(zhí)行了js,ifame 的 src 更改時蹋盆,UIWebView 會先回調(diào)shouldStartLoadWithRequest回調(diào)方法费薄,
例子:easy-js:HLJBindJavaScript:setShare%3A:s%3A%257B%2522iconUrl
獲取request string硝全,截取成四個部分栖雾,協(xié)議頭部easy-js:開頭,obj為對象伟众,method為方法析藕,formattedArgs
必須是easy-js:協(xié)議開頭,
獲取類實例方法的簽名凳厢,新建一個NSInvocation實例账胧,指定實例與方法
invoker設(shè)置參數(shù),然后執(zhí)行invoke先紫,注意參數(shù)中function類型的區(qū)分治泥。
獲取invoker執(zhí)行的結(jié)果通過webView執(zhí)行js代碼返回結(jié)果值。
六 分析回調(diào)function的處理過程
上面說過js端call方法這樣處理function的參數(shù)遮精,EasyJS對象中__callbacks對象存儲方法實現(xiàn)對象
再看native端攔截到請求居夹,其中的代碼
再看EasyJSDataFunction中:
return [self.webViewstringByEvaluatingJavaScriptFromString:injection];
將回調(diào)方法執(zhí)行參數(shù)解析封裝js函數(shù)字符串败潦;
注意:
第一個參數(shù)表示js函數(shù)的唯一ID,方便js端找到該函數(shù)對象
第二個表示第一次回調(diào)完成是否移除該回調(diào)執(zhí)行的函數(shù)對象的bool值准脂,然后webView主動執(zhí)行劫扒,這樣就完成個整個的回調(diào)過程。
七 最后總結(jié):
那么這個注入流程就清晰啦@旮唷9导ⅰ!
這個easy的核心也在EasyJSWebViewProxyDelegate湾戳;
NSString* js =INJECT_JS;//INJECT_JS是一段js代碼
[webViewstringByEvaluatingJavaScriptFromString:js];
當webview加載你想要訪問的url的時候贤旷,等資源加載結(jié)束了webViewDidFinishLoad;才會去注入院塞,這里確實也存在注入延遲的問題遮晚,就是說js一旦要做一些一開始加載就想回調(diào)app的事情就做不了了,這也是弊端之一拦止;
好县遣,拋開這個問題不看 我們繼續(xù);
等資源加載結(jié)束了webViewDidFinishLoad ? 就會開始注入對象了
于是調(diào)注入方法- (void) injectJavascript:(UIWebView *)webView{}
[webViewstringByEvaluatingJavaScriptFromString:injection];
我們是通過橋接調(diào)用執(zhí)行window.EasyJS = {}
這樣window就有了EasyJS對象汹族;
EasyJS對象有是call ?inject invokeCallback 三個方法 ?一個__callbacks對象萧求;
以河貍家injection為例 :EasyJS.inject("HLJJavaScript", ["jsShowSomething:", "loginBlock", "resetPassBlock", ?"setShare:"]);
js執(zhí)行這個代碼:因為已經(jīng)存在EasyJS對象,EasyJS可直接調(diào)用注入inject顶瞒;
那么HLJBindJavaScript對象就是native接收和處理js回調(diào)oc所要做的事情夸政;
暴露給js方法有jsShowSomething:", "loginBlock", "resetPassBlock", ?"setShare:”;
于是當h5想要讓native做些事情時榴徐,如設(shè)置分享守问,就可以執(zhí)行:
window.HLJJavaScript.setShare(json串)
就可以很開心滴實現(xiàn)交互啦!?幼省耗帕!
八 期待拓展
當我們利用此協(xié)議實現(xiàn)了js與oc交互之后,其實可以幫助我們做很多很多事情袱贮;
如何利用運行時機制+組件化設(shè)計搭建app框架仿便;
將會在不久后輸出文章;
我的心愿是:咸魚翻身T芪 K砸恰!不要再讓我?guī)浀饺巳撕按颍柒莉。闻坚。?br>