前言
随着业务越来越复杂,App应用越来越大,变成了巨石应用,对于项目的开发、部署、维护都是一个挑战,所以想借鉴于hybrid的方式,将应用进行拆分,变成一个个h5应用,然后通过web-view组件将h5应用嵌入进来,类似于iframe实现微前端的方式,这样做的好处是改造的成本不大,又能实现拆分应用的目的,缺点是h5和App之间的通讯就不是那么得心应手。
我们项目为了实现跨端,采用的是uni-app开发App,所以这边就举一个封装h5调用uni-app开发App的原生方法工具函数,封装的结果是可以实现调用函数后返回一个Promise,达到双向通讯的效果。即使你使用的技术栈站不一致,但是核心逻辑是一致。
App端
App端主要做的是接收h5发过来的消息,将string类型的函数名称到真正函数的映射,函数执行完成后,将函数的执行结果发送到h5端,实现数据的双向传递,接收消息是通过监听web-view
的message方法,发送数据是通过 webView.evalJS
方法执行h5挂载到window上的全局方法,即如下的invokeResolve
和invokeReject
,这两个方法是h5端那边promise的resolve和reject方法,函数类型映射则是借助于正则,当然这一点不使用正则一样可以实现,看自己的喜好。
App端工具函数封装
// utils/index.js
/**
* 解析函数名称
* @param {string} fnName 函数名称
* @returns function
*/
const parseNativeFn = (fnName) => {
// uni-app的原生方法有uni.xxx 和 plus.xxx.xxx
// 定义正则校验和读取对应函数
const uniReg = /^uni\.(.+)/;
const plusReg = /^plus\.(.+)/;
// 原生方法名称
let nativeFn = null;
// 当前函数对应的正则
let reg = null;
// 校验函数是否为uni.xxx或者plus.xxx
// 并且赋值对应的全局对象uni或plus,需要手动赋值默认对象,才能根据key值获取到对应的函数
if (uniReg.test(fnName)) {
nativeFn = uni;
reg = uniReg;
} else if (plusReg.test(fnName)) {
nativeFn = plus;
reg = plusReg;
} else {
return;
}
// 正则读取uni.xxx或plus.xxx的xxx属性,因为uni和plus在上面已经赋值为全局对象了
fnName = fnName.match(reg)[1];
// 需要遍历的原因是函数不仅仅只有两级,可能存在多级
// 有可能为plus.device.uuid等
const fnNameArr = fnName.split('.');
for (const key of fnNameArr) {
nativeFn = nativeFn[key];
}
// 最终会将字符串转成原生函数
return nativeFn;
};
// 执行原生插件
/**
* 解析函数名称
* @param {string} fnName 函数名称
*/
export const invokeNative = (data, scope) => {
const currentWebview = scope.$getAppWebview();
setTimeout(() => {
// 获取uni-app webview示例的方式
const webView = currentWebview.children()[0];
const { fn, params } = data;
// 判断传递的函数为空
if (!fn) {
webView.evalJS(`invokeReject('请传入函数名称!')`);
return;
}
const nativeFn = parseNativeFn(fn);
// 不是以uni或plus开头或者对应函数不存在都会走到这里
if (!nativeFn) {
webView.evalJS(`invokeReject('${fn}该方法不存在!')`);
return;
}
if (typeof nativeFn === 'function') {
// 如果是函数,都会有success, fail,执行h5那边Promise的resolve和reject
nativeFn({
...params,
success: function (res) {
webView.evalJS(`invokeResolve(${JSON.stringify(res)})`);
},
fail: function (err) {
webView.evalJS(`invokeReject(${JSON.stringify(err)})`);
},
});
} else {
// 有些原生方法仅仅只是返回值,例如plus.device.uuid的结果为id,直接执行h5那边Promise的resolve方法,返回对应id
webView.evalJS(`invokeResolve('${nativeFn}')`);
}
});
};
App端工具函数使用
// src/index/index.vue
<template>
<web-view src="http://172.0.0.1:8080" @message="handleMessage"></web-view>
</template>
<script>
import { invokeNative } from '@/utils'
export default {
methods: {
// 接收h5传递过来消息的监听函数
handleMessage(e) {
// 获取接受参数
const data = e.detail.data[0];
const { type } = data;
// type 区分消息类型
if (type === 'invoke') {
// 执行对应函数
invokeNative(data, this.$scope);
}
},
},
};
</script>
H5端
h5端主要做的就是等待UniAppJSBridgeReady
执行完成,这个是uni-app的发送消息的条件,其他JSDK也是有类似的ready函数,然后将需要执行的方法发送到App端,同时返回一个Promise,并将Promise的resolve、reject方法挂载到window上,等待App端执行,实现调用一个方法,能实现数据的双向传递,并且能返回一个Promise,同时借助于闭包,实现一个单例模式,避免NativeInstance
被多次实例化。
H5端工具函数封装
// src/utils/index.js
class NativeInstance {
constructor() {
// h5 向uni-app发送消息,需要等到UniAppJSBridgeReady执行完成才可以
this.readyPromise = new Promise((resolve) => {
document.addEventListener('UniAppJSBridgeReady', function () {
resolve();
});
});
}
async invoke(data) {
// 执行原生方法时做一个等待
await this.readyPromise;
data = {
type: 'invoke',
...data,
};
// 发送消息
webUni.postMessage(
{
data,
},
'*',
);
return new Promise((resolve, reject) => {
// 将Promise的resoleve, reject赋值给invokeResolve、invokeReject
// App同过桥的方式会执行这两个方法
window.invokeResolve = resolve;
window.invokeReject = reject;
});
}
}
let instance = null;
// 这边借助闭包的方式,实现一个单例模式,避免当前文件被多次调用,
// 实例化多次NativeInstance,UniAppJSBridgeReady方法监听多次
export default () => {
if (!instance) {
instance = new NativeInstance();
}
return instance;
};
h5端使用
<template>
<div class="page">
<button @click="handleInvokeUni">调用uni函数</button>
<button @click="handleInvokePlus">调用plus函数</button>
</div>
</template>
<script>
import getNativeInstance from '@/utils'
export default {
methods: {
// 调用uni.xxx方法
handleInvokeUni() {
getNativeInstance()
.invoke({
fn: 'uni.getLocation', // 函数名称
params: { // 参数
type: 'xxx',
},
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
},
// 调用plus.xxx.xxx方法
handleInvokePlus() {
getNativeInstance()
.invoke({
fn: 'plus.device.uuid'
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
},
}
}
</script>
小结
这边虽然是列举了封装h5调用App端原生方法工具函数
的示例,但是想阐述的核心是如何简单的实现数据双向传递,并返回一个Promise,便于使用。这种逻辑还是有较多的使用场景,比如h5调用electron实现客户端的方法
、iframe实现微前端时调用基座方法
等等。