为了应对一些经常变更或者时效性短的页面开发,工作中往往会使用原生app里面嵌套前端h5页面的快速开发方式,这就不可避免的需要从原生app拿到相关数据,或者调用原生的页面或者方法; 中间需要用到JavascriptBridge
先讲ios端的webview;有两个可以选择 ①UIWebView ios端先通过JSContext获取js上下文,然后通过这个上下文,进行 OC & JS 的双端交互。
_jsContext =
[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
_jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"%@",@"获取 WebView JS 执行环境失败了!");
};
② WKWebView。 通过 userContentController 把需要观察的 JS 执行函数注册起来然后通过一个协议方法,将所有注册过的 JS 函数执行的参数传递到此协议方法中。 注册需要执行的函数
[webView.configuration.userContentController addScriptMessageHandler:self name:@"jsFunc"];
在js中就可以调用了
window.webkit.messageHandlers.jsFunc.postMessage({name : "李四",age : 22});
然后引出这边的主角 WebViewJavaScriptBridge
WebViewJavaScriptBridge 用于 WKWebView & UIWebView 中 OC 和 JS 交互。
它的基本原理是:
- 把 OC 的方法注册到桥梁中,让 JS 去调用。
- 把 JS 的方法注册在桥梁中,让 OC 去调用。
第一步先使用注册
oc端 略 js端 如下
function setupWebViewJavascriptBridge(callback) {
// WebViewJavaScriptBridge用于 WKWebView & UIWebView 中 OC 和 JS 交互。
if (window.WebViewJavascriptBridge) {
return callback(WebViewJavascriptBridge);
}
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback);
}
// 创建一个 WVJBCallbacks 全局属性数组,并将 callback 插入到数组中。
window.WVJBCallbacks = [callback];
// 创建一个iframe 元素
const WVJBIframe = document.createElement('iframe');
// 该元素不显示
WVJBIframe.style.display = 'none';
// 设置 iframe 的 src 属性
WVJBIframe.src = 'https://__bridge_loaded__';
// 把 iframe 添加到当前文导航上
document.documentElement.appendChild(WVJBIframe);
// 主线程执行完后再移除
setTimeout(() => { document.documentElement.removeChild(WVJBIframe) }, 0)
}
第二步 oc端和js端分别都想WebViewJavaScriptBridge注入方法
oc端
[_jsBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"dataFrom JS : %@",data[@"data"]);
responseCallback(@"扫描结果 : www.baidu.com");
}];
- scanClick 是 OC block 的一个别名。
- block 本身,是 JS 通过某种方式调用到 scanClick 的时候,执行的代码块。
- data ,由于 OC 这端由 JS 调用,所以 data 是 JS 端传递过来的数据。
- responseCallback OC 端的 block 执行完毕之后,往 JS 端传递的数据。
js端
// 这里主要是注册 OC 将要调用的 JS 方法。
setupWebViewJavascriptBridge(function(bridge){
// 声明 OC 需要调用的 JS 方法。
bridge.registerHanlder('testJavaScriptFunction',function(data,responseCallback){
// data 是 OC 传递过来的数据.
// responseCallback 是 JS 调用完毕之后传递给 OC 的数据
alert("JS 被 OC 调用了.");
responseCallback({data: "js 的数据",from : "JS"});
})
});
示例,客户端提供下面几个api
const api = {
// 拿取客户端用户信息
getUserInfo: {
version: '1.1.8',
successCallback: 'callback',
failCallback: null
},
// 关闭网页
closeWebView: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
// 跳转原生页面
jump: {
version: '4.2.8',
successCallback: null,
failCallback: null
}
}
我们比方获取客户端用户信息接口,oc端注册好后将注册的方法名告诉给我们了, 这时,我们就需要先function 一个它setupWebViewJavascriptBridge, 拿到WebViewJavaScriptBridge对象;
然后用WebViewJavaScriptBridge.callHandler('getUserInfo',{uid: 'xx', sid: 'xxxxx'}),调用oc端的注册的getUserInfo方法,并且传递参数;
这时我们需要承接oc端调用后的返回值,那只能通过oc端成功后,调用js的方法,把返回值放到函数参数中返回来;所以需要js端也向WebViewJavaScriptBridge中注册一个方法,以供oc端调用
WebViewJavaScriptBridge.registerHandler('jsFunForOcCallback', data => { console.log('oc端方式被调用成功后返回给js的值', data) })
如果还需要传递失败的函数,那就要再继续注册一个失败的js函数供客户端调用
** 以上是js端调用oc端信息 **
还有oc端直接调用js端的,这时js端就只需要注册一个函数,供oc端调用即可
另外复制上我们项目对ios 安卓的一套注册
/**
* 贪吃蛇大作战JSBridge
* wiki地址: http://wiki.17qq.me/pages/viewpage.action?pageId=590045
*/
import { Toast } from '../comps'
import { compareVersion } from '../utils/util'
import events from '../events'
const isIOSNewWebview = !navigator.userAgent.match(/Android/i)
function checkTCSApp () {
return new Promise((resolve, reject) => {
if (window.Tcsdzz) {
return resolve()
}
if (window.notInTcsdzz) {
return reject(new Error('请在******内打开'))
}
let count = 0
const timer = setInterval(() => {
if (window.Tcsdzz) {
clearInterval(timer)
resolve()
} else {
count++
if (count > 20) {
clearInterval(timer)
window.notInTcsdzz = true
reject(new Error('请在贪吃蛇大作战内打开'))
}
}
}, 100)
})
}
function initWKBridge () {
return new Promise(resolve => {
if (window.WebViewJavascriptBridge) {
return resolve(window.WebViewJavascriptBridge)
}
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(resolve)
}
window.WVJBCallbacks = [resolve]
const WVJBIframe = document.createElement('iframe')
WVJBIframe.style.display = 'none'
WVJBIframe.src = 'https://__bridge_loaded__'
document.documentElement.appendChild(WVJBIframe)
setTimeout(() => { document.documentElement.removeChild(WVJBIframe) }, 0)
})
}
const api = {
certifySuccess: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
closeWebView: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
jump: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
track: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
notifyRedDot: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
alertAward: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
share: {
version: '4.2.8',
successCallback: 'successCallback',
failCallback: 'cancelCallback'
},
checkAd: {
version: '4.2.8',
successCallback: 'adCheckCallback',
failCallback: null
},
showAd: {
version: '4.2.8',
successCallback: 'adShowCallback',
failCallback: null
},
canWatchAd: {
version: '4.2.8',
successCallback: 'hasAdCheckCallback',
failCallback: null
},
getDisplayCutout: {
version: '4.2.8',
successCallback: 'jsGetDisplayCutoutCallback',
failCallback: null
},
httpPost: {
version: '4.2.8',
successCallback: 'httpCallback',
failCallback: null
},
getUserInfo: {
version: '4.2.8',
successCallback: 'callback',
failCallback: null
},
buyProduct: {
version: '4.2.8',
successCallback: 'buyCallback',
failCallback: null
},
switchAccount: {
version: '4.3.18',
successCallback: null,
failCallback: null
},
unzipLottiePack: {
version: '4.2.11',
successCallback: 'successCallback',
failCallback: null
},
getCardInfo: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
getTimeDiff: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
editInvitationCard: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
inviteFriend: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
playMusic: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
enterRoom: {
version: '4.2.8',
successCallback: null,
failCallback: null
},
viewUserDetail: {
version: '4.2.8',
successCallback: null,
failCallback: null
}
}
window.refreshWebFromNative = function () {
events.$emit('tcs:page_refuse')
}
window.updateWord = function (content) {
events.$emit('onEditContent', content)
}
if (isIOSNewWebview) {
initWKBridge().then(bridge => {
bridge.registerHandler('refreshWebFromNative', () => {
events.$emit('tcs:page_refuse')
})
bridge.registerHandler('updateWord', ([content]) => {
events.$emit('onEditContent', content)
})
})
}
export default {
methods: {
$tcs (method, params) {
const config = api[method] || null
if (!config) {
throw new Error(`Tcsdzz.${method} is undefined`, '方法不存在')
}
if (config.version !== '4.2.8' && compareVersion(config.version) === 1) {
Toast('请升级到最新版本', 5000)
return Promise.reject(new Error('请升级到最新版本'))
}
if (isIOSNewWebview) {
if (method === 'getCardInfo' || method === 'getTimeDiff') {
return new Promise((resolve) => {
initWKBridge().then(bridge => {
console.log(`callHandler ${method}`, params)
bridge.callHandler(method, params, resolve)
}).catch(err => {
Toast(err.message)
})
})
}
return new Promise((resolve, reject) => {
initWKBridge().then(bridge => {
if (config.successCallback) {
const successCallbackName = `${method}_success_${Date.now()}_${Math.floor(Math.random() * 100)}`
bridge.registerHandler(successCallbackName, data => {
console.log(`recv => ${method}`, data)
resolve(data[0])
})
params[config.successCallback] = successCallbackName
}
if (config.failCallback) {
const failCallbackName = `${method}_fail_${Date.now()}_${Math.floor(Math.random() * 100)}`
bridge.registerHandler(failCallbackName, reject)
params[config.successCallback] = failCallbackName
}
console.log(`callHandler ${method}`, params)
if (method === 'playMusic') {
bridge.callHandler(method, { isPlay: params })
return
}
if (method === 'viewUserDetail') {
bridge.callHandler(method, { uid: params })
return
}
bridge.callHandler(method, params)
}).catch(err => {
Toast(err.message)
})
})
} else if (config.successCallback || config.failCallback) {
return new Promise((resolve, reject) => {
checkTCSApp().then(() => {
if (config.successCallback) {
const successCallbackName = `${method}_success_${Date.now()}_${Math.floor(Math.random() * 100)}`
console.log(successCallbackName)
window[successCallbackName] = params => {
console.log(`recv => ${method}`)
resolve(params)
}
params[config.successCallback] = successCallbackName
}
if (config.failCallback) {
const failCallbackName = `${method}_fail_${Date.now()}_${Math.floor(Math.random() * 100)}`
window[failCallbackName] = reject
params[config.successCallback] = failCallbackName
}
console.log(`Tcsdzz.${method}`, params)
window.Tcsdzz[method](JSON.stringify(params))
}).catch(reject)
})
} else {
return new Promise((resolve, reject) => {
checkTCSApp().then(() => {
if (method === 'viewUserDetail' || method === 'playMusic') {
resolve(window.Tcsdzz[method](params))
return
}
resolve(params ? window.Tcsdzz[method](JSON.stringify(params)) : window.Tcsdzz[method]())
}).catch(reject)
})
}
}
}
}