概念

首先需要知道Hybrid开发,即NativeWeb混合开发的一种技术。

优缺点对比

特性 web native
灵活性 无需发版,灵活 需要发版,不灵活
功能 不能使用 native 接口 可以使用所有 native 接口
兼容性 iosandroid 使用一套代码 需要开发两套代码

混合开发的意义就是结合两者的优点,摒弃缺点。需要快速迭代的需求可以使用前端进行开发,已经稳定的功能可以使用客户端进行开发

js bridge是实现客户端和前端数据通信的技术

封装

功能

主要功能

  1. 前端同步调用客户端方法
  2. 前端异步调用客户端方法,需要传callback
  3. 客户端同步调用前端方法,对前端来说相当于事件

辅助功能

  1. 判断当前环境是浏览器还是App,浏览器环境无App方法。不能通过userAgent进行判断,浏览器自带的手机模拟器的userAgent值也是native。判断前端是否存在客户端注入的方法是可行的
  2. 是否开启 debug模式,会在控制台打印调用客户端方法的细节
  3. 判断客户端是否存在某个方法
  4. 实现EventEmitter用来管理事件

实现

前端同步调用客户端

最简单的方式就是客户端把方法fn注册到window对象,然后前端直接调用window.fn()去触发。

以上方法会存在一些问题

  1. 污染全局变量,如果想在window对象上添加同名函数,会覆盖客户端方法
  2. 不好进行管理

比较好的方法是将客户端注入的方法统一放到window对象的属性中,例如window.nativeApi = {},

前端调用客户端某个方法window.nativeApi.foo()

前端异步调用客户端

异步调用其实是由**两次单向(前端先调用客户端,然后客户端在调用前端)**调用组成

前端是无法直接将异步回调当作参数传给客户端并让其执行的,异步回调只能在前端执行

目前可行的做法是在前端创建一个map用来存储前端的callbackkeycallbackIdvaluecallback。前端调用客户端方法时将callbackId作为参数传过去,当客户端方法执行结束后需要执行前端回调时,调用前端的指定方法handleMessage,将这个callbackIdcallback 参数传回前端。前端在根据callbackIdmap中找到指定的callback,执行并将参数传给这个callback,这样就实现了异步调用客户端方法了

客户端同步调用前端

基本原理就是前端将方法注入到window或者window对象下的属性中,客户端在去调用这个方法。

基于上述原理可以有以下两种方式实现客户端调用前端方法

第一种就是上述原理所述方法,但是这种方法会有弊端相同方法名的函数只能注册一次,多次注册后者会覆盖前者,但是在开发中经常会遇到需要多次注册相同函数的需求。例如wakeUp事件,在页面唤醒后执行前端函数,往往很多页面都会注册wakeUp事件,这时就需要有多个wakeUp函数。第二种方法可以满足这种需求

1
2
3
4
5
6
7
8
9
10
11
12
// 在a页面注册wakeUp函数
window.wakeUp = function () {
console.log("a页面");
};

// 在b页面注册wakeUp事件
window.wakeUp = function () {
console.log("b页面");
};

// 这时b页面的wakeUp函数必然会覆盖a页面的wakeUp函数
// 可能会导致a页面出现不可预测的问题

第二种方法是基于事件驱动的,重点在前端。我们可以把客户端调用前端方法理解成前端注册了一个事件,等待客户端去触发这个事件相当于前端监听客户端的这个事件。因为前端注册方法后并不会立即执行,而是等待触发了客户端某些行为后才会触发,例如wakeUp页面唤醒事件

既然是事件,肯定可以注册多次,并且需要支持注册事件触发事件卸载事件等一系列操作,在这里使用订阅-发布者模式EventEmitter最为适合,这样也就支持了在多个页面注册相同函数(事件)的功能了

首先开发一个EventEmitter,基于它封装三个函数。分别是Native_on(type, callback)监听客户端方法;Native_off(type, callback)取消监听客户端方法;handleMessage(message)触发事件并且参数中会携带事件的信息(事件名,回调参数等等),该方法会绑定到window上由客户端进行触发。这三个方法的本质就是对EventEmitter中的队列进行维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class EventEmitter {
constructor() {
// 维护事件即事件回调
this.events = {};
}

/**
* 监听事件
* @param {string} type 事件名
* @param {() => any} callback 触发后执行的回调
*/
on(type, callback) {
if (!type) return;

if (this.events[type]) {
this.events[type].push(callback);
} else {
this.events[type] = [callback];
}
}

/**
* 卸载事件
* @param {string} type 事件名
* @param {() => any} callback 监听时的回调
*/
off(type, callback) {
if (!type) return;

if (this.events[type]) {
this.events[type] = this.events[type].filter((cb) => cb !== callback);
} else {
// 事件不存在
}
}

/**
* 触发事件
* @param {string} type 事件名
*/
emit(type) {
if (!type) return;

if (this.events[type]) {
this.events[type].forEach((cb) => {
typeof cb === "function" && cb();
});
} else {
// 事件不存在
}
}
}

// 实例化一个event
const events = new EventEmitter();

function Native_on(type, callback) {
events.on(type, callback);
}

function Native_off(type, callback) {
events.off(type, callback);
}

function handleMessage(message) {
const { type } = message;
events.emit(type);
}

// 注册到window上,等待客户端调用
window.handleMessage = handleMessage;

function wakeUp() {
console.log("wakeUp");
}

// 监听事件
Native.on("wakeUp", wakeUp);

// 卸载事件
Native.off("wakeUp", wakeUp);

// 客户端触发事件
window.handleMessage({ type: "wakeUp" });

两种方法比较

特性 方法一 方法二
复杂度 不复杂,直接将前端方法注入window等待客户端调用 复杂,客户端只需要调用handleMessage方法通知前端,大部分工作是有前端完成(EventEmitter)
功能 不支持同一方法注册多个回调,否则会进行覆盖,最终只能保留一个 支持同一方法注册多个回调,同时支持卸载回调

使用哪种方法取决于是否有注册多个回调的需求,没有则选择方法一非常简单,如果有则必须使用第二种方法

通信时参数的数据结构

双端通信必须提前统一接口数据结构

handleMessage

1
2
3
4
5
6
{
"type": "", // message类型 event 或者 callback
"callback_id": "", // 异步回调的callback_id
"event": "", // 事件名称
"args": "" // 传给事件回调的参数
}