曾静的博客

但行好事,莫问前程.

嗨,我是曾静 (@devzeng),目前暂居深圳。


这是我用来记录平日学习笔记的地方,欢迎您的访问.

hvigor插件开发实战

hvigor 构建工具是一款基于 TypeScript 实现的构建任务编排工具,主要提供任务管理机制,包括任务注册编排、工程模型管理、配置管理等关键能力,提供专用于构建和测试应用的流程和可配置设置。DevEco Studio 使用构建工具 hvigor 来自动执行和管理构建流程,实现应用/服务构建任务流的执行,完成 HAP/APP 的构建打包。

harmony-next

hvigor 允许开发者实现自己的插件,开发者可以定义自己的构建逻辑。目前主要提供了两种方式来实现插件:基于hvigorfile脚本开发插件 和 基于typescript项目开发。主要的区别就是代码是直接放在 hvigorfile.ts 里面还是将逻辑放在一个独立的 npm 库里面然后再 hvigorfile.ts 里面接入使用。

为了方便快速初始化项目我做了一个模板工程 hvigor_plugin_template ,免去一些项目初始化的操作。

开发

开发一个插件最核心的代码是下面的代码:

import { HvigorNode, HvigorPlugin } from '@ohos/hvigor';

export function customPlugin(): HvigorPlugin {
    return {
        pluginId: 'customPlugin',
        apply(node: HvigorNode) {
            console.log(`node: ${node.getNodeName()}, path: ${node.getNodePath()}`);
            console.log('hello customPlugin!');
        }
    }
}

上面的代码本质上是将函数 customPlugin 导出给外部使用(注册插件就是方法调用的过程),这个函数最终返回的是 HvigorPlugin 定义如下:

/**
 * HvigorPlugin定义声明
 */
export interface HvigorPlugin {
    /**
     * 插件的唯一标示ID
     */
    pluginId: string;
    /**
     * 插件元数据定义
     */
    context?: (() => any) | any;
    /**
     * 插件的主体方法,定义插件实现逻辑(例如任务注册等)
     * 在hvigor的生命周期配置阶段调用,进行task的注册
     *
     * @param node
     */
    apply: (node: HvigorNode) => void | Promise<void>;
}

HvigorPlugin 主要有三个参数pluginId、context、apply:

(1)pluginId:[必填]插件的唯一标示ID

(2)context:[非必填]插件元数据定义(暂未深入研究)

(3)apply: [必填]插件的主体方法,业务逻辑写在 apply 里面, 这里只有一个参数 HvigorNode (Hvigor Plugin 扩展接口),主要提供的接口如下:

hvigor-node

比如可以通过如下代码获取当前正在操作的模块所在的路径:

const modulePath = node.getNodePath()
console.log(`模块目录:${modulePath}`);

然后就可以通过一些纯JS的代码实现目录文件相关扫描

const scanDir = modulePath + options.scanDir;
fs.readdirSync(scanDir).forEach((file) => {
    const fullPath = path.join(scanDir, file);
    const stats = fs.lstatSync(fullPath)
    if (stats.isFile() && (file !== '.DS_Store' && fullPath.endsWith(".ets"))) {
        console.log(`文件路径: ${fullPath}`)
    }
})

找到需要识别的文件,就可以通过文件的一些特征(一般会定义一些自定义的装饰器)来提取一些参数。这里可以采用AST解析或者是正则解析,推荐是使用AST解析的方式实现。

@ServiceModule('a')
export class ModuleAService {

  @ServiceAction('openPage')
  openFormPage(params?: object) {
    router.pushUrl({
      url: "pages/FormPage",
      params: params
    })
  }
}

对 ets 文件源码解析提取装饰器的相关数据:

const filePath = 'xxxx.ets'
const file = path.parse(filePath).name
const sourceCode = readFileSync(filePath, "utf-8");
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.ES2021, false);
ts.forEachChild(sourceFile, (node: ts.Node) => {
    // 解析装饰器定义和提取参数生成需要的数据
    if (node.kind == ts.SyntaxKind.ClassDeclaration) {
      let child = node as ts.ClassDeclaration
      // ... 省略解析代码
    }
    // ... 省略解析代码
})

这里有一个小技巧可以使用 https://ts-ast-viewer.com 这个网站来生成可视化的数据然后对着写解析代码就行了。

ts-ast-viewer

最终生成的数据,以下是示例:

[
    {
        'identifier': 'ModuleAService', /* 类名 */
        'file': 'services/ModuleA', /* 类所在文件相对路径 */
        'module': 'a', /* 模块标识名 */
        'actions': {
            'identifier': 'openFormPage', /* 方法名称 */
            'action': 'openPage' /* 动作名称 */
        }
    },
    {
        'identifier': 'ModuleBService', 
        'file': 'services/ModuleB', 
        'module': 'b',
        'actions': {
            'identifier': 'openFormPage',
            'action': 'openPage'
        }
    }
]

根据提取到的数据结合动态模板来生成模板代码。

{{#each pluginList}}
import { {{identifier}} } from './{{file}}';
{{/each}}

export default function execute(module: string, action: string, params: object[]) {
    switch (module) {
        {{#each pluginList}}
        case '{{module}}':
            execute{{module}}Service(module, action, params);
            break;
        {{/each}}
        default:
            throw new Error(`未知module: ${module}`);
    }
}

{{#each pluginList}}
function execute{{module}}Service(module: string, action: string, params: object[]) {
    const plugin = new {{identifier}}();
    switch (action) {
        {{#each actions}}
        case '{{action}}':
            plugin.{{identifier}}(params);
            break;
        {{/each}}
        default:
            throw new Error(`${module}未知action: ${action}`);
    }
}

{{/each}}

对模板进行编译解析生成具体的代码:

import path from 'path';
import Handlebars from "handlebars";
import { writeFileSync, readFileSync } from "fs";

export function compileTemplate(templatePath: string, data: any, outputPath: string) {
    const tpl = readFileSync(templatePath, { encoding: "utf8" });
    const template = Handlebars.compile(tpl);
    const output = template(data);
    writeFileSync(outputPath, output, { encoding: "utf8" });
}

到这里就基本上实现了一个代码生成器,在一定程度上可以实现“代码动态调用”。

使用

1、打包项目

因为目前只是开发阶段所以不必要每次都发布到npm仓库里面去,一般情况下我都倾向于在本地打包然后直接添加本地依赖的方式使用。

npm pack

执行上面的命令会在项目根目录下面生成一个 项目名称-版本号.tgz 的文件(如: hvigor_demo_plugin-0.1.0.tgz)

2、在 hvigor\hvigor-config.json5 添加如下配置

"dependencies": {
    "@zengjing/hvigor_demo_plugin": "file:../dependencies/hvigor_demo_plugin-0.1.0.tgz"
}

让添加的dependencies生效的方法:

  • 方式1:执行编辑区右上角 Install Now 或执行菜单 File -> Sync and Refresh Project 进行工程 Sync 后,DevEco Studio 将会根据 hvigor-config.json5 中的依赖配置自动安装。

  • 方式2:执行 hvigorw --sync 命令。

说明:每次改动新功能之后建议将版本号进行调整然后重新打包,避免因为缓存的原因导致不生效。

3、在 hvigorfile.ts 中导入插件

import { customPlugin } from '@zengjing/hvigor_demo_plugin';

export default {
    system: appTasks,
    plugins:[
        customPlugin()
    ]
}

4、执行运行项目插件就会自动生效了,可以在build日志中查看是否生效

参考资料

最近的文章

ThinkPad无法启动系统问题解决办法

最近几天一台Windows电脑(ThinkPad)无法启动进入系统,开机有个错误提示界面,然后就自动关机,错误界面如下图:上面的文字内容如下:Verifying shlm SBAT data failed: Security Policy ViolationSomething has gone seriously wrong: SBAT self-check failed: Security Policy Violation 开始怀疑是系统硬件问题,使用系统自带的自检工具查了一下没有发现有...…

Note继续阅读
更早的文章

自定义GitHub Actions

GitHub Actions 是 GitHub 官方推出的持续集成和持续交付(CI/CD)平台,可以让用户便捷实现自动化构建、测试和部署流程。关于 GitHub ActionsGitHub Actions 有几个概念(workflow、event、job、action、step、runner),下面会简单介绍常用的workflow、job、step的使用。workflow (工作流程)workflow 是可配置的自动化流程,可以运行一个或多个job。workflow 由仓库 .github...…

Note继续阅读