项目地址:vue-webview-js-bridge

主要功能:基于WebViewJavascriptBridge开发的 webview-js-bridge 的 vue 插件,后面废话较多,可以直接看项目。如果刚好闲来无事,权当打发时间

故事背景

本人前端,在一个移动办公 APP 团队。由于业务简单,并没有使用 RN,WEEX 等技术。使用 native 做壳,主要逻辑都是前端控制。主技术栈是 Vue.

bug 出现

在一段时间内,团队接到多人反馈,APP 每天都需要重新登陆。作为团队的前端我很诧异,因为登陆 token 在前端保存,有效期是三天,怎么可能会每天都要登陆呢?

排查原因

  1. token 有效期三天这个肯定没有问题
  2. 团队成员手机都没出过问题,证明逻辑没问题
  3. 收集问题反馈人手机信息,发现所有反馈人员的手机有一个共同点,储存空间都不足(ios),问题也主要集中在苹果手机

定位问题原因,可能是因为手机储存空间不足,把储存的 token 清理了。按着这个思路开始排查…
难道是手机内存空间不足,把储存的 token 当作没用的信息清理了?(token 存在 localstorage 里面)。
通过 google 发现,ios 确实存在这种情况,当手机储存空间不足时候会把系统认为无用的信息清理掉,给主要程序提供运行环境。

问题复现

找到一台储存空间不足的苹果测试机,登陆到 APP 后,找一部非常大的电影下载。这时候系统就会清理空间来满足下载电影的需求。结果发现 APP 需要重新登陆了。问题复现,原因确认。

解决问题

一个小 APP,仅仅为了储存 token 而引入 sqlite 之类的本地数据库,很有些大材小用。但是 localstorage,cookie 等能储存的地方又都在系统清理的范围内。最后决定把 token 储存在 native 端。

解决方案

  1. native 开发类似 localstorage 的功能
  2. token 存入 native 端
  3. 前端启动时查询 native 端是否存在 token,存在就同步到 localstorage,不存在,跳转登陆页面。
  4. 其他逻辑不变

方案确定,一个字,撸起袖子一顿猛干。。。

坑中有坑

难受…,就不描述经过了。直接说结论了
前端和 Native 端通信采用的是WebViewJavascriptBridge,问题就出在这里,见代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jsBridge from "./utils/native";
// 省略n行...
router.beforeEach((to, from, next) => {
if (to.meta.auth) {
// 问题出在这里,从APP启动到vue运行到这里,jsBridge实例还未初始化完成,导致jsBridge是undifined
jsBridge.callHandler("getStorage", { key: "token" }).then((res) => {
let token = res.body;
if (token && token !== "null") {
next();
} else {
next("/login");
}
store.commit("UPDATE_TOKEN", { token });
});
} else {
next();
}
});
// 省略n行...

问题已经确定,是调用 jsBridge 实例时,它还未初始化完成。感兴趣的可以去看下WebViewJavascriptBridge源码,虽然初始化时间很短,但是已经造成了问题,APP 到这里启动报错。

继续解决问题:既然未初始化完成,那我们等它完成再用不就完了。有人提议用setTimeout,但是延时多少时间呢?时间短了不一定 hold 住啊,毕竟不同手机初始化时间还是有差别的。设置长了影响体验,作为一个前端如果用户体验都不顾那还做什么前端,所以这个最容易也是最 low 的方法被 pass 了。

想一个优雅的解决办法

这就是我写的这个 package 的初衷,废话说了这么多,进入正题了

  1. 从解决问题的角度出发

既然使用 jsBridge 实例时你还没初始化完成,那我们就每次等你初始化完成,把实例回传回来后我再调用,这样就不同担心 bridge 实例不可用的问题了

核心代码

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
// ...
init (callback) {
if (window.WebViewJavascriptBridge) {
return callback(window.WebViewJavascriptBridge)
}
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback)
}
window.WVJBCallbacks = [callback]
var WVJBIframe = document.createElement('iframe')
WVJBIframe.style.display = 'none'
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'
document.documentElement.appendChild(WVJBIframe)
setTimeout(function () {
document.documentElement.removeChild(WVJBIframe)
}, 0)
}
callHandler (payload) {
let _resolve
let _reject
const readyPromise = new Promise((resolve, reject) => {
_resolve = resolve
_reject = reject
})
this.init(function (bridge) {
try {
bridge.callHandler(nativeHandlerName, payload, (response) => {
_resolve(response)
})
} catch (e) {
_reject(e)
}
})
return readyPromise
}
// ...

习惯了使用 Promise 的调用方式,顺手把调用方法 Promise 化处理。

后续

为了开发效率,不能一直在 APP 里面调试,又增加了 mock 功能,debug 功能。

项目地址:vue-webview-js-bridge 如果刚好对你还有些帮助,欢迎 star,欢迎 pr