Hvigor插件实现JS桥接

按需加载的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"
    ]
  }
}

核心特性

  1. 按需加载实例​:

    • 只在H5调用时创建桥接实例
    • 避免不必要的资源消耗
  2. 模块化管理​:

    • 每个模块独立注册和管理
    • 支持按模块控制权限
  3. 双向回调支持​:

    • 异步方法返回Promise
    • 原生主动调用H5回调
  4. 自动化生成​:

    • Hvigor插件自动扫描注解
    • 生成桥接加载器代码
  5. 安全机制​:

    • 回调超时保护
    • 参数序列化验证
    • 模块权限控制
  6. 高性能设计​:

    • 按需加载桥接模块
    • 避免一次性全部加载
    • 回调缓存管理

使用方法

  1. 在业务代码中添加@JSAPI注解:

    TS 复制代码
    @JSAPI('MyModule', 'myMethod')
    async myMethod(param: any): Promise<any> { ... }
  2. 在H5中调用桥接方法:

    TS 复制代码
    // 确保已初始化
    await JSBridge.initialize();
    
    // 调用方法
    const result = await JSBridge.invoke('MyModule', 'myMethod', { data });
  3. 编译项目:

    复制代码
    hvigorw
  4. 运行应用并加载H5页面

相关推荐
沉淀风飛几秒前
鸿蒙Next在内存管理总结
前端·harmonyos
沉淀风飛3 分钟前
【最新】鸿蒙Next 装饰器使用方法整理
harmonyos
zhanshuo12 小时前
ArkTS 编译错误不求人:快速定位与修复全攻略
harmonyos
zhanshuo13 小时前
Android 到鸿蒙,不止是兼容:分布式能力改造全攻略
harmonyos
前端世界16 小时前
HarmonyOS 设备自动发现与连接全攻略:从原理到可运行 Demo
华为·harmonyos
御承扬16 小时前
HarmonyOS NEXT系列之编译三方C/C++库
c语言·c++·harmonyos
无风听海16 小时前
HarmonyOS之module.json5功能详解
harmonyos·module.json5
HMS Core16 小时前
HarmonyOS SDK助力讯飞听见App能力建设
华为·harmonyos
xyccstudio19 小时前
鸿蒙动态共享包HSP
前端·harmonyos