三分钟手写JSBridge让Vue3 + TS 项目拥有调用鸿蒙原生能力
前言
相信大家能够认同鸿蒙的出现确实增加了很多就业机会,也有很多前端开发亦或是安卓、ios开发转了鸿蒙开发,根据行业调研,纯原生鸿蒙开发占比可能不足三成 ,剩下的肯定更多的是混合开发、直接用h5 页面在原生用webview 套壳,也确实大部分公司不需要追求极致的原生丝滑体验,只要能功能满足用户,而且还不用花费那么多成本去维护跨平台项目,h5是成本最低的方式,所以大多鸿蒙开发就要面临的是如何向前端项目提供鸿蒙原生能力以便前端直接通过js 调用。
一、官方例子
1.1官方提供的 H5 调用鸿蒙函数的方法:
developer.huawei.com/consumer/cn...
可以看到其实鸿蒙做的非常好,他基本上是将你在鸿蒙中封装的对象直接注入到webView 中,这样在H5 页面直接可以在全局调用这个对象即可调用原生的能力。
css
[WebView] ←双向通信→ [JSBridge] ←桥接→ [鸿蒙API]
↑ ↑
[前端TS类型] [Native能力代理层]
1.2 缺点
但是现在基本上前端都是工程化,很少直接用这种html 页面而且都是跟ts 搭配, 官方的示例就显得很简单,不仅没有很好的错误校验、同步异步的区分,也没办法在前端能够调用鸿蒙函数的时候有很好的ts 提示, 所以我现在就教你们在鸿蒙中封装 一个JSBridge
能够在vue3 项目中完美调用,并且能够在前端有很好的ts 提示
方案 | 维护成本 | 类型安全 | 兼容性 | 开发体验 |
---|---|---|---|---|
官方方案 | 低 | 差 | 高 | 一般 |
手写方案 | 中 | 优 | 可控 | 优秀 |
二、实现原理与步骤
2.1 鸿蒙端Web组件配置
首先在首页引入 web 组件
arkts
import { webview } from '@kit.ArkWeb';
import { OhJSBridge } from '../utils/OhJSBridge';
@Entry
@Component
struct Index {
controller: webview.WebviewController = new webview.WebviewController();
@State jsBridge: OhJSBridge = new OhJSBridge();
methodList: string[] = []
aboutToAppear() {
// 配置Web开启调试模式
webview.WebviewController.setWebDebuggingAccess(true);
this.methodList = Object.keys(this.jsBridge)
}
build() {
Column() {
Column() {
Web({ src: 'http://xxx.xxx.xxx/home', controller: this.controller })
.multiWindowAccess(true)//设置是否开启多窗口权限,默认不开启。
.javaScriptProxy({
object: this.jsBridge,
name: "OhJSBridge",
methodList: this.methodList,
controller: this.controller
})
}
.height('100%')
.width('100%')
}
}
}
src 中就填你本地启动的url 即可
2.2 JSBridge核心层实现
第二步 就是最核心的我们的jsBridge,所有的原生能力都可以在这里实现
typescript
import { call } from '@kit.TelephonyKit';
import { promptAction } from '@kit.ArkUI';
import { scanCore, scanBarcode } from '@kit.ScanKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { OhJSBridgeInterface } from './OhJSBridgeInterface';
export class OhJSBridge implements OhJSBridgeInterface {
constructor() {
}
phoneCall = async (phoneNumber: string, isShowToast: boolean = true) => {
if (!phoneNumber) {
isShowToast && promptAction.showToast({
message: '号码不能为空',
duration: 2000
})
throw new Error(`号码不能为空`)
}
try {
return await call.makeCall(phoneNumber)
} catch (error) {
hilog.error(0x0001, '[phoneCall]', `错误: ${error.code}, message: ${error.message}`);
throw error as Error
}
}
scanQrCode = async () => {
let options: scanBarcode.ScanOptions = {
scanTypes: [scanCore.ScanType.ALL],
enableMultiMode: true, //开启多码扫描
enableAlbum: true, //是否开启相册,默认true。
};
try {
const result = await scanBarcode.startScanForResult(getContext(this), options)
hilog.info(0x0001, '[scanQrCode]', `扫码结果 ${JSON.stringify(result)}`);
return result
} catch (error) {
hilog.error(0x0001, '[scanQrCode]', `错误: ${error.code}, message: ${error.message}`);
throw error as Error
}
}
}
建议将不同功能模块拆分为独立文件(如PhoneService.ts
、ScanService.ts
),通过OhJSBridge
统一聚合暴露,我这边只是一个示范而已。
2.3 类型定义与前端集成
在上面我们可以看到,我把这个JSBridge类实现一个接口OhJSBridgeInterface
为啥要提取出这个类型呢?
为的就是将这个ts 类型提取出来提供给前端用,这样大家都能公用一个类型文件
typescript
enum ScanType {
FORMAT_UNKNOWN = 0,
AZTEC_CODE = 1,
CODABAR_CODE = 2,
CODE39_CODE = 3,
CODE93_CODE = 4,
CODE128_CODE = 5,
DATAMATRIX_CODE = 6,
EAN8_CODE = 7,
EAN13_CODE = 8,
ITF14_CODE = 9,
PDF417_CODE = 10,
QR_CODE = 11,
UPC_A_CODE = 12,
UPC_E_CODE = 13,
MULTIFUNCTIONAL_CODE = 14,
ONE_D_CODE = 100,
TWO_D_CODE = 101,
ALL = 1001
}
interface ScanCodeRect {
left: number;
top: number;
right: number;
bottom: number;
};
interface Point {
x: number;
y: number;
}
interface ScanResult {
scanType: ScanType;
originalValue: string;
scanCodeRect?: ScanCodeRect;
cornerPoints?: Array<Point>;
}
export interface OhJSBridgeInterface {
/**
* 打电话
* @param phoneNumber 手机号
* @param isShowToast 是否显示toast
* @returns
*/
phoneCall: (phoneNumber: string, isShowToast?: boolean) => Promise<void>
/**
* 打电话
* @param phoneNumber 手机号
* @param isShowToast 是否显示toast
* @returns
*/
scanQrCode: () => Promise<ScanResult>
}
这里得注意一个点 比如scanQrCode这个函数 其实他的ts 类型在arkts 中是内置的类型
typescript
OhJSBridge.scanQrCode: () => Promise<scanBarcode.ScanResult>
我目前是手动将scanBarcode.ScanResult
的类型拷贝出来然后替换成 ScanResult
本来的想法是通过js 直接提取出 这个ets 类型 然后通过某种办法 读取他的内置类型最后生成一个ts 文件的,但是没弄出来,如果大家有好办法可以分享出来
然后就是我前端了
vue
<template>
<div class="page-box">
<div class="label">扫码</div>
<van-button class="btn" @click="scanQrCode" type="primary" block>scanQrCode</van-button>
<div class="label">电话</div>
<van-button class="btn" @click="phoneCall" type="primary" block>phoneCall</van-button>
</div>
</template>
<script lang="ts" setup>
import { showConfirmDialog } from 'vant'
import { ref } from 'vue'
const result = ref('回调结果')
function phoneCall() {
window.OhJSBridge.phoneCall('121')
}
async function scanQrCode() {
let res = await window.OhJSBridge.scanQrCode()
result.value = JSON.stringify(res)
showConfirmDialog({
title: '扫码结果',
message: result.value,
confirmButtonText: '确定'
})
}
</script>
然后我们写一个全局的 common.d.ts 来定义 OhJSBridge 类型
typescript
interface Window {
OhJSBridge: import('./OhJSBridgeInterface').OhJSBridgeInterface
}
OhJSBridgeInterface
这个文件就是 我们之前在鸿蒙项目中定义的类型 你可以直接复制一份 到前端项目中将后缀名 改成 ts 即可,当然你也可以写个脚本将OhJSBridgeInterface .ets
文件转成 OhJSBridgeInterface.ts
鼠标附上去发现ts 类型都有了
整体流程:
2.4 效果验证
让我们看看在鸿蒙系统中的效果
当然如果你想做的更好,还可以将OhJSBridge
封装成一个 npm 包,将 类型 OhJSBridgeInterface
抛出来,并且在包里判断是否是鸿蒙环境,如果不是的话就做一下兼容性处理,返回一些Mock数据,让代码更健壮,这后面的我相信是个前端应该都能做到,我就不做示范了。
三、总结
我这个只是一个最简单的方式,如果想做的更加"企业化",还能做的有很多,比如:
- 写个脚本解析鸿蒙ETS类型文件,生成前端
.d.ts
声明,彻底解决手动复制类型的问题 - 统一错误处理机制
- 安全防护策略,我目前是把所有的函数都抛给前端的,你还可以给方法加白名单校验
- 封装npm
- Mock数据
- WebView预热
- ...
如果大家有兴趣,我可以再写一篇文章丰富这些内容。
项目示例代码: