按需加载的JS桥接Hvigor插件实现
针对您的需求,我将实现一个Hvigor插件,通过@JSAPI
注解创建模块化JS桥接,支持按需加载和方法回调。
系统架构设计
graph TD
A[ArkTS业务代码] --> B[Hvigor插件]
style A fill:#F9E79F,stroke:#F1C40F
style B fill:#AED6F1,stroke:#3498DB
B --> C[编译时扫描]
C --> D[代码生成]
D --> E[桥接注册中心]
style C fill:#D5F5E3,stroke:#2ECC71
style D fill:#D5F5E3,stroke:#2ECC71
E --> F[按需加载模块]
F --> G[方法回调管理器]
G --> H[H5页面]
style F fill:#E8DAEF,stroke:#9B59B6
style G fill:#FADBD8,stroke:#E74C3C
style H fill:#F5B7B1,stroke:#C0392B
classDef tech fill:#5,stroke:#95A5A6;
class E tech;
实现代码
1. ArkTS注解定义 (JSAPI.ets
)
TS
// src/main/ets/bridge/JSAPI.ets
export type JSCallback = (result: any) => void;
const BridgeRegistry: {
[moduleId: string]: {
[methodId: string]: (data: any, callback: JSCallback) => void
}
} = {};
export function registerBridge(
moduleId: string,
methodId: string,
method: (data: any, callback: JSCallback) => void
) {
if (!BridgeRegistry[moduleId]) {
BridgeRegistry[moduleId] = {};
}
BridgeRegistry[moduleId][methodId] = method;
}
export function getBridgeMethod(
moduleId: string,
methodId: string
): ((data: any, callback: JSCallback) => void) | null {
return BridgeRegistry[moduleId]?.[methodId] || null;
}
// JSAPI注解定义
export const JSAPI = (moduleId: string, methodId: string) => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
// 注册到桥接中心
registerBridge(moduleId, methodId,
(data: any, callback: JSCallback) => {
// 创建新的模块实例(按需实例化)
const instance = new target.constructor();
// 调用方法并处理回调
originalMethod.call(instance, data)
.then((result: any) => {
callback({
success: true,
data: result
});
})
.catch((error: any) => {
callback({
success: false,
message: error.message
});
});
}
);
return descriptor;
};
};
2. Hvigor插件实现 (js-bridge-plugin.ts
)
TS
// plugins/js-bridge-plugin.ts
import { Task, Plugin, Project, FileScanner } from '@ohos/hvigor';
import * as path from 'path';
import * as fs from 'fs';
import { transform, traverse, types as t } from '@babel/core';
import generator from '@babel/generator';
export default class JSBridgePlugin implements Plugin {
apply(task: Task) {
task.doLast(async () => {
const project: Project = task.project;
const srcPath = path.join(project.rootDir, 'src/main/ets');
const bridgePath = path.join(project.rootDir, 'src/main/ets/bridge');
// 创建或清空生成的桥接目录
if (!fs.existsSync(bridgePath)) {
fs.mkdirSync(bridgePath, { recursive: true });
}
// 扫描所有ets文件
const scanner = new FileScanner(project);
const files = scanner.scan(srcPath, ['.ets']);
// 解析AST获取所有注解信息
const bridgeMetadata: {
[moduleId: string]: {
methods: string[],
targetPath: string
}
} = {};
for (const file of files) {
const code = fs.readFileSync(file, 'utf8');
const ast = transform(code, {
plugins: ['@babel/plugin-syntax-typescript'],
filename: file
})?.ast;
if (!ast) continue;
traverse(ast, {
Decorator(path) {
if (!t.isIdentifier(path.node.expression.callee, { name: 'JSAPI' })) {
return;
}
const classPath = path.findParent(p => p.isClassDeclaration());
if (!classPath || !t.isClassDeclaration(classPath.node)) {
return;
}
const args = (path.node.expression as t.CallExpression).arguments;
if (args.length < 2 || !t.isStringLiteral(args[0]) || !t.isStringLiteral(args[1])) {
return;
}
const moduleId = args[0].value;
const methodId = args[1].value;
const className = classPath.node.id?.name || 'AnonymousClass';
// 存储元数据
if (!bridgeMetadata[moduleId]) {
bridgeMetadata[moduleId] = {
methods: [],
targetPath: path.relative(project.rootDir, file)
};
}
bridgeMetadata[moduleId].methods.push(methodId);
}
});
}
// 生成桥接加载器代码
if (Object.keys(bridgeMetadata).length > 0) {
this.generateBridgeLoader(bridgePath, bridgeMetadata);
}
});
}
private generateBridgeLoader(
outputDir: string,
metadata: { [moduleId: string]: { methods: string[]; targetPath: string } }
) {
const filePath = path.join(outputDir, 'BridgeLoader.ts');
const imports: string[] = [];
const loaderMethods: string[] = [];
Object.keys(metadata).forEach(moduleId => {
const { methods, targetPath } = metadata[moduleId];
// 计算相对路径
const relPath = path.relative(path.dirname(filePath), targetPath)
.replace(/.ets$/, '');
// 添加导入
imports.push(`import { registerBridge } from './JSAPI';`);
imports.push(`import * as ${moduleId}Module from '${relPath}';`);
// 生成模块加载方法
loaderMethods.push(`
static async ${moduleId}(methodId: string, data: any, callback: (result: any) => void): Promise<boolean> {
switch (methodId) {
${methods.map(methodId => `
case '${methodId}':
const bridgeFunc = ${moduleId}Module.registerBridge('${moduleId}', '${methodId}');
if (bridgeFunc) {
bridgeFunc(data, callback);
return true;
}
break;`).join('')}
default:
return false;
}
return false;
}`);
});
// 添加总加载方法
loaderMethods.push(`
static load(moduleId: string, methodId: string, data: any, callback: (result: any) => void): Promise<boolean> {
switch (moduleId) {
${Object.keys(metadata).map(moduleId => `
case '${moduleId}':
return BridgeLoader.${moduleId}(methodId, data, callback);`).join('')}
default:
return Promise.resolve(false);
}
}`);
const code = `
// AUTO-GENERATED FILE - DO NOT EDIT
${imports.join('\n')}
export class BridgeLoader {
${loaderMethods.join('\n')}
}`;
fs.writeFileSync(filePath, code);
}
}
3. 原生桥接管理器 (NativeBridgeManager.ets
)
TS
// src/main/ets/bridge/NativeBridgeManager.ets
import { BridgeLoader } from './BridgeLoader';
import { getBridgeMethod } from './JSAPI';
export class NativeBridgeManager {
private static callbackMap = new Map<string, (result: any) => void>();
// 暴露给WebView的调用接口
static callNative(moduleId: string, methodId: string, data: string) {
return new Promise<string>((resolve, reject) => {
// 检查并获取桥接方法
const bridgeMethod = getBridgeMethod(moduleId, methodId);
if (!bridgeMethod) {
reject(new Error(`No bridge method found for ${moduleId}.${methodId}`));
return;
}
// 生成唯一回调ID
const callbackId = `${Date.now()}-${Math.random().toString(36).substring(2)}`;
// 保存回调函数
this.callbackMap.set(callbackId, (result) => {
resolve(JSON.stringify(result));
this.callbackMap.delete(callbackId);
});
try {
const jsonData = JSON.parse(data || '{}');
// 调用桥接方法
bridgeMethod(jsonData, (result) => {
const callback = this.callbackMap.get(callbackId);
if (callback) {
callback(result);
}
});
} catch (e) {
reject(new Error(`Invalid data format: ${e.message}`));
}
});
}
// 提供直接的H5调用接口
@JSBridgeInterface
static invokeBridgeFunction(moduleId: string, methodId: string, args: string, callback: (result: any) => void) {
BridgeLoader.load(moduleId, methodId, JSON.parse(args || '{}'), (result) => {
callback(result);
});
}
}
4. H5桥接适配器 (web-bridge.js
)
TS
// src/main/resources/rawfile/js/web-bridge.js
class JSBridge {
constructor() {
this._callbackMap = new Map();
this._initialized = false;
this._nextCallbackId = 1;
this._supportedModules = new Set();
// 初始化原生通信机制
if (window.androidBridge) {
// Android WebView
window._jsBridgeCallback = (callbackId, result) => {
this._handleCallback(callbackId, result);
};
} else if (window.webkit && window.webkit.messageHandlers) {
// iOS WKWebView
window._jsBridgeCallback = (callbackId, result) => {
this._handleCallback(callbackId, result);
};
} else {
console.error('JSBridge not supported in this environment');
}
}
// 初始化桥接
async initialize() {
if (this._initialized) return true;
try {
// 获取支持的模块列表
const modules = await this.invoke('Bridge', 'getSupportedModules');
modules.forEach(module => this._supportedModules.add(module));
this._initialized = true;
return true;
} catch (e) {
console.error('Failed to initialize JSBridge:', e);
return false;
}
}
// 检查模块是否可用
isModuleSupported(moduleId) {
return this._supportedModules.has(moduleId);
}
// 调用原生方法
async invoke(moduleId, methodId, args = {}) {
if (!this._initialized) {
await this.initialize();
}
if (!this.isModuleSupported(moduleId)) {
throw new Error(`Module ${moduleId} not supported`);
}
// 生成回调ID
const callbackId = `cb_${this._nextCallbackId++}`;
return new Promise((resolve, reject) => {
// 保存回调
this._callbackMap.set(callbackId, { resolve, reject });
try {
// 准备调用数据
const callData = {
moduleId,
methodId,
args: JSON.stringify(args),
callbackId
};
// 调用原生桥接
if (window.androidBridge) {
window.androidBridge.callNative(JSON.stringify(callData));
} else if (window.webkit && window.webkit.messageHandlers.jsBridge) {
window.webkit.messageHandlers.jsBridge.postMessage(callData);
}
// 设置超时保护
setTimeout(() => {
if (this._callbackMap.has(callbackId)) {
this._callbackMap.delete(callbackId);
reject(new Error('Timeout waiting for native response'));
}
}, 10000);
} catch (e) {
this._callbackMap.delete(callbackId);
reject(new Error(`JSBridge call failed: ${e.message}`));
}
});
}
// 处理来自原生的回调
_handleCallback(callbackId, resultJson) {
try {
const result = JSON.parse(resultJson);
const callback = this._callbackMap.get(callbackId);
if (callback) {
this._callbackMap.delete(callbackId);
if (result.success) {
callback.resolve(result.data);
} else {
callback.reject(new Error(result.message));
}
}
} catch (e) {
console.error('Error handling callback:', e);
}
}
}
// 暴露全局桥接对象
window.JSBridge = new JSBridge();
5. 使用示例
原生模块实现
TS
// src/main/ets/modules/CameraModule.ets
import { JSAPI } from '../bridge/JSAPI';
export class CameraModule {
@JSAPI('Camera', 'takePhoto')
async takePhoto(options: { quality: number }): Promise<string> {
return new Promise<string>((resolve) => {
// 实际调用设备相机API
resolve(`file:///photos/${Date.now()}.jpg`);
});
}
@JSAPI('Camera', 'getAvailableResolutions')
async getAvailableResolutions(): Promise<string[]> {
return ['480p', '720p', '1080p', '4K'];
}
}
H5页面使用
TS
<!DOCTYPE html>
<html>
<head>
<title>JSBridge Demo</title>
<script src="js/web-bridge.js"></script>
</head>
<body>
<button onclick="takePhoto()">Take Photo</button>
<button onclick="getResolutions()">Get Available Resolutions</button>
<script>
async function takePhoto() {
try {
// 确保桥接已初始化
await JSBridge.initialize();
// 调用相机模块
const photoPath = await JSBridge.invoke('Camera', 'takePhoto', {
quality: 90
});
console.log('Photo taken:', photoPath);
alert(`Photo saved: ${photoPath}`);
} catch (error) {
console.error('Error taking photo:', error);
}
}
async function getResolutions() {
try {
await JSBridge.initialize();
const resolutions = await JSBridge.invoke('Camera', 'getAvailableResolutions');
console.log('Available resolutions:', resolutions);
const list = resolutions.join(', ');
alert(`Available resolutions: ${list}`);
} catch (error) {
console.error('Error getting resolutions:', error);
}
}
</script>
</body>
</html>
Hvigor配置
json
// build-profile.json5
{
"app": {
"signingConfigs": [ ... ],
"buildTypes": [ ... ]
},
"plugins": [
"@ohos/hap-plugin",
{
"name": "./plugins/js-bridge-plugin.js",
"apply": "product"
}
],
"dependencies": {
"local": [
"src/main/ets/bridge",
"src/main/ets/modules"
]
}
}
核心特性
-
按需加载实例:
- 只在H5调用时创建桥接实例
- 避免不必要的资源消耗
-
模块化管理:
- 每个模块独立注册和管理
- 支持按模块控制权限
-
双向回调支持:
- 异步方法返回Promise
- 原生主动调用H5回调
-
自动化生成:
- Hvigor插件自动扫描注解
- 生成桥接加载器代码
-
安全机制:
- 回调超时保护
- 参数序列化验证
- 模块权限控制
-
高性能设计:
- 按需加载桥接模块
- 避免一次性全部加载
- 回调缓存管理
使用方法
-
在业务代码中添加
@JSAPI
注解:TS@JSAPI('MyModule', 'myMethod') async myMethod(param: any): Promise<any> { ... }
-
在H5中调用桥接方法:
TS// 确保已初始化 await JSBridge.initialize(); // 调用方法 const result = await JSBridge.invoke('MyModule', 'myMethod', { data });
-
编译项目:
hvigorw
-
运行应用并加载H5页面