iOS程序中網(wǎng)頁(yè)與OC代碼互相調(diào)用的原理+一個(gè)實(shí)現(xiàn)方案的分析

導(dǎo)語(yǔ)

越來(lái)越多的iOS程序都內(nèi)嵌了H5頁(yè)面,微信也提供了公眾平臺(tái)方便第三方應(yīng)用開(kāi)發(fā)者以網(wǎng)頁(yè)JS的形式向用戶(hù)提供更豐富的界面展示和內(nèi)容。iOS移動(dòng)客戶(hù)端想盡可能多的提供接口讓網(wǎng)頁(yè)能獲取App內(nèi)的信息,與此同時(shí),App也希望能有一種手段讓能控制網(wǎng)頁(yè)JS的行為。這種控制與被控制,調(diào)用與被調(diào)用,就是這篇文章所關(guān)注核心技術(shù)點(diǎn)。

核心技術(shù)點(diǎn)

稍加分析,我們可以將App內(nèi)的網(wǎng)頁(yè)JS與Native Code的互動(dòng)分解為兩大難點(diǎn)。網(wǎng)頁(yè)JS如何向Native Code傳遞其想調(diào)用的Native Code接口和參數(shù) Native Code收到了網(wǎng)頁(yè)JS調(diào)用后,執(zhí)行相關(guān)的邏輯結(jié)束后,如何將執(zhí)行的結(jié)果通知回網(wǎng)頁(yè)JS

一、網(wǎng)頁(yè)JS如何向Native Code傳遞其想調(diào)用的Native Code接口和參數(shù)
  1. 當(dāng)我們用 UIWebView 作為App內(nèi)嵌的瀏覽器進(jìn)行網(wǎng)絡(luò)訪(fǎng)問(wèn)時(shí),每次網(wǎng)頁(yè)訪(fǎng)問(wèn)一個(gè)新的url,都會(huì)觸發(fā)其delegate協(xié)議 UIWebViewDelegate 的回調(diào)方法
 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

在這個(gè)方法內(nèi)部我們可以通過(guò) request 參數(shù)獲取網(wǎng)頁(yè)即將訪(fǎng)問(wèn)的url

[[request URL] absoluteString]

網(wǎng)頁(yè)可以通過(guò)觸發(fā)訪(fǎng)問(wèn)新的url,將想調(diào)用的Native Code接口和參數(shù)通過(guò)JSON格式化后,放在新的url中,通知回Native Code的 UIWebViewDelegate 的回調(diào)方法

  1. 網(wǎng)頁(yè)中觸發(fā)新的url有兩種常用的方式
  • 網(wǎng)頁(yè)JS通過(guò)修改doucument.loaction
document.location='http://new.url/params?key=value'
  • 新建一個(gè)看不見(jiàn)的iframe標(biāo)簽,修改它的屬性 src
newIframe = document.createElement('iframe');
newIframe.style.display = 'none';
document.documentElement.appendChild(newIframe);
newIframe.src = 'http://new.url/params?key=value';
document.documentElement.removeChild(newIframe);
  • 這兩種方法一個(gè)很大的區(qū)別在于,前者因?yàn)槭切薷牡闹鱢rame的location,極有可能破壞整個(gè)網(wǎng)頁(yè)的JS邏輯,并造成網(wǎng)頁(yè)卡頓等異常現(xiàn)象。后者因?yàn)閯?chuàng)建了一個(gè)新的不可見(jiàn)子frame,邏輯上只影響這個(gè)空的iframe,UI上因?yàn)椴伙@示也不會(huì)造成任何UI干擾和渲染耗時(shí)。

二、Native Code如何調(diào)用網(wǎng)頁(yè)JS接口并向其傳遞參數(shù)
  1. 核心方法是 UIWebView 的方法
 - (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

參數(shù) script 就是需要執(zhí)行的JS代碼,返回結(jié)果為JS執(zhí)行結(jié)果。這里建議如果JS需要返回大量數(shù)據(jù),最好將數(shù)據(jù)放入數(shù)組或?qū)ο螅俎D(zhuǎn)化成JSON字符串返回給Native Code。

  1. 使用方法
  • 如果Native Code想讓網(wǎng)頁(yè)JS返回用戶(hù)選購(gòu)的商品ID列表,首先需要準(zhǔn)備JS代碼。我們把選購(gòu)的商品ID放入數(shù)組,再將數(shù)組轉(zhuǎn)化成JSON字符串返回。代碼如下
function getOrderList() {
  var orderList = ['10001', '10045', '54321'];
  var orderListString = JSON.stringify(orderList);
  orderList = [];
  return orderListString;
};
  • Native Code只需要執(zhí)行
NSString *js = @"getOrderList();";
NSString *result = [webView stringByEvaluatingJavaScriptFromString:js];

就可以讓網(wǎng)頁(yè)JS執(zhí)行相關(guān)的代碼,并同步的取得返回值

行文至此,我們已經(jīng)掌握了Native Code和網(wǎng)頁(yè)JS進(jìn)行交互的兩個(gè)核心技術(shù)方法。接下來(lái),講介紹如何運(yùn)用這些方法來(lái)打造一套專(zhuān)屬于App的JSSDK。

三、一個(gè)完整的JSSDK實(shí)現(xiàn)方案

我們依然以一個(gè)購(gòu)物頁(yè)面作為例子來(lái)講述,如何實(shí)現(xiàn)一套JSSDK。

  1. 構(gòu)建Native Code接口和JSSDK接口
    我們?cè)贜ative Code層先定義好,交互接口的名稱(chēng)字符串。這個(gè)字符串就是第三方應(yīng)用開(kāi)發(fā)者看見(jiàn)的,由App開(kāi)發(fā)者提供的JSSDK的接口。
NSString *const jssdk_getUserInfo = @"jssdk.getUserInfo";
NSString *const jssdk_payMyBill = @"jssdk.payMyBill";
NSString *const jssdk_showLogistics = @"jssdk.showLogistics";
  1. JS構(gòu)建一次合法的調(diào)用Native Code
    上面已經(jīng)介紹了如何從JS層將信息傳遞給Native Code,那么哪些信息是網(wǎng)頁(yè)需要傳遞給App的呢?SDK的提供者需要提供: 接口名稱(chēng) 接口參數(shù) callback回調(diào)ID 。其中 callback回調(diào)ID 是隱式調(diào)用的,第三方應(yīng)用開(kāi)發(fā)者并不需要關(guān)心。它是用于SDK內(nèi)部正確的回調(diào)到調(diào)用者指定方法的一個(gè)索引。
var callback_index = 1;
var callback_map = {};
function callNativeCode(func, params, callback) {
        if (!func || typeof func !== 'string') return;
        if (typeof params !== 'object') params = {};

        var callbackID = (callback_index++).toString();

        if (typeof callback === 'function')
          callback_map[callbackID] = callback;

        var paramsObj = {'func':func,'params':params,'callbackID':callbackID};
        var paramsForNative = JSON.stringify(paramsObj);
        callNativeCode(paramsForNative);
}
callNativeCode('jssdk.getUserInfo',{'code':'0fec3575758a06c203868edcca748565'},function(openid, name, sex){
// to something to process return value
});

上面代碼中的callNativeCode實(shí)際上進(jìn)行了兩個(gè)工作。將接口名稱(chēng)、參數(shù)、回調(diào)callbackID對(duì)象經(jīng)過(guò)JSON處理的字符串保存起來(lái)。以及通過(guò)通過(guò)改變iframe屬性的方式,通知Native主動(dòng)來(lái)取數(shù)據(jù)。Native Code在回調(diào)中就可以在回調(diào)中收到JS傳遞過(guò)來(lái)的新url。

var callNativeCodeQueue = [];
function callNativeCode(message) {
     callNativeCodeQueue.push(message);

     messagingIframe = document.createElement('iframe');
     messagingIframe.style.display = 'none';
     messagingIframe.src = 'http://new.url/params?key=value';
     document.documentElement.appendChild(messagingIframe);
     document.documentElement.removeChild(messagingIframe);
};
  1. Native Code主動(dòng)獲取JS想調(diào)用的接口列表
 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
...
    NSRange jssdkSchemaRange = [urlString rangeOfString:@"'http://new.url/params?key=value"];
    if (jssdkSchemaRange.location == 0) {
        NSString *jssdkInputQueueJSON = [webView stringByEvaluatingJavaScriptFromString:@"fetchJSSDKInputQueueJSON();"];
        [self unpackJSSDKInputQueueJSON:jssdkInputQueueJSON];

        return NO;
    }
...
}

 - (void)unpackJSSDKInputQueueJSON:(NSString *)jssdkInputQueueJSON {
    NSArray *inputQueue = [jssdkInputQueueJSON JSONArray];
    if (nil == inputQueue) return;
    for (int i = 0; i < inputQueue.count; i++) {
            NSString *oneJSCall = [inputQueue stringAtIndex:i];
            NSDictionary  *jsCallParams = [oneJSCall JSONDictionary];
            if (nil == jsCallParams) return;

            NSString *funcName = [jsCallParams stringForKey:@"func"];
            NSDictionary *params = [jsCallParams dictionaryForKey:@"params"];
            NSString *callbackID = [dic stringForKey:@"callbackID"];
            if ((funcName.length == 0) || (params == nil)) return;

            [self jsInvokeNativeCode:funcName withParams:params withCallbackID:callbackID];
}

 - (void)functionCall:(NSString *)funcName withParams:(NSDictionary *)params withCallbackID:(NSString *)callbackID {
    if ("jssdk.getUserInfo" == funcName) return [self getUserInfo:params withCallbackID:callbackID];
    if ("jssdk.payMyBill" == funcName) return [self payMyBill:params withCallbackID:callbackID];
    if ("jssdk.showLogistics" == funcName) return [self showLogistics:params withCallbackID:callbackID];
 }

似乎JS可以把想傳遞的內(nèi)容放在url中傳遞給Native Code,但這里強(qiáng)烈不推薦這樣實(shí)現(xiàn)。上面OC回調(diào)方法中的 request 中的長(zhǎng)度是有限制的。一旦數(shù)據(jù)超常依然只能從Native Code層發(fā)起調(diào)用,主動(dòng)獲取數(shù)據(jù),如上面的代碼所示。document.location還有一個(gè)很?chē)?yán)重的問(wèn)題,就是異步帶來(lái)的請(qǐng)求被忽略。如果我們連續(xù) 2 個(gè) js 調(diào) Native Code,連續(xù) 2 次改 document.location 的話(huà),在 Native Code 的 delegate 方法中,只能截獲后面那次請(qǐng)求,前一次請(qǐng)求由于很快被替換掉,所以被忽略掉了。

  1. Native Code將處理結(jié)果返回JS層
    和第二步中取JS調(diào)用數(shù)據(jù)的方式一樣。Native Code把返回?cái)?shù)據(jù)構(gòu)造成NSDictionary并用JSON格式化后傳遞給JS層。
 -(NSString*) callbackToJS:(NSString*)callbackID withResult:(NSDictionary*)nativeResult {
    NSMutableDictionary *toJSResult = [NSMutableDictionary dictionary];
    [toJSResult setObject:nativeResult ? nativeResult:[NSDictionary dictionary] forKey:@"params"];
    [toJSResult setObject:callBackID forKey:@"callBackID"];
    NSString *toJSResultJSON = [toJSResult JSONRepresentation]; //將NSDictionary格式化成NSString字符串
    NSString *jsFinalResult = [_webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"processFinalResultFromNativeCode(%@);", toJSResultJSON]];

    return jsFinalResult;
 }
  1. JS拿到Native Code執(zhí)行結(jié)果,并將其返回給初始調(diào)用時(shí)注冊(cè)的callback函數(shù)
function processFinalResultFromNativeCode(jsFinalResult) {
      var msgWrap = jsFinalResult;
      var ret = callback_map [jsFinalResult['callbackID']](jsFinalResult['params']);
      delete callback_map [jsFinalResult['callbackID']];
      return JSON.stringify(ret);
};
  1. 將JSSDK框架注入第三方網(wǎng)頁(yè)
    從整體流程來(lái)講,JSSDK框架的注入應(yīng)該是第一步,但卻被放到了文章的末尾。如果理解了Native Code和JS如何互相調(diào)用交互,框架的注入那就是水到渠成的事情。將上面講到的JS邏輯封裝到一個(gè)自完成自調(diào)用的JS函數(shù)里面,通過(guò)Native Code將處理結(jié)果返回JS層一模一樣的方式,就可以給WEB注入我們的JSSDK框架了。奉上文章最后一段代碼來(lái)解釋?zhuān)琂SSDK是如何開(kāi)始其生命。
NSString *jssdkPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"jssdk.js"];
NSString *jssdkContent = [NSString stringWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];
[_webView stringByEvaluatingJavaScriptFromString:jssdkContent];

到這里一個(gè)簡(jiǎn)單但完整的JSSDK的demo就給各位看官呈現(xiàn)完畢。實(shí)際實(shí)現(xiàn)中會(huì)加入數(shù)據(jù)合法性、消息派發(fā)機(jī)制來(lái)輔助整個(gè)框架的更高效和安全的實(shí)現(xiàn)。這篇文章囿于篇幅限制只介紹了最關(guān)鍵的一些部分,像JSON格式化、HTML調(diào)用JSSDK接口、UIWebView等都沒(méi)有涉及。有興趣的讀者可以閱讀一些相關(guān)文章和代碼,從整體上補(bǔ)全JSSDK的全流程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,610評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,939評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,668評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 56,004評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,173評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,426評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,656評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,833評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,247評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,580評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,371評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,621評(píng)論 2 380

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