jsNative 是什么
jsNative 是一款标准化 jsbridge 的框架,为了解决常见 jsbridge 的混乱问题,如描述不清等,说白了可以理解成 ts 版本的 jsbridge,强约束我们定义的 jsbridge 的接口,这对于大型应用和多人开发的项目尤为重要。具体的调用可以一窥, 为了调用 net.request 方法, 通过 args 给参数加了约束,通过 invoke 指定了调用的过程
jsNative.add({
"invoke": "method.json",
"name": "net.request",
"method": "_naNet.request",
"args": [
{"name": "url", "value": "string"},
{"name": "method", "value": "string"},
{"name": "onsuccess", "value": "function"}
]
});
jsNative.invoke('net.request', ['my-url', 'GET', data => {}]);
其中 method.json 代表调用过程一个快捷方式,等价于下面,可以看出这个过程包括了对参数的校验、参数中回调函数参数的解码、参数中回调函数的编码、参数的序列化处理、调用 native 的方式以及最后对返回值的处理。
"invoke": [
"ArgCheck",
"ArgFuncArgDecode:JSON",
"ArgFuncEncode",
"ArgEncode:JSON",
"CallMethod",
"ReturnDecode:JSON"
],
具体 jsNative 的使用可以去看看官方文档,可以说 jsNative 抹平了不同场景下 js 和 native 通信的差异性。
jsNative 原理
从上面就可以看出,jsNative 对 jsbridge 中的每个过程进行了干预,我们通过 args 保证参数的合法性,通过 invoke 来确保对 native 的调用从入参、调用方式到最后的返回值都符合我们预期想要的值,这种规范化带来的好处远远大于使用的繁琐。下面拿上面net.request这个例子作为引子,讲讲 jsNative 内部怎么去运转。
jsNative 可以看为一个 api 容器,里面包含了通过所有 add添加的api 描述,当然我们也通过新建一个新的容器,而不使用默认的容器。当调用 jsNative.add 的时候,在标准化我们参数之后,就把描述塞入 ``this.apis``` 里面去。
var realDesc = normalizeDescription(description, this.descriptionPropMerger);
this.apiIndex[name] = this.apisLen;
this.apis[this.apisLen++] = realDesc;
接下来是jsNative.invoke('net.request', ['my-url', 'GET', data => {}]);进行 native 通信,那么在 invoke 过程中发生了什么呢?
invoke: function (name, args) {
return invokeDescription(this.apis[this.apiIndex[name]], args);
},
invoke 首先是拿到我们刚才标准化后的 des,然后调用 invokeDescription, invokeDescription 遍历 getProcessors 拿到的Processors,不断调用 processor 对参数 args 进行处理。
function invokeDescription(description, args) {
if (description) {
args = args || [];
each(getProcessors(description), function (processor) {
args = processor(args);
});
return args;
}
}
那么 getProcessors 里面到底干了什么呢?主要是从 description.invoke 中拿到我们 processors
前面我们说过,我们初始化 jsNative.add 的时候,invoke 为 methond.json 会在 normalizeDescription 的调换下面的数组,可以认为 methos.json 是下面数组的快捷方式
"invoke": [
"ArgCheck",
"ArgFuncArgDecode:JSON",
"ArgFuncEncode",
"ArgEncode:JSON",
"CallMethod",
"ReturnDecode:JSON"
],
那么在下面 description.invoke 的遍历过程中,对于每个 processName, 冒号后面为这个 processName 的 option。
function getProcessors(description) {
var processors = [];
if (!description.invoke) {
throw new Error('[' + apiContainer.options.errorTitle + '] invoke undefined: ' + description.name);
}
each(description.invoke, function (processName) {
var dotIndex = processName.indexOf(':');
var option;
if (dotIndex > 0) {
option = processName.slice(dotIndex + 1);
processName = processName.slice(0, dotIndex);
}
var processor = processorCreators[processName](description, option, apiContainer);
if (typeof processor === 'function') {
processors.push(processor);
}
});
return processors;
}
OK,那么下面依次看看 invoke 中的各个方法到底是干什么的。
"invoke": [
"ArgCheck",
"ArgFuncArgDecode:JSON",
"ArgFuncEncode",
"ArgEncode:JSON",
"CallMethod",
"ReturnDecode:JSON"
],
- ArgCheck 进行参数校验, 对于参数不符合我们预设要求的,直接抛出错误
function checkArgs(args, declarations, apiContainer) {
each(declarations, function (declaration, i) {
var errorMsg;
var value = normalizeValueDeclaration(declaration.value);
switch (checkValue(args[i], value)) {
case 1:
errorMsg = ' is required.';
break;
case 2:
errorMsg = ' type error. must be ' + JSON.stringify(value.type || 'Array');
break;
case 3:
errorMsg = ' type error, must be oneOf ' + JSON.stringify(value.oneOf);
break;
case 4:
errorMsg = ' type error, must be oneOfType ' + JSON.stringify(value.oneOfType);
break;
case 5:
errorMsg = ' type error, must be arrayOf ' + JSON.stringify(value.arrayOf);
break;
}
if (errorMsg) {
var title = apiContainer && apiContainer.options.errorTitle || 'jsNative';
throw new Error('[' + title + ' Argument Error]' + declaration.name + errorMsg);
}
});
}
- ArgFuncArgDecode:JSON, 对参数中的回调函数中的参数进行解码还是原封不动地传过去。
ArgFuncArgDecode: function (description, option) {
return option === 'JSON'
? wrapDecodeFuncArgs
: returnRaw;
},
/**
* 对调用参数中的所有回调函数,进行参数解码(反序列化)包装
*
* @inner
* @param {Array} args 调用参数
* @return {Array}
*/
function wrapDecodeFuncArgs(args) {
each(args, function (arg, i) {
if (typeof arg === 'function') {
args[i] = wrapDecodeFuncArg(arg);
}
});
return args;
}
/**
* 对回调函数的参数进行解码(反序列化)包装
*
* @inner
* @param {Function} fn 回调函数
* @return {Function}
*/
function wrapDecodeFuncArg(fn) {
return function (arg) {
fn(typeof arg === 'string' ? JSON.parse(arg) : arg);
};
}
- ArgFuncEncode 对参数的回调函数进行序列化,因为大部分我们调用 native的时候无法把函数也传过去,所以只能传函数 id,让客户端在适当的使用调用我们的函数,同时注意函数变量回收,防止内存泄漏
function wrapArgFunc(args) {
each(args, function (arg, i) {
if (typeof arg === 'function') {
args[i] = wrapFunc(arg);
}
});
return args;
}
function wrapFunc(fn) {
var funcName = FUNC_PREFIX + (funcId++);
root[funcName] = function (arg) {
delete root[funcName];
fn(arg);
};
return funcName;
}
- ArgEncode:JSON 是否需要对传入的参数进行序列化处理
function argJSONEncode(args) {
each(args, function (arg, i) {
args[i] = JSON.stringify(arg);
});
return args;
}
ArgEncode: function (description, option) {
return option === 'JSON'
? argJSONEncode
: returnRaw;
},
- CallMethod 决定怎么调用 native的方法,CallMethod 相当于直接调用 native 注入到客户端的方法
CallMethod: function (description, option) {
var methodOwner;
var methodName;
function findMethod() {
if (!methodOwner) {
var segs = description.method.split('.');
var lastIndex = segs.length - 1;
methodName = segs[lastIndex];
methodOwner = root;
for (var i = 0; i < lastIndex; i++) {
methodOwner = methodOwner[segs[i]];
}
}
}
return function (args) {
findMethod();
switch (description.args.length) {
case 0:
return methodOwner[methodName]();
case 1:
return methodOwner[methodName](args[0]);
case 2:
return methodOwner[methodName](args[0], args[1]);
case 3:
return methodOwner[methodName](args[0], args[1], args[2]);
}
return methodOwner[methodName].apply(methodOwner, args);
};
},
但是 js 与客户端的通信当然不止这一种方法啦, 具体使用的区别可以,看 description 中的描述,其中可以看出method 根据是否支持参数复杂类型区别,对于不支持的需要使用methos.json,常见场景为 Android 下 addJavaScriptInterface 注入的方法, prompt 支持 同步返回、参数需要合并和序列化 以及返回参数需要进行反序列化,location、iframe 这两种拦截的方式只支持异步返回以及参数合并+序列化,CallMessage 支持 ios, 虽然支持复杂类型但是只支持 Object 与 Array,但是不支持 Function,以及只能异步返回
var INVOKE_CALL_MAP = {
method: 'CallMethod',
prompt: 'CallPrompt',
location: 'CallLocation',
iframe: 'CallIframe',
message: 'CallMessage'
};
/**
* 通过 prompt 对话框进行 Native 调用
*
* @inner
* @param {string} source 要传递的数据字符串
* @return {string}
*/
function callPrompt(source) {
return root.prompt(source);
}
/**
* 通过 location.href 进行 Native 调用
*
* @inner
* @param {string} url 要传递的url字符串
*/
function callLocation(url) {
root.location.href = url;
}
/**
* 通过 iframe 进行 Native 调用
*
* @inner
* @param {string} url 要传递的url字符串
*/
function callIframe(url) {
var iframe = document.createElement('iframe');
iframe.src = url;
document.body.appendChild(iframe);
document.body.removeChild(iframe);
}
CallMessage: function (description) {
return function (args) {
root.webkit.messageHandlers[description.handler].postMessage(args);
};
},
- ReturnDecode:JSON 对返回值进行怎样的处理, 当然从 js 与 native 通信的不同,不是所有的 invoke 都有这一步
function returnJSONDecode(source) {
return typeof source === 'string' ? JSON.parse(source) : source;
}
ReturnDecode: function (description, option) {
return option === 'JSON'
? returnJSONDecode
: returnRaw;
}
总结
jsNative 可以说在我们调用 jsbridge 过程中进行全方位的护卫,大大提成了应用的稳定性和可维护性,虽然有一定的上手成本。