hybrid app如何通過js注入實現(xiàn)js與native交互(EasyJSWebView)

前戲

自己從事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)里童叠;

圖片發(fā)自簡書App

EasyJSWebViewProxyDelegate的關(guān)鍵回調(diào)如何實現(xiàn)呢框喳?

核心代碼:

圖片發(fā)自簡書App


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扶认;

圖片發(fā)自簡書App


對INJECT_JS 這段js進行深入源碼解析

1.EasyJS對象的內(nèi)容

圖片發(fā)自簡書App2.EasyJS的inject方法解析

當js執(zhí)行這段js侨拦,會在window創(chuàng)建 EasyJS對象,EasyJS對象有call ?/inject /invokeCallback 三個方法 ?辐宾,一個__callbacks對象狱从;

我們從下往上看;

解析inject方法:

圖片發(fā)自簡書App

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)中做的事情:

圖片發(fā)自簡書App

首先簡化下結(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"), "”); 這就是正則表達式 驼卖,做一些字符替換

好,接著看:

圖片發(fā)自簡書App

注意左邊鸿秆,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方法啦屁使;

圖片發(fā)自簡書App


第一個對象 ,第二個方法 奔则,第三個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)方法费薄,

圖片發(fā)自簡書App

例子: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é)果值。

圖片發(fā)自簡書App

六 分析回調(diào)function的處理過程

上面說過js端call方法這樣處理function的參數(shù)遮精,EasyJS對象中__callbacks對象存儲方法實現(xiàn)對象

圖片發(fā)自簡書App

再看native端攔截到請求居夹,其中的代碼

圖片發(fā)自簡書App


再看EasyJSDataFunction中:


圖片發(fā)自簡書App

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>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市兢孝,隨后出現(xiàn)的幾起案子窿凤,更是在濱河造成了極大的恐慌搀擂,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卷玉,死亡現(xiàn)場離奇詭異哨颂,居然都是意外死亡,警方通過查閱死者的電腦和手機相种,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門威恼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人寝并,你說我怎么就攤上這事箫措。” “怎么了衬潦?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵斤蔓,是天一觀的道長。 經(jīng)常有香客問我镀岛,道長弦牡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任漂羊,我火速辦了婚禮驾锰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘走越。我一直安慰自己椭豫,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布旨指。 她就那樣靜靜地躺著赏酥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谆构。 梳的紋絲不亂的頭發(fā)上裸扶,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音低淡,去河邊找鬼姓言。 笑死瞬项,一個胖子當著我的面吹牛蔗蹋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播囱淋,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼猪杭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了妥衣?” 一聲冷哼從身側(cè)響起皂吮,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤戒傻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蜂筹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體需纳,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年艺挪,在試婚紗的時候發(fā)現(xiàn)自己被綠了不翩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡麻裳,死狀恐怖口蝠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情津坑,我是刑警寧澤妙蔗,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站疆瑰,受9級特大地震影響眉反,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜穆役,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一禁漓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧孵睬,春花似錦播歼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蹈集,卻和暖如春烁试,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拢肆。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工减响, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人郭怪。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓支示,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鄙才。 傳聞我的和親對象是個殘疾皇子颂鸿,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容