概念和应用
通信桥梁:JS Bridge 充当了 Web 应用和客户端之间的通信桥梁。通过 JSBridge,我们可以在 web 和客户端之间进行双向通信,使这两者能够互相调用和传递数据 功能应用:使用JS Bridge,我们可以在 JavaScript 中调用客户端中的功能。如打开相机、录音、获取客户端信息等
框架设计
- 调用/输出规范化
- 事件系统:客户端主动和web通讯
- 模块化:api检测/底层call/客户端相关/其它
- 跨平台: IOS Android PC
- 客户端接口设计:原子性、多版本
- SDK接口设计:规范、友好、解耦
- 安全性:权限、代码安全
- 数据监控
通道规范
这里的规范包含web传递给客户端的json设计,也包含客户端回调给web的数据结构,即下文的入参和出参规范
入参规范
- method, 要调用的方法名
- params, 传递的参数
- callback,客户端回调的方法名。这里需要告诉客户端一个window上的函数名,客户端会 evaluateJavascript('window.callbackXXX("json")'),将回调以这样的形式传递给web
const json = {
method: 'xx', // 和客户端约定的方法名
params: {}, // 传递给客户端的参数
callback: 'randomFunctionName_xxx' // 回调函数名
}
出参规范
- callstatus, 客户端执行状态, ok | fail
- result: 执行成功,客户端返回的数据。失败时为null
- err: 执行失败,客户端的错误信息.成功时为null
- extra: 更详细的错误信息
// 成功返回
const successJson = {
callstatus: 'ok', // 调用状态:客户端执行接口成功
result: {
xxx: ''
}
err: ''
}
// 程序调用失败
const errJson: {
callstatus: 'fail',
result: '',
err: 'method undefined' // 客户端执行接口错误信息
extra: '' // 更详细的错误,上报用
}
底层调用
客户端会注入一个函数在window下
- IOS一般为window.webkit.messageHandlers.xxxx
- Android一般为 window.xxxx.invoke
- Window C++的程序就比较多样,可要求客户端同事提供 window.xxxx
// 1.调用
const json = {
method: 'openUrl',
params: {
url:'https://xxxxx.cn'
},
callback:'open_url_12345677'
}
// 由客户端提供
const queryKey = 'xxx_web_query';
// IOS
window.webkit.messageHandlers[queryKey].postMessage(json);
// android
window[queryKey].invoke(JSON.stringify(json));
// PC C++ or 其它
window[queryKey](json);
// 2.输出
// 客户端需要将回调事件invoke到callback name上,才能让前端异步获得结果
window.[open_url_12345677] = function(response){
if(response.callstatus !== 'ok'){
// 异常处理
throw new Error(body.err);
}
// 成功
return body.result;
}
代码实现片段
底层invoke call设计片段
const agent = window.navigator.userAgent.toLowerCase();
const isAndroid = /Android/i.test(agent);
const isIos = /iPhone|iPod|iPad/i.test(agent);
class Invoke extends BaseClass {
private defaultQuery: string;
constructor() {
super();
this.defaultQuery = QUERY_KEY;
}
isSupport(): boolean {
return (
window.webkit?.messageHandlers?.[this.defaultQuery] !== undefined ||
window[this.defaultQuery] !== undefined;
);
}
/**
* IOS QUERY
*/
protected iosQuery(json: IInputJson) {
window.webkit.messageHandlers[this.defaultQuery].postMessage(json);
}
/**
* ANDROID QUERY
* @param json
*/
protected androidQuery(json: IInputJson) {
window[this.defaultQuery].invoke(JSON.stringify(json));
}
/**
* 最底层 exec json方法
* @param json
* @param cb
*/
private execJson(json: IInputJson, cb: IExecCallBack) {
try {
// IOS移动端
if (isIos) {
return this.iosQuery(json);
}
// android端
if (isAndroid) {
return this.androidQuery(json);
}
return cb({ err: 'client_not_support', message: JSON.stringify(json) });
} catch (ex: any) {
cb({
err: 'client_exec_error',
message: `${ex.message}, params: ${JSON.stringify(json)}`,
});
}
}
/**
* exec
* @param method
* @param params
* @param cb
* @returns
*/
private execWithCallback(method: string, params: any, cb: IExecCallBack) {
// 嵌套性回调函数,所有回调fn都放在__client_api__下
const innerCallback = genRandomCallbackName(method);
const json = {
method,
params,
callback: `${WINDOW_OBJ_NAME}.${innerCallback}`,
version: this.version,
};
if (!window[WINDOW_OBJ_NAME]) {
window[WINDOW_OBJ_NAME] = {};
}
window[WINDOW_OBJ_NAME][innerCallback] = (res: any) => {
// 支持业务方自己处理response
delete window[WINDOW_OBJ_NAME][innerCallback];
callbackHandle(res, cb);
};
return this.execJson(json, cb);
}
/**
* promise exec
* @param method
* @param params
* @returns
*/
public call(method: string, params?: Record<string, any>, rawCallback?: any): Promise<any> {
return new Promise((resolve, reject) => {
this.execWithCallback(method, params || {}, (err, result) => {
err ? reject(err) : resolve(result);
}, rawCallback);
});
}
}
api检测模块
该模块必须有,提供给web业务方使用时,先检测是否支持需要调用的api
api.check检测接口
// 判断当前客户端版本是否支持指定JS接口 check()
const json = {
method: 'api.check',
params: {
apis: ['system.clipboard.write', 'system.voice.startRecord']
},
callback:'api_check_xxxxx'
}
// 返回
{
callstatus:'ok',
result:{
'system.clipboard.write':true,
'system.voice.startRecord':false
}
}
api.list获取支持列表
// 返回支持的 api列表
const json = {
method: 'api.list',
callback:'api_list_xxxxx'
}
// 返回
{
callstatus:'ok',
result: ['api.check', 'api.list', 'system.voice.startRecord','system.clipboard.write']
}
client客户端
客户端相关的模块,必须要有客户端信息的接口,其它的接口根据业务需要自行添加即可
getClientInfo 获取客户端信息
// 1.获取客户端信息getClientInfo()
const json = {
method: 'client.getClientInfo',
callback:'client_getClientInfo_xxxxx'
}
// 返回
{
callstatus:'ok',
result: {
version:'10.0.2434.44', // 客户端外发的版本
appName:'', // 客户端名称
channel: '', // 客户端渠道
}
}
event 事件(重要)
在jsbridge的设计里面,除开web主动调用客户端功能外,还必须设计客户端主动调用web的事件系统
- 事件系统的应用场景:用户状态变更或其它一些客户端信息变更需要通知web的场景
- 事件需要注册,客户端才会主动回调,避免回调到前端出现function undefined的情况。所以将js bridge的event事件设计成类型js的事件系统。web业务方需要的时候再去监听,不需要时需要自己手动移除监听
注册事件
// 注册监听的事件 event.addEventListener
json = {
method:'event.addEventListener',
params:{
eventName: 'event.system.voice.onRecording',
// 调用频率,约定成默认值
callback: '__client_api_.xxx_xxxx'
}
}
// 监听事件成功
{
callstatus:'ok',
result: true
}
移除注册事件
// 移除监听的事件 event.removeEventListener
json = {
method:'event.removeEventListener',
params: {
eventName: 'event.system.voice.onRecording'
}
}
// 移除监听事件成功
{
callstatus:'ok',
result: true
}
调用示例
import {event} from 'js-bridge';
componentDidMount() {
event.addEventListener('client.userchange', () => {
// TODO
});
}
componentWillUnmount() {
event.removeEventListener('client.userchange');
}
代码实现片段
/*
* 事件注册和管理服务
*
*/
import execService, { genRandomCallbackName, callbackHandle } from './exec';
import { WINDOW_OBJ_NAME, METHODS } from './../constant';
class EventService {
private mapping: any;
constructor() {
this.mapping = {};
}
/**
* 增加监听事件
* @param eventName 事件名
* @param callback 回调函数
*/
addEventListener(eventName, cb: Function) {
let innerCallback = genRandomCallbackName(eventName);
this.mapping[eventName] = innerCallback;
window[WINDOW_OBJ_NAME][innerCallback] = (res) => {
return callbackHandle(res, cb);
};
return execService.execPromise(METHODS.EVENT.ADD_EVENT_LISTENER,
{
eventName,
callback: `${WINDOW_OBJ_NAME}.${innerCallback}`,
callbackName: `${WINDOW_OBJ_NAME}.${innerCallback}` // TODO: 暂时保留
});
}
/**
* 移除事件监听
* @param eventName 事件名
*/
removeEventListener(eventName) {
execService.execPromise(METHODS.EVENT.REMOVE_EVENT_LISTENER, { eventName });
let innerCallback = this.mapping[eventName];
if (innerCallback) {
delete window[WINDOW_OBJ_NAME][innerCallback];
delete this.mapping[eventName];
}
}
}
export default new EventService();
错误码
错误码 | 解释 |
---|---|
method undefined | 方法不存在 |
permission deny | 没有权限 |
版本升级原则
客户端
- 接口实现保持原子性
- 保留多版本
- 同一个接口,小改动,不影响业务方,可以在原接口上修改
- 同一个接口,大改动,需要新增接口。为了避免代码冗肿,监控数据会告诉客户端代码删除的时间节点
web调用方
- 做多版本接口的兼容代码检测
- 不支持的接口调用,业务方做不支持提示
权限
需要服务端接入,内部使用可忽略
- 白名单
- 接口权限管理
- 调用频次
- 接口鉴权
监控
埋点上报,监控sdk分布和调用情况
- 调用方
- 前端版本
- 客户端环境
- 接口
- 成功失败
- 耗时(效率监控)
字段 | 解释 | 枚举 |
---|---|---|
action | 事件 | 业务方 |
client | 端名称 | userAgent |
js_version | 前端版本 | 1.0.2 |
api_method | 调用接口 | xxxx |
api_params | 接口查询参数 | xxx |
api_result | 调用结果 | true/false |
api_cost | 接口耗时 | 毫秒为单位 |
extra | 其它 |
踩坑
并发回调效率
- 原因:客户端并发回调, 由于JS引擎单线程导致IO并发效率,正常单次回调10ms内,并发可能会存在几百ms的耗时
- 方案:js单线程无法避开,可尝试patch call批量调用客户端接口
数据格式
- 支持string,布尔值,对象,数组
- 不自持二进制数据格式
通道体积限制
涉及大体积的数据传输,需要设计分页,而且性能也会差
- Android 20M
- IOS 6M
- PC C++ ?
iframe嵌套
- 同域下:window.parent, 找到top。 需注意内存泄漏问题
- 不同域:不同套代码,屏蔽;同套代码,window.top 承接回调,用postMessage做分发