浅谈 jsNative

2,250 阅读5分钟

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"
],
  1. 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);
        }
    });
}
  1. 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);
    };
}
  1. 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;
}
  1. 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;
},

  1. 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);
    };
},
  1. 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 过程中进行全方位的护卫,大大提成了应用的稳定性和可维护性,虽然有一定的上手成本。