payload: {"action":"opened","issue":{"url":"https://api.github.com/repos/mcuking/my-blog/issues/36","repository_url":"https://api.github.com/repos/mcuking/my-blog","labels_url":"https://api.github.com/repos/mcuking/my-blog/issues/36/labels{/name}","comments_url":"https://api.github.com/repos/mcuking/my-blog/issues/36/comments","events_url":"https://api.github.com/repos/mcuking/my-blog/issues/36/events","html_url":"https://github.com/mcuking/my-blog/issues/36","id":427234540,"node_id":"MDU6SXNzdWU0MjcyMzQ1NDA=","number":36,"title":"mini-JSBridge 实现原理解析","user":{"login":"mcuking","id":22924912,"node_id":"MDQ6VXNlcjIyOTI0OTEy","avatar_url":"https://avatars2.githubusercontent.com/u/22924912?v=4","gravatar_id":"","url":"https://api.github.com/users/mcuking","html_url":"https://github.com/mcuking","followers_url":"https://api.github.com/users/mcuking/followers","following_url":"https://api.github.com/users/mcuking/following{/other_user}","gists_url":"https://api.github.com/users/mcuking/gists{/gist_id}","starred_url":"https://api.github.com/users/mcuking/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mcuking/subscriptions","organizations_url":"https://api.github.com/users/mcuking/orgs","repos_url":"https://api.github.com/users/mcuking/repos","events_url":"https://api.github.com/users/mcuking/events{/privacy}","received_events_url":"https://api.github.com/users/mcuking/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2019-03-30T03:02:29Z","updated_at":"2019-03-30T03:02:29Z","closed_at":null,"author_association":"OWNER","body":"mini-JSBridge 项目以 js 与 android 通信为例，讲解 JSBridge 实现原理，下面提到的方法在iOS（UIWebview 或 WKWebview）均有对应方法。\r\n\r\n## 1. native to js\r\n两种 native 调用 js 方法，注意被调用的方法需要在 JS 全局上下文上\r\n\r\n##### loadUrl\r\n##### evaluateJavascript\r\n\r\n#### 1.1 loadUrl\r\n```java\r\nmWebview.loadUrl(\"javascript: func()\");\r\n```\r\n\r\n#### 1.2 evaluateJavascript\r\n```java\r\nmWebview.evaluateJavascript(\"javascript: func()\", new ValueCallback<String>() {\r\n    @Override\r\n    public void onReceiveValue(String value) {\r\n        return;\r\n    }\r\n});\r\n```\r\n两种方法的对比如下表：\r\n\r\n| 方法名 | 优点 | 缺点 |\r\n| ------ | ------ | ------ |\r\n| loadUrl | 兼容性好 | 1. 会刷新页面 2. 无法获取js方法执行结果 |\r\n| evaluateJavascript | 1. 性能好 2. 可获取js执行后的返回值 | 仅在安卓4.4以上可用 |\r\n\r\n## 2. js to native\r\n\r\n三种 js 调用 native 方法\r\n##### 拦截 Url Schema（假请求）\r\n##### 拦截 prompt alert confirm\r\n##### 注入 JS 上下文\r\n\r\n#### 2.1 拦截 Url Schema\r\n\r\n即由 h5 发出一条新的跳转请求，native 通过拦截 URL 获取 h5 传过来的数据。\r\n\r\n跳转的目的地是一个非法不存在的URL地址，例如：\r\n\r\n```javascript\r\n\"JSBridge://methodName?{\"data\": arg, \"cbName\": cbName}\"\r\n```\r\n\r\n具体示例如下：\r\n\r\n```javascript\r\n\"JSBridge://openScan?{\"data\": {\"scanType\": \"qrCode\"}, \"cbName\": \"handleScanResult\"}\"\r\n```\r\n\r\nh5 和 native 约定一个通信协议，例如 JSBridge, 同时约定调用 native 的方法名 methodName 作为域名，以及后面带上调用该方法的参数 arg，和接收该方法执行结果的 js 方法名 cbName。\r\n\r\n具体可以在 js 端封装相关方法，供业务端统一调用，代码如下：\r\n\r\n```javascript\r\nwindow.callbackId = 0;\r\n\r\nfunction callNative(methodName, arg, cb) {\r\n    const args = {\r\n      data: arg === undefined ? null : JSON.stringify(arg),\r\n    };\r\n\r\n    if (typeof cb === 'function') {\r\n      const cbName = 'CALLBACK' + window.callbackId++;\r\n      window[cbName] = cb;\r\n      args['cbName'] = cbName;\r\n    }\r\n\r\n    const url = 'JSBridge://' + methodName + '?' + JSON.stringify(args);\r\n\r\n    ...\r\n}\r\n```\r\n以上封装中较为巧妙的是将用于接收 native 执行结果的 js 回调方法 cb 挂载到 window 上，并为防止命名冲突，通过全局的 callbackId 来区分，然后将该回调函数在 window 上的名字放在参数中传给 native 端。native 拿到 cbName 后，执行完方法后，将执行结果通过 native 调用 js 的方式（上面提到的两种方法），调用 cb 传给 h5 端（例如将扫描结果传给 h5）。\r\n\r\n至于如何在 h5 中发起请求，可以设置 window.location.href 或者创建一个新的 iframe 进行跳转。\r\n\r\n```javascript\r\nfunction callNative(methodName, arg, cb) {\r\n    ...\r\n\r\n    const url = 'JSBridge://' + method + '?' + JSON.stringify(args);\r\n\r\n    // 通过 location.href 跳转\r\n    window.location.href = url;\r\n\r\n    // 通过创建新的 iframe 跳转\r\n    const iframe = document.createElement('iframe');\r\n    iframe.src = url;\r\n    iframe.style.width = 0;\r\n    iframe.style.height = 0;\r\n    document.body.appendChild(iframe);\r\n\r\n    window.setTimeout(function() {\r\n        document.body.removeChild(iframe);\r\n    }, 800);\r\n}\r\n```\r\n\r\nnative 会拦截 h5 发出的请求，当检测到协议为 JSBridge 而非普通的 http/https/file 等协议时，会拦截该请求，解析出 URL 中的 methodName、arg、 cbName，执行该方法并调用 js 回调函数。\r\n\r\n下面以安卓为例，通过覆盖 WebViewClient 类的 shouldOverrideUrlLoading 方法进行拦截，android 端具体封装会在下面单独的板块进行说明。\r\n\r\n```java\r\nimport android.util.Log;\r\nimport android.webkit.WebView;\r\nimport android.webkit.WebViewClient;\r\n\r\npublic class JSBridgeViewClient extends WebViewClient {\r\n    @Override\r\n    public boolean shouldOverrideUrlLoading(WebView view, String url) {\r\n        JSBridge.call(view, url);\r\n        return true;\r\n    }\r\n}\r\n```\r\n\r\n##### 拦截 URL Schema 的问题\r\n\r\n- 连续发送时消息丢失\r\n\r\n如下代码：\r\n\r\n```javascript\r\nwindow.location.href = \"JSBridge://callNativeNslog?{\"data\": \"111\", \"cbName\": \"\"}\";\r\nwindow.location.href = \"JSBridge://callNativeNslog?{\"data\": \"222\", \"cbName\": \"\"}\";\r\n```\r\n\r\njs 此时的诉求是在同一个运行逻辑内，快速的连续发送出2个通信请求，用客户端本身IDE的log，按顺序打印111，222，那么实际结果是222的通信消息根本收不到，直接会被系统抛弃丢掉。\r\n\r\n原因：因为h5的请求归根结底是一种模拟跳转，跳转这件事情上 webview 会有限制，当 h5 连续发送多条跳转的时候，webview会直接过滤掉后发的跳转请求，因此第二个消息根本收不到，想要收到怎么办？js 里将第二条消息延时一下。\r\n\r\n```javascript\r\n//发第一条消息\r\nlocation.href = \"JSBridge://callNativeNslog?{\"data\": \"111\", \"cbName\": \"\"}\";\r\n\r\n//延时发送第二条消息\r\nsetTimeout(500,function(){\r\n    location.href = \"JSBridge://callNativeNslog?{\"data\": \"222\", \"cbName\": \"\"}\";\r\n});\r\n```\r\n但这并不能保证此时是否有其他地方通过这种方式进行请求，为系统解决此问题，js 端可以封装一层队列，所有 js 代码调用消息都先进入队列并不立刻发送，然后 h5 会周期性比如500毫秒，清空一次队列，保证在很快的时间内绝对不会连续发2次请求通信。\r\n\r\n- URL 长度限制\r\n\r\n如果需要传输的数据较长，例如方法参数很多时，由于URL长度限制，仍以丢失部分数据。\r\n\r\n\r\n#### 2.2 拦截 prompt alert confirm\r\n\r\n即由 h5 发起 alert confirm prompt，native 通过拦截 prompt 等获取 h5 传过来的数据。\r\n\r\n因为 alert confirm 比较常用，所以一般通过 prompt 进行通信。\r\n\r\n约定的传输数据的组合方式以及 js 端封装方法的可以类似上面的 拦截 URL Schema 提到的方式。\r\n\r\n```javascript\r\nfunction callNative(methodName, arg, cb) {\r\n    ...\r\n\r\n    const url = 'JSBridge://' + method + '?' + JSON.stringify(args);\r\n\r\n    prompt(url);\r\n}\r\n```\r\n\r\nnative 会拦截 h5 发出的 prompt，当检测到协议为 JSBridge 而非普通的 http/https/file 等协议时，会拦截该请求，解析出 URL 中的 methodName、arg、 cbName，执行该方法并调用 js 回调函数。\r\n\r\n下面以安卓为例，通过覆盖 WebChromeClient 类的 onJsPrompt 方法进行拦截，android 端具体封装会在下面单独的板块进行说明。\r\n\r\n```java\r\nimport android.webkit.JsPromptResult;\r\nimport android.webkit.WebChromeClient;\r\nimport android.webkit.WebView;\r\n\r\npublic class JSBridgeChromeClient extends WebChromeClient {\r\n    @Override\r\n    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {\r\n        result.confirm(JSBridge.call(view, message));\r\n        return true;\r\n    }\r\n}\r\n```\r\n\r\n这种方式没有太大缺点，也不存在连续发送时信息丢失。不过 iOS 的 UIWebView 不支持该方式（WKWebView 支持）。\r\n\r\n\r\n#### 2.3 注入 JS 上下文\r\n\r\n即由 native 将实例对象通过 webview 提供的方法注入到 js 全局上下文，js 可以通过调用 native 的实例方法来进行通信。\r\n\r\n具体有安卓 webview 的 addJavascriptInterface，iOS UIWebview 的 JSContext，iOS WKWebview 的 scriptMessageHandler。\r\n\r\n下面以安卓 webview 的 addJavascriptInterface 为例进行讲解。\r\n\r\n首先 native 端注入实例对象到 js 全局上下文，代码大致如下，具体封装会在下面的单独板块进行讲解：\r\n\r\n```java\r\npublic class MainActivity extends AppCompatActivity {\r\n\r\n    private WebView mWebView;\r\n\r\n    @Override\r\n    protected void onCreate(Bundle savedInstanceState) {\r\n        super.onCreate(savedInstanceState);\r\n        setContentView(R.layout.activity_main);\r\n\r\n        mWebView = (WebView) findViewById(R.id.mWebView);\r\n\r\n        ...   \r\n\r\n        // 将 NativeMethods 类下面的提供给 js 的方法转换成 hashMap\r\n        JSBridge.register(\"JSBridge\", NativeMethods.class);\r\n        \r\n        // 将 JSBridge 的实例对象注入到 js 全局上下文中，名字为 _JSBridge，该实例对象下有 call 方法\r\n        mWebView.addJavascriptInterface(new JSBridge(mWebView), \"_JSBridge\");\r\n    }\r\n}\r\n\r\npublic class NativeMethods {\r\n    // 用来供 js 调用的方法\r\n    public static void methodName(WebView view, JSONObject arg, CallBack callBack) {\r\n    }\r\n}\r\n\r\npublic class JSBridge {\r\n    private WebView mWebView;\r\n\r\n    public JSBridge(WebView webView) {\r\n        this.mWebView = webView;\r\n    }\r\n\r\n\r\n    private  static Map<String, HashMap<String, Method>> exposeMethods = new HashMap<>();\r\n\r\n    // 静态方法，用于将传入的第二个参数的类下面用于提供给 javacript 的接口转成 Map，名字为第一个参数\r\n    public static void register(String exposeName, Class<?> classz) {\r\n        ...\r\n    }\r\n\r\n    // 实例方法，用于提供给 js 统一调用的方法\r\n    @JavascriptInterface\r\n    public String call(String methodName, String args) {\r\n        ...\r\n    }\r\n}\r\n```\r\n\r\n然后 h5 端可以在 js 调用 window._JSBridge 实例下面的 call 方法，传入的数据组合方式可以类似上面两种方式。具体代码如下：\r\n\r\n``` javascript\r\nwindow.callbackId = 0;\r\n\r\nfunction callNative(method, arg, cb) {\r\n    let args = {\r\n        data: arg === undefined ? null : JSON.stringify(arg),\r\n    };\r\n\r\n    if (typeof cb === 'function') {\r\n        const cbName = 'CALLBACK' + window.callbackId++;\r\n        window[cbName] = cb;\r\n        args['cbName'] = cbName;\r\n    }\r\n\r\n    if (window._JSBridge) {\r\n        window._JSBridge.call(method, JSON.stringify(args));\r\n    }\r\n}\r\n```\r\n\r\n#####  注入 JS 上下文的问题\r\n\r\n以安卓 webview 的 addJavascriptInterface 为例，在安卓 4.2 版本之前，js 可以利用 java 的反射 Reflection API，取得构造该实例对象的类的內部信息，并能直接操作该对象的内部属性及方法，这种方式会造成安全隐患，例如如果加载了外部网页，该网页的恶意 js 脚本可以获取手机的存储卡上的信息。\r\n\r\n在安卓 4.2 版本后，可以通过在提供给 js 调用的 java 方法前加装饰器 @JavascriptInterface，来表明仅该方法可以被 js 调用。\r\n\r\n\r\n## 3. 安卓端 java 的封装\r\n\r\n待续。。。"}}
other: {"actor":{"display_login":"mcuking"}}