導(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ù)
- 當(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)方法
- 網(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ù)
- 核心方法是 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。
- 使用方法
- 如果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。
- 構(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";
- 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);
};
- 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)求由于很快被替換掉,所以被忽略掉了。
- 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;
}
- 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);
};
- 將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的全流程。