导语
越来越多的iOS程序都内嵌了H5页面,微信也提供了公众平台方便第三方应用开发者以网页JS的形式向用户提供更丰富的界面展示和内容。iOS移动客户端想尽可能多的提供接口让网页能获取App内的信息,与此同时,App也希望能有一种手段让能控制网页JS的行为。这种控制与被控制,调用与被调用,就是这篇文章所关注核心技术点。
核心技术点
稍加分析,我们可以将App内的网页JS与Native Code的互动分解为两大难点。网页JS如何向Native Code传递其想调用的Native Code接口和参数 Native Code收到了网页JS调用后,执行相关的逻辑结束后,如何将执行的结果通知回网页JS
一、网页JS如何向Native Code传递其想调用的Native Code接口和参数
- 当我们用 UIWebView 作为App内嵌的浏览器进行网络访问时,每次网页访问一个新的url,都会触发其delegate协议 UIWebViewDelegate 的回调方法
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
在这个方法内部我们可以通过 request 参数获取网页即将访问的url
[[request URL] absoluteString]
网页可以通过触发访问新的url,将想调用的Native Code接口和参数通过JSON格式化后,放在新的url中,通知回Native Code的 UIWebViewDelegate 的回调方法
- 网页中触发新的url有两种常用的方式
- 网页JS通过修改doucument.loaction
document.location='http://new.url/params?key=value'
- 新建一个看不见的iframe标签,修改它的属性 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);
这两种方法一个很大的区别在于,前者因为是修改的主frame的location,极有可能破坏整个网页的JS逻辑,并造成网页卡顿等异常现象。后者因为创建了一个新的不可见子frame,逻辑上只影响这个空的iframe,UI上因为不显示也不会造成任何UI干扰和渲染耗时。
二、Native Code如何调用网页JS接口并向其传递参数
- 核心方法是 UIWebView 的方法
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
参数 script 就是需要执行的JS代码,返回结果为JS执行结果。这里建议如果JS需要返回大量数据,最好将数据放入数组或对象,再转化成JSON字符串返回给Native Code。
- 使用方法
- 如果Native Code想让网页JS返回用户选购的商品ID列表,首先需要准备JS代码。我们把选购的商品ID放入数组,再将数组转化成JSON字符串返回。代码如下
function getOrderList() {
var orderList = ['10001', '10045', '54321'];
var orderListString = JSON.stringify(orderList);
orderList = [];
return orderListString;
};
- Native Code只需要执行
NSString *js = @"getOrderList();";
NSString *result = [webView stringByEvaluatingJavaScriptFromString:js];
就可以让网页JS执行相关的代码,并同步的取得返回值
行文至此,我们已经掌握了Native Code和网页JS进行交互的两个核心技术方法。接下来,讲介绍如何运用这些方法来打造一套专属于App的JSSDK。
三、一个完整的JSSDK实现方案
我们依然以一个购物页面作为例子来讲述,如何实现一套JSSDK。
- 构建Native Code接口和JSSDK接口
我们在Native Code层先定义好,交互接口的名称字符串。这个字符串就是第三方应用开发者看见的,由App开发者提供的JSSDK的接口。
NSString *const jssdk_getUserInfo = @"jssdk.getUserInfo";
NSString *const jssdk_payMyBill = @"jssdk.payMyBill";
NSString *const jssdk_showLogistics = @"jssdk.showLogistics";
- JS构建一次合法的调用Native Code
上面已经介绍了如何从JS层将信息传递给Native Code,那么哪些信息是网页需要传递给App的呢?SDK的提供者需要提供: 接口名称 接口参数 callback回调ID 。其中 callback回调ID 是隐式调用的,第三方应用开发者并不需要关心。它是用于SDK内部正确的回调到调用者指定方法的一个索引。
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实际上进行了两个工作。将接口名称、参数、回调callbackID对象经过JSON处理的字符串保存起来。以及通过通过改变iframe属性的方式,通知Native主动来取数据。Native Code在回调中就可以在回调中收到JS传递过来的新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主动获取JS想调用的接口列表
- (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可以把想传递的内容放在url中传递给Native Code,但这里强烈不推荐这样实现。上面OC回调方法中的 request 中的长度是有限制的。一旦数据超常依然只能从Native Code层发起调用,主动获取数据,如上面的代码所示。document.location还有一个很严重的问题,就是异步带来的请求被忽略。如果我们连续 2 个 js 调 Native Code,连续 2 次改 document.location 的话,在 Native Code 的 delegate 方法中,只能截获后面那次请求,前一次请求由于很快被替换掉,所以被忽略掉了。
- Native Code将处理结果返回JS层
和第二步中取JS调用数据的方式一样。Native Code把返回数据构造成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执行结果,并将其返回给初始调用时注册的callback函数
function processFinalResultFromNativeCode(jsFinalResult) {
var msgWrap = jsFinalResult;
var ret = callback_map [jsFinalResult['callbackID']](jsFinalResult['params']);
delete callback_map [jsFinalResult['callbackID']];
return JSON.stringify(ret);
};
- 将JSSDK框架注入第三方网页
从整体流程来讲,JSSDK框架的注入应该是第一步,但却被放到了文章的末尾。如果理解了Native Code和JS如何互相调用交互,框架的注入那就是水到渠成的事情。将上面讲到的JS逻辑封装到一个自完成自调用的JS函数里面,通过Native Code将处理结果返回JS层一模一样的方式,就可以给WEB注入我们的JSSDK框架了。奉上文章最后一段代码来解释,JSSDK是如何开始其生命。
NSString *jssdkPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"jssdk.js"];
NSString *jssdkContent = [NSString stringWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];
[_webView stringByEvaluatingJavaScriptFromString:jssdkContent];
到这里一个简单但完整的JSSDK的demo就给各位看官呈现完毕。实际实现中会加入数据合法性、消息派发机制来辅助整个框架的更高效和安全的实现。这篇文章囿于篇幅限制只介绍了最关键的一些部分,像JSON格式化、HTML调用JSSDK接口、UIWebView等都没有涉及。有兴趣的读者可以阅读一些相关文章和代码,从整体上补全JSSDK的全流程。