这方面的文章很多, 做个小总结吧, 扯着扯着可能要扯出很多东西, 只为记录一些东西
有摘录, 有原创:)
H5 和原生 app(ios,android) 交互的介质基本都是基于 webview, 可以把 Webview 看作是一个性能打八折的浏览器
IOS 调用 Javascript
ios 中可以跟 js 交互的有 WKWebView UIWebViewJavaScriptCorestringByEvaluatingJavaScriptFromString... 等
WKWebView: 苹果在 iOS8 之后也引入了专门负责处理网页视图的框架 WebKit, 再说 webkit 是啥, 接触过 H5chrome 的肯定都知道 chrome 使用的也是基于 webkit 内核的 Chromium 引擎 WKWebView 优点很多, 支持更多 H5 特性, 刷新效率及内置手势等, 更加强大, 性能也更优, 不一一列举, 如果大家 app 不需要兼容 7 及以下版本, 不需要拦截一些请求, 直接解析本地一些文件, 建议使用 WKWebView
UIWebView: 较老的 H5 容器
JavaScriptCore(ios7 及以后版本)JavaScriptCore 框架是 webkit 重要组成部分, 主要是对 JS 进行解析和提供执行环境, 是 Javascript 的虚拟机, 有点类似 v8 引擎, 我自己这么理解:) 正是它为 iOS 提供了执行 JavaScript 代码的能力 ReactNative 应该都是通过 JavaScriptCore 去解析的
stringByEvaluatingJavaScriptFromString(ios7 以前版本)
iOS7 以前 OC 对 JS 的操作只有 Webview 里面一个函数, 很长是吧
JS 对 OC 的回调都是基于 URL 的拦截有很多开源库, 其中 WebViewJavascriptBridge 用的最多, 后边也会重点说到为了兼容 ios 更低版本, 这个方式使用的是最广泛的, 也算是身经百战兼容性比较好的
Javascript 调用 IOS(OCSwift) 原理:
UIWebView 的特性, 在 UIWebView 内发起的所有网络请求, 都可以在 Native 层被捕捉到
利用这一特性, 就可以在 UIWebView 内发起一个自定义的网络请求, 一般格式: jsbridge://method? 参数 1=value1 & 参数 2=value2
于是在 UIWebView 中, 只要发现是 jsbridge:// 开头的 url, 就不进行内容的加载, 而是执行相应的逻辑处理
嵌入 webview 的 h5 中的 js 一般是通过动态创建隐藏 iframe 标签, 赋值上文提到的链接给 src,iframe 不会引起页面调转刷新
主要代码:
- var src= 'jsbridge://method? 参数 1=value1 & 参数 2=value2';
- var iframe = document.createElement('iframe');
- iframe.style.display = 'none';
- iframe.src = src;
- document.body.appendChild(iframe);
- // 再删除 iframesetTimeout(function() {
- iframe.remove();
- }, 50);
Android 和 Javascript 互相调用
Android WebView 也是基于 WebKit 引擎的一个组件, Android 的 Webview 在低版本和高版本采用了不同的 webkit 版本内核, 4.4 后直接使用了 Chrome
这个组件功能非常强大, 除了具有一般 View 的属性和设置外, 还可以对 url 请求页面加载渲染页面交互进行强大的处理
Android 调用 JS 代码的方法主要有 2 种:
WebView 的 loadUrl
WebView 的 evaluateJavascript
JS 调用 Android 代码的方法主要有 3 种:
WebView 的 addJavascriptInterface 进行对象映射 (低版本 Android4 以下好像有一些安全问题, 本人没有验证)
WebViewClient 的 shouldOverrideUrlLoading 方法回调拦截 url
WebChromeClient 的 onJsAlertonJsConfirmonJsPrompt 方法回调拦截 JS 对话框 alert()confirm()prompt() 消息
一般常用 onJsPromptprompt 进行回调拦截
啰嗦了这么多, 还没说到主题, JsBridge 一句话, JSBridge 是 Native 代码与 JS 代码的通信桥梁
设计一个 jsbridge 主要分几大步骤:
第一步: 设计出一个 Native 与 JS 交互的全局中间对象
第二步: JS 如何调用 Native
第三步: Native 如何得知 api 被调用
第四步: 分析 url - 参数和回调的格式
第五步: Native 如何调用 JS
第六步: H5 中 api 方法的注册以及格式
H5 端 JS 核心代码 (转载刘贝, 当然还有很多很成熟的第三方 jsbridge 库)
- (function() { (function() {
- var hasOwnProperty = Object.prototype.hasOwnProperty;
- var JSBridge = window.JSBridge || (window.JSBridge = {});
- //jsbridge 协议定义的名称
- var CUSTOM_PROTOCOL_SCHEME = 'CustomJSBridge';
- // 最外层的 api 名称
- var API_Name = 'namespace_bridge';
- // 进行 url scheme 传值的 iframe
- var messagingIframe = document.createElement('iframe');
- messagingIframe.style.display = 'none';
- messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name;
- document.documentElement.appendChild(messagingIframe);
- // 定义的回调函数集合, 在原生调用完对应的方法后, 会执行对应的回调函数 id
- var responseCallbacks = {};
- // 唯一 id, 用来确保每一个回调函数的唯一性
- var uniqueId = 1;
- // 本地注册的方法集合, 原生只能调用本地注册的方法, 否则会提示错误
- var messageHandlers = {};
- // 当原生调用 H5 注册的方法时, 通过回调来调用 (也就是变为了异步执行, 加强安全性)
- var dispatchMessagesWithTimeoutSafety = true;
- // 本地运行中的方法队列
- var sendMessageQueue = [];
- // 实际暴露给原生调用的对象
- var Inner = {
- /**
- * @description 注册本地 JS 方法通过 JSBridge 给原生调用
- * 我们规定, 原生必须通过 JSBridge 来调用 H5 的方法
- * 注意, 这里一般对本地函数有一些要求, 要求第一个参数是 data, 第二个参数是 callback
- * @param {String} handlerName 方法名
- * @param {Function} handler 对应的方法
- */
- registerHandler: function(handlerName, handler) {
- messageHandlers[handlerName] = handler;
- },
- /**
- * @description 调用原生开放的方法
- * @param {String} handlerName 方法名
- * @param {JSON} data 参数
- * @param {Function} callback 回调函数
- */
- callHandler: function(handlerName, data, callback) {
- // 如果没有 data
- if (arguments.length == 3 && typeof data == 'function') {
- callback = data;
- data = null;
- }
- _doSend({
- handlerName: handlerName,
- data: data
- },
- callback);
- },
- /**
- * iOS 专用
- * @description 当本地调用了 callHandler 之后, 实际是调用了通用的 scheme, 通知原生
- * 然后原生通过调用这个方法来获知当前正在调用的方法队列
- */
- _fetchQueue: function() {
- var messageQueueString = JSON.stringify(sendMessageQueue);
- sendMessageQueue = [];
- return messageQueueString;
- },
- /**
- * @description 原生调用 H5 页面注册的方法, 或者调用回调方法
- * @param {String} messageJSON 对应的方法的详情, 需要手动转为 json
- */
- _handleMessageFromNative: function(messageJSON) {
- setTimeout(_doDispatchMessageFromNative);
- /**
- * @description 处理原生过来的方法
- */
- function _doDispatchMessageFromNative() {
- var message;
- try {
- message = JSON.parse(messageJSON);
- } catch(e) {
- //TODO handle the exception
- console.error("原生调用 H5 方法出错, 传入参数错误");
- return;
- }
- // 回调函数
- var responseCallback;
- if (message.responseId) {
- // 这里规定, 原生执行方法完毕后准备通知 h5 执行回调时, 回调函数 id 是 responseId
- responseCallback = responseCallbacks[message.responseId];
- if (!responseCallback) {
- return;
- }
- // 执行本地的回调函数
- responseCallback(message.responseData);
- delete responseCallbacks[message.responseId];
- } else {
- // 否则, 代表原生主动执行 h5 本地的函数
- if (message.callbackId) {
- // 先判断是否需要本地 H5 执行回调函数
- // 如果需要本地函数执行回调通知原生, 那么在本地注册回调函数, 然后再调用原生
- // 回调数据有 h5 函数执行完毕后传入
- var callbackResponseId = message.callbackId;
- responseCallback = function(responseData) {
- // 默认是调用 EJS api 上面的函数
- // 然后接下来原生知道 scheme 被调用后主动获取这个信息
- // 所以原生这时候应该会进行判断, 判断对于函数是否成功执行, 并接收数据
- // 这时候通讯完毕 (由于 h5 不会对回调添加回调, 所以接下来没有通信了)
- _doSend({
- handlerName: message.handlerName,
- responseId: callbackResponseId,
- responseData: responseData
- });
- };
- }
- // 从本地注册的函数中获取
- var handler = messageHandlers[message.handlerName];
- if (!handler) {
- // 本地没有注册这个函数
- } else {
- // 执行本地函数, 按照要求传入数据和回调
- handler(message.data, responseCallback);
- }
- }
- }
- }
- };
- /**
- * @description JS 调用原生方法前, 会先 send 到这里进行处理
- * @param {JSON} message 调用的方法详情, 包括方法名, 参数
- * @param {Function} responseCallback 调用完方法后的回调
- */
- function _doSend(message, responseCallback) {
- if (responseCallback) {
- // 取到一个唯一的 callbackid
- var callbackId = Util.getCallbackId();
- // 回调函数添加到集合中
- responseCallbacks[callbackId] = responseCallback;
- // 方法的详情添加回调函数的关键标识
- message['callbackId'] = callbackId;
- }
- var uri;
- //android 中, 可以通过 onJsPrompt 或者截取 Url 访问都行
- var ua = navigator.userAgent;
- if (ua.match(/(iPhone\sOS)\s([\d_]+)/) || ua.match(/(iPad).*OS\s([\d_]+)/)) {
- //ios 中, 通过截取客户端 url 访问
- // 因为 ios 可以不暴露 scheme, 而是由原生手动获取
- // 正在调用的方法详情添加进入消息队列中, 原生会主动获取
- sendMessageQueue.push(message);
- uri = Util.getUri();
- } else {
- //android 中兼容处理, 将所有的参数一起拼接到 url 中
- uri = Util.getUri(message);
- }
- // 获取 触发方法的 url scheme
- // 采用 iframe 跳转 scheme 的方法
- messagingIframe.src = uri;
- }
- var Util = {
- getCallbackId: function() {
- // 如果无法解析端口, 可以换为 Math.floor(Math.random() * (1 << 30));
- return 'cb_' + (uniqueId++) + '_' + new Date().getTime();
- },
- // 获取 url scheme
- // 第二个参数是兼容 android 中的做法
- //android 中由于原生不能获取 JS 函数的返回值, 所以得通过协议传输
- getUri: function(message) {
- var uri = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name;
- if (message) {
- // 回调 id 作为端口存在
- var callbackId, method, params;
- if (message.callbackId) {
- // 第一种: h5 主动调用原生
- callbackId = message.callbackId;
- method = message.handlerName;
- params = message.data;
- } else if (message.responseId) {
- // 第二种: 原生调用 h5 后, h5 回调
- // 这种情况下需要原生自行分析传过去的 port 是否是它定义的回调
- callbackId = message.responseId;
- method = message.handlerName;
- params = message.responseData;
- }
- // 参数转为字符串
- params = this.getParam(params);
- //uri 补充
- uri += ':' + callbackId + '/' + method + '?' + params;
- }
- return uri;
- },
- getParam: function(obj) {
- if (obj && typeof obj === 'object') {
- return JSON.stringify(obj);
- } else {
- return obj || '';
- }
- }
- };
- for (var key in Inner) {
- if (!hasOwnProperty.call(JSBridge, key)) {
- JSBridge[key] = Inner[key];
- }
- }
- })();
- // 注册一个测试函数
- JSBridge.registerHandler('testH5Func',
- function(data, callback) {
- alert('测试函数接收到数据:' + JSON.stringify(data));
- callback && callback('测试回传数据...');
- });
- /*
- ***************************API********************************************
- * 开放给外界调用的 api
- * */
- window.jsapi = {};
- /**
- ***app 模块
- * 一些特殊操作
- */
- jsapi.app = {
- /**
- * @description 测试函数
- */
- testNativeFunc: function() {
- // 调用一个测试函数
- JSBridge.callHandler('testNativeFunc', {},
- function(res) {
- callback && callback(res);
- });
- }
- };
- })();
来源: https://juejin.im/post/5abb0eec6fb9a028e46eae67