1. 背景
项目需要做一个安卓app
,而且不需要上架应用市场,部门也没有安卓开发,想着就套个webview
就行了吧。没有选择react native
之类的是因为这些工具需要安装很多环境工具,我只是开发一个壳子没必要这么复杂。
用webview
也方便快速修复页面问题。
所以最后选择了uniapp
,但是uniapp
本身就是套在一个大的webview
下的, 所以再套一个webview
难免会有一些意想不到的问题,下面就是一些踩过的坑记录。
2. 项目初始化
新建项目就默认模板就行,我只需要壳子。
启动了之后可以看到有两个调试工具
第一个就是网页上常用的vue
调试工具,可以看到vue
组件属性啥的,第二个就是类似chrome
的控制台,但是无法查看元素
,还有就是必须让设备和电脑在同一个网段下才行,不然连接不上。
hbuilder
的控制台本身也有一些输出,比如页面的console
但是这里输出对象的时候不是很方便查看,如果你需要的话就打开上面说的第二个调试工具。
3. webview
使用
整个项目很简单,大概就这样一个页面
html
<template>
<web-view :src='PROJECT_PATH' @message="onMessage"></web-view>
</template>
<script>
// ...
</script>
3.1 网页与app
通信
这是最重要的一个功能,可以参考官方文档。
网页和app
交互总结起来就是这两点:
网页 -> APP
:window.uni.postMessage();
APP -> 网页
:webview.evalJS()
3.1.1. 网页 -> APP
首先要在项目中引入uni.webview.js,这个就相当于jsbridge
,可以让网页操作uniapp
。
初始化完成后会在window
上挂载一个uni
对象,通过uni.postMessage
就能往app
发送消息,app
中监听onMessage
就行。
这里有几个小坑:
- 发送的格式
window.uni.postMessage({ data: 数据 })
,必须要有个字段data
,这样app
才能收到数据。源码
2. 发送的数据不需要序列化成字符串,uniapp
会转换json
。 3. app
在message
事件中接收到事件参数应该这样解构
ts
function onMessage(e) {
const {
type,
data
} = e.detail.data[0]
}
3.1.2. APP -> 网页
app
向网页传输消息就直接调用网页的js
就行了。这里我统一封装了一个函数:
ts
// app向网页发送消息
const deliverMessage = (msg) => {
// 调用webview中的deliverMessage函数
// 这个函数是我在网页挂载的一个全局函数,调用deliverMessage后会触发页面中的一些事件
currentWebview.evalJS(`deliverMessage(${JSON.stringify(msg)})`)
}
上面的代码例子中出现的currentWebview
需要我们自己去获取。
ts
// vue2中
const rootWebview = this.$scope.$getAppWebview()
this.currentWebview = rootWebview.children()[0]
// vue3中
import {
getCurrentInstance,
ref,
} from "vue";
const currentWebview = ref(null)
const vueInstance = getCurrentInstance()
const rootWebview = vueInstance.proxy.$scope.$getAppWebview()
currentWebview.value = rootWebview.children()[0]
这里也有一个坑,rootWebview.children()
如果你一渲染就获取是无法获取到webview
实例的,具体原因没有深入研究,估计是异步的原因
这里提供两个思路:
- 加一个定时器,延迟获取
webview
,这个方法虽然听起来不保险,但是实际测试还是挺稳当的。关键是简单。
ts
setTimeout(() => {
currentWebview.value = rootWebview.children()[0]
}, 1000)
- 你要是觉得定时器不保险,那就使用
plus
的api
手动创建webview
。但是消息处理这块比较麻烦。官网参考
html
<template>
<!-- 这里面就不需要webview组件了 -->
</template>
ts
// 我这里vue3为例
onMounted(() => {
plus.globalEvent.addEventListener('plusMessage', ({data: {type, args}}) => {
// 是网页调用uni的api
if(type === 'WEB_INVOKE_APPSERVICE') {
const {data: {name, arg}} = args
// 是发送消息事件
if(name === 'postMessage') {
// arg就是传过来的数据
}
}
})
const wv = plus.webview.create("", "webview", {
'uni-app': 'none',
})
wv.loadURL(网页地址)
rootWebview.append(wv);
})
plus.globalEvent.addEventListener
这个是翻源码找到的,主要是我不想改uni.webview.js
的源码,所以只有找到正确的监听事件。
WEB_INVOKE_APPSERVICE
是uniapp
内部定义的一个名字,反正就是用来交互操作的命名空间。
这样基础的互操作就有了。
3.1.3. 整个流程
网页
调用window.uni.postMessage({ data })
=>app
监听(用组件的onMessage
或者自定义的globalEvent
)app
调用网页
定义的函数deliverMessage
并传递参数,网页
中的deliverMessage
内部处理监听
ts
// 网页中的deliverMessage
window.deliverMessage = (msg) => {
// 触发网页注册的监听器
eventListeners.forEach((listener) => {
});
};
3.2. 返回拦截
默认情况下,手机按下返回键,app
会响应提示是否退出,但是实际我需要网页进入二级路由的时候,按下手机返回键是返回上一级路由而不是退出。当路由是一级路由时才提示是否退出app
ts
import {
onBackPress,
onShow,
} from '@dcloudio/uni-app'
// 页面当前的路由信息
const pageRoute = shallowRef()
onBackPress(() => {
// tab页正常app返回逻辑
if (pageRoute.value?.isTab) {
return false
} else {
// 二级路由拦截app返回
return true
}
})
pageRoute
是页面当前路由信息,页面通过监听路由变化触发routeChange
事件,将路由信息传给app
。当按下返回键的时候,判断当前路由配置是不是tab
页,如果是就正常退出,不是就拦截返回。
4. 总结
有了通信功能,很多操作就可以实现了,比如获取设备safeArea
,获取设备联网状态等等。