一键多环境构建——用 Hvigor 玩转 HarmonyOS Next

引言

在 HarmonyOS Next 的应用开发中,常常需要针对不同环境(测试、预发、线上)或不同签名(调试、正式)输出多个 APP/HAP 包。虽然 HarmonyOS 提供了多目标构建(Multi-Target Build)能力,可以在同一项目里配置多个 product 并生成不同包名的多个产物,但某些深度定制(如动态修改 module.json5 中的 client_idapp_id,或基于构建时间改写输出包名)仍需借助自定义 Hvigor 插件来完成。

结合示例项目,演示如何通过 Hvigor 插件配合多目标构建,实现:

  1. 统一管理各环境的 API 地址、埋点地址等配置;
  2. 动态注入 clientIdappId 等应用信息;
  3. 基于构建时间和 product 名称,定制化输出包名,便于后续上架和线上排查。

一、在项目中使用 Hvigor 自定义插件

1. Hvigor 插件的入口

在 HarmonyOS 项目根目录存在入口文件 hvigorfile.ts,将自定义 task 注册到 plugins 中:

typescript 复制代码
// 文件:hvigorfile.ts
import { buildSchemaProcessing } from './scripts/build-schema-processing';

export default {
  system: appTasks,             // Hvigor 内置任务,不可修改
  plugins: [
    buildSchemaProcessing()     // 自定义插件:动态修改 module.json5、build-profile.json5 等
  ]
};

点击 DevEco Studio 上方的 Sync Now 或执行 hvigor sync,即可将插件加载到构建流程中。


2. Hvigor 常用 API 概览

在自定义插件代码中,我们通常会用到以下核心 API:

  • hvigor.getRootNode():获取根节点,进而拿到各插件上下文
  • rootNode.getContext(pluginId):获取指定插件的上下文对象,例如 OHOS_APP_PLUGINOHOS_HAP_PLUGIN
  • context.getAppJsonOpt() / getModuleJsonOpt():读取 app.json5module.json5 的 AST 对象
  • context.getBuildProfileOpt():读取根目录 build-profile.json5 的 AST 对象
  • context.setAppJsonOpt() / setBuildProfileOpt():将修改后的 AST 写回,生效于后续构建
typescript 复制代码
// 示例:读取上下文
const root = hvigor.getRootNode();
const appCtx = root.getContext(OhosPluginId.OHOS_APP_PLUGIN) as OhosAppContext;
const hapCtx = root.getContext(OhosPluginId.OHOS_HAP_PLUGIN) as OhosAppContext;
const appJson = appCtx.getAppJsonOpt();
const modJson = hapCtx.getModuleJsonOpt();
const buildProfile = appCtx.getBuildProfileOpt();

// 修改后需要重新 set 回去
appCtx.setAppJsonOpt(appJson);
appCtx.setBuildProfileOpt(buildProfile);

二、结合多目标构建与自定义插件 实现环境切换

1. 规范 Product 命名

为了脚本中便于解析,我们约定 productname 采用下划线分隔、固定格式:

js 复制代码
<应用标识>_<签名标识>_<环境标识>
例如:demo1_debug_test、demo1_release_preRelease、demo2_debug_official

build-profile.json5 中配置多个 product:

json5 复制代码
"products": [
  {
    "name": "demo1_debug_test",
    "signingConfig": "debug",
    "buildOption": {
      "arkOptions": {
        "buildProfileFields": {
          "productsName": "demo1",
          "buildTime": "",
          "apiUrl": "",
          "trackApiUrl": ""
        }
      }
    }
  },
  {
    "name": "demo1_release_officiallyReleased",
    "signingConfig": "release",
    "buildOption": { /* 同上 */ }
  },
  /* 更多 product 配置... */
]

TipbuildProfileFields 下的字段会被注入到应用运行时,便于在代码中读取环境信息。


2. 本地配置文件:config.json

将各环境的 API 地址,以及各应用的 clientId/appId 信息集中管理:

json 复制代码
{
  "environmentInfo": {
    "test": {
      "apiUrl": "https://api.test.com",
      "trackApiUrl": "https://stat.test.com"
    },
    "preRelease": {
      "apiUrl": "https://api-pre.example.com",
      "trackApiUrl": "https://stat-pre.example.com"
    },
    "officiallyReleased": {
      "apiUrl": "https://api.prod.com",
      "trackApiUrl": "https://stat.prod.com"
    }
  },
  "appConfigInfo": {
    "demo1": {
      "clientId": "111898773",
      "appId": "5765880207855284373"
    },
    "demo2": {
      "clientId": "6917571239128090930",
      "appId": "6917571239128090930"
    }
  }
}

并提供两个辅助函数,在插件中使用:

typescript 复制代码
// scripts/config.ts
import cfgRaw from '../config.json';
import { format } from 'date-fns';

export function formatBuildTime(date = new Date()): string {
  const pad = (n: number) => String(n).padStart(2, '0');
  return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}`
       + `_${pad(date.getHours())}-${pad(date.getMinutes())}-${pad(date.getSeconds())}`;
}

export function getLocalConfig() {
  const cfg = JSON.parse(JSON.stringify(cfgRaw));
  cfg.buildTime = formatBuildTime();
  return cfg;
}

3. 插件实现核心逻辑

build-schema-processing.ts 中,利用 Hvigor 生命周期钩子完成配置注入与产物重命名。

typescript 复制代码
// scripts/build-schema-processing.ts
import { getLocalConfig } from './config';

export function buildSchemaProcessing() {
  const localCfg = getLocalConfig();
  let currentProduct = '', versionName = '', bundleName = '', appConfig: any;

  return {
    pluginId: 'custom-build-processor',
    apply(hvigor) {
      hvigor.getRootNode().afterNodeEvaluate(root => {
        // 获取上下文
        const appCtx = root.getContext(OhosPluginId.OHOS_APP_PLUGIN) as OhosAppContext;
        const hapCtx = root.getContext(OhosPluginId.OHOS_HAP_PLUGIN) as OhosAppContext;
        const buildProfile = appCtx.getBuildProfileOpt();
        const appJson = appCtx.getAppJsonOpt();
        const modJson = hapCtx.getModuleJsonOpt();

        // 当前 product 信息
        currentProduct = appCtx.getCurrentProduct().getProductName() || '';
        versionName = appJson.app.versionName;
        const productKeys = currentProduct.split('_');
        const appKey = productKeys[0];
        const envKey = productKeys[2];
        appConfig = localCfg.appConfigInfo[appKey];
        const envConfig = localCfg.environmentInfo[envKey];

        // 注入 app.json5 中的 clientId、appId
        if (modJson) {
          modJson['module']['appId'] = appConfig.appId;
          modJson['module']['clientId'] = appConfig.clientId;
          hapCtx.setModuleJsonOpt(modJson);
        }

        // 遍历各 product,注入环境 & 时间 & 重命名 artifact
        (buildProfile.app.products || []).forEach((prd: any) => {
          prd.buildOption.arkOptions.buildProfileFields.buildTime  = localCfg.buildTime;
          prd.buildOption.arkOptions.buildProfileFields.apiUrl     = envConfig.apiUrl;
          prd.buildOption.arkOptions.buildProfileFields.trackApiUrl= envConfig.trackApiUrl;

          const suffix = `${appKey}_${versionName}_${localCfg.buildTime}`;
          if (prd.name === currentProduct) {
            bundleName = prd.bundleName || appJson.app.bundleName;
          }
          prd.output.artifactName = `AtomicPlatform-${suffix}`;
        });

        appCtx.setBuildProfileOpt(buildProfile);
      });

      hvigor.buildFinished(() => {
        console.log(`📅 构建时间: ${localCfg.buildTime}`);
        console.log(`📦 当前产物: ${currentProduct}`);
        console.log(`🔖 包名(bundleName): ${bundleName}`);
        console.log(`🆔 ClientID: ${appConfig.clientId}`);
        console.log(`🆔 AppID: ${appConfig.appId}`);
      });
    }
  };
}

三、运行效果与日志验证

执行 hvigor build,在日志中即可看到注入与重命名结果:

复制代码
====================== 编译包信息 ======================
📅 构建时间:  2025-04-24_09-32-05
📦 当前产物: demo2_debug_test
🔖 包名: com.atomicservice.6917571239128090930
🆔 ClientID: 6917571239128090930
🆔 AppID: 6917571239128090930
====================================================

同时,输出的 HAP 包会命名为:

复制代码
AtomicPlatform-demo2_1.0.0_2025-04-24_09-32-05.hap

通过上述方式,每次在不同 product 与环境下编译,都能自动完成配置注入与产物重命名,极大提升了多环境多签名的开发与发布效率。


四、总结

  • 统一管理 :将环境信息、应用信息集中在 config.json,便于维护与扩展;
  • 自动注入 :借助 Hvigor 插件,在构建阶段动态修改 module.json5build-profile.json5,免去手动切换;
  • 定制产物:基于构建时间与 product 名称,自定义输出包名,方便版本追踪与线上排查。

未来可在插件中进一步添加:

  1. 构建报告自动上传到 CI/CD 平台;
  2. 自动生成构建差异对比的 HTML 报告;
  3. 与 Git 提交、发布流程集成,构建完成自动触发审核或推送。

五、源码仓库

仓库分支参考:feature/hvigorfileBuild

仓库地址:MultiBuildDemo: 构建多目标产物 - Gitee.com

六、参考文档

扩展构建-编译构建-DevEco Studio - 华为HarmonyOS开发者

HarmonyOS Next 编译之如何构建不同包名应用在日常的开发中涉及到多签名和多产物构建输出时手动切换签名文件和包 - 掘金

HarmonyOS多环境+多渠道+自定义路径输出+自定义名称一键打app和hap包前言 做移动端开发时,不可避免的会遇到 - 掘金

sumer/cn/doc/harmonyos-guides/ide-build-expanding)

HarmonyOS Next 编译之如何构建不同包名应用在日常的开发中涉及到多签名和多产物构建输出时手动切换签名文件和包 - 掘金

HarmonyOS多环境+多渠道+自定义路径输出+自定义名称一键打app和hap包前言 做移动端开发时,不可避免的会遇到 - 掘金

相关推荐
NapleC3 小时前
HarmonyOS NEXT:多设备的自由流转
华为·harmonyos
鸿蒙布道师8 小时前
鸿蒙NEXT开发正则工具类RegexUtil(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
Huang兄8 小时前
鸿蒙-使用Charles抓包
harmonyos
CZIDC8 小时前
[特殊字符][特殊字符] HarmonyOS相关实现原理聊聊![特殊字符][特殊字符]
华为·harmonyos
别说我什么都不会9 小时前
【仓颉三方库】算法类—— flexSearch4cj
harmonyos
IT运维爱好者9 小时前
Ubuntu 22.04.4操作系统初始化详细配置
linux·运维·服务器·ubuntu
二流小码农10 小时前
鸿蒙开发:如何更新对象数组
android·ios·harmonyos
yangshuo128112 小时前
风车邮箱系统详细使用指南:Windows与Ubuntu双平台解析
linux·windows·ubuntu
君莫笑1111112 小时前
从零到一教你在鸿蒙中实现微信分享--全流程
前端·harmonyos