鸿蒙构建排除指定模块:hvigorfile脚本实战与踩坑记录

发布时间:2026/10/19 12:00:47
鸿蒙构建排除指定模块:hvigorfile脚本实战与踩坑记录 鸿蒙开发做到后面工程里的模块一定会多起来。我之前维护的一个项目就是典型入口模块 entry几个业务能力模块一个跨端公共库还有一个内部调试工具模块。平时开发调试离不开那个工具模块但发布正式包的时候它又绝对不能出现在最终产物里。一开始大家都是手动改配置发布前靠人肉检查说实话心里没底。后来我把这套逻辑直接写进了 hvigorfile 构建脚本里让构建过程自己判断要不要把指定模块排除掉省心很多。这篇文章就把我在 hvigorfile 上做排除指定模块这整件事的完整思路、具体实现和踩坑记录分享出来。1. hvigorfile被很多人当成摆设的构建入口1.1 根目录和模块目录的 hvigorfile 各自负责什么每次新建一个鸿蒙工程根目录下都会自动生成一个 hvigorfile.ts旧版本可能是 .js每个模块目录下也会各有一个 hvigorfile.ts。大部分开发者一辈子都不会去动它因为 DevEco Studio 的图形化操作已经覆盖了日常需求。但其实这两个东西的分工非常清晰根目录的 hvigorfile.ts管整个工程级别的任务编排比如同步所有模块、全局编译配置、最终打包聚合。模块目录的 hvigorfile.ts只管当前模块自己的构建任务比如编译、生成中间产物、产出自有的 HAP/HAR/HSP。这两个文件本质上就是 hvigor 构建工具在启动时加载的入口脚本。hvigor 自身是一个基于任务流Task的构建引擎它本身是个 Node 运行时环境所以这两个文件里不仅可以写导出的默认配置还能写任何 Node.js 的代码——读写文件、解析 JSON、执行命令行脚本全都允许。这就是我们做排除指定模块的底层基础。1.2 从任务流视角理解打包流程hvigor 的构建过程是一条很长的任务流水线。我理解它的方式就是类比工厂装配线Sync同步阶段把工程里的所有模块和配置整理清楚然后进入 Load 阶段加载插件和任务接着是 Configure 阶段确定要执行哪些任务最后 Execute 阶段才真正开始编译、打包。对于我们做排除指定模块来说最关键的一个认知是hvigor 在 Sync 阶段就会确定这个工程由哪些模块组成。如果你在 Sync 阶段让某个模块消失后面所有编译、打包的过程都不会有它。反之如果你到了 Execute 阶段才想起要排除某个模块那就只能靠各种 hook钩子在任务之间做干预难度和风险都会成倍上升。所以我把实操思路定在了Sync 之前干预模块列表这个节点上这也是后面所有方案的根本方向。2. 排除指定模块先分清 HAP、HAR、HSP 再说2.1 三种产物在打包时的命运完全不同这是我踩过最大的坑之一刚开始没搞清楚模块类型就动手排除结果发现怎么排都排不干净。鸿蒙工程里的模块打包产物严格分三种HAP应用安装包最终要发布到用户手机上的东西。HAR静态共享包。它会被编译进引用方模块的产物里也就是说一旦 entry 依赖了一个 HAR打包时这个 HAR 的内容就进 HAP 了没有独立存在这一说。HSP动态共享包。它是独立打包的入口模块只是引用它运行时动态加载。最终多包发布时 HSP 和 HAP 是分开的两个文件。搞清楚这个之后排除指定模块的语义就变得非常具体了排除一个 HAR 模块本质上是排除引用关系——让 entry 不再依赖它这样它就不会被打进 HAP。排除一个 HSP 模块本质上是排除发布打包时的产物集合——让最终的发布任务不产出或包含这个 HSP。排除一个 HAP 模块那就更直接了整个模块都不参与构建。2.2 真实需求长什么样我遇到的需求就是前面说的调试模块。它是一个 HAR 类型的模块被 entry 在 debug 场景下引用。平时开发调试需要它提供一些内部工具入口但正式发布包绝对不能带这些能力。这种需求用一句话说就是Release 构建时把这个模块从 entry 的依赖链里摘掉。还有一种很常见的需求是多产品形态。比如同一个工程既有普通版 A 产品又有定制版 B 产品。B 产品不需要某个独立业务模块但 A 产品要。这种场景下每次都手动改模块列表迟早会出事。不管是哪种情况核心都是同一个技术动作让构建脚本在特定条件下自动调整工程的模块参与范围。明白了目标和模块类型的关系方案选型就不会跑偏。3. 四种排除路径我最后选了脚本化3.1 手动改配置的静态方案最原始的方案是改根目录的 build-profile.json5。这个文件里有一个 modules 数组列出了工程参与构建的所有模块。手动把它注释掉再点 Sync 工程那个模块就消失了。缺点也很明显容易忘、容易漏而且每个人都要记住这个操作流程。还有一种静态方案是改 entry 模块的 oh-package.json5把 dependencies 里对指定模块的依赖项删掉。这个方法也能达到目的但同样依赖人肉记忆而且多人协作时经常出现有人改了没提交的混乱。我不否认静态方案简单直观但如果你经历过一次正式包发出去之后发现调试功能还在里面的事故就会明白自动化是刚需不是可选项。3.2 基于 hvigorfile 的动态方案既然 hvigorfile 是构建启动器且具备完整的 Node 能力那就可以在脚本里读取工程配置、分析参数、动态调整模块列表。这才是标题说的hvigorfile 构建脚本的使用真正有价值的地方。我调研过几种公开的思路最后筛选出两条可行路线第一脚本预处理 build-profile.json5。在 hvigor 正式启动任务流之前通过脚本修改 modules 数组把指定模块踢出去。这个方案的优点是干预得非常彻底效果等同于手动注释再 Sync但是自动化了。第二脚本调整 entry 的依赖配置。针对 HAR 类型的排除需求在 Release 构建时把依赖项从 entry 的 oh-package.json5 里临时移除。这个方案更精确只影响依赖链不会让模块从工程里消失调试时还能继续引用。两条路线没有绝对的好坏取决于你的场景。如果你的目的是这个模块在正式包里彻底不要出现我建议路线一如果你的目的是保留模块但临时解除与某个入口的依赖关系路线二更合适。4. 实操把排除逻辑写进根目录 hvigorfile4.1 准备阶段先想清楚触发条件我在实际项目里做的第一件事是确定触发条件。我当时的诉求很简单Debug 构建保留调试模块Release 构建排除它。所以我需要读取构建模式。hvigor 执行时可以通过命令行参数传入模式等信息。常见命令大致长这样hvigorw assembleHap --mode module -p productdefault -p buildModerelease --no-daemon这里的 buildMode 参数就是我们要抓的东西。在 hvigorfile 里可以通过解析 process.argv 拿到它。我把兼容环境变量的方式也做了定义一个环境变量 EXCLUDE_MODULE_DEBUG_TOOL如果设为 true则无论什么构建模式都排除指定模块。这样 CI 环境里也好控制。4.2 核心编码动态编辑 build-profile.json5接下来是核心代码。我用的技术栈是 Node.js 原生能力加一个 JSON5 解析库。build-profile.json5 支持注释普通 JSON.parse 解析不了所以必须用 json5 库读取。先写公共函数部分。我把这些逻辑放在一个独立文件里方便维护比如项目根目录下放一个 build-script-utils.js然后在 hvigorfile.ts 里引用。// build-script-utils.js const fs require(fs); const path require(path); const JSON5 require(json5); const ROOT_DIR __dirname; // 这个文件放在工程根目录下 function readJson5(filePath) { const content fs.readFileSync(filePath, utf-8); return JSON5.parse(content); } function writeJson5(filePath, data) { // 注意JSON5.stringify 不是所有版本都有我遇到的情况是 // 这个库只提供 stringify 方法但输出质量还行 const content JSON5.stringify(data, null, 2); fs.writeFileSync(filePath, content, utf-8); } function getBuildProfilePath() { return path.join(ROOT_DIR, build-profile.json5); } function removeModuleFromProfile(moduleName) { const profilePath getBuildProfilePath(); const profile readJson5(profilePath); const modules profile.modules || []; const beforeCount modules.length; profile.modules modules.filter(item { // 模块项可能是字符串也可能是对象取决于你的工程结构 if (typeof item string) { return item ! moduleName; } return item.name ! moduleName; }); const afterCount profile.modules.length; writeJson5(profilePath, profile); return beforeCount ! afterCount; }这里有个需要注意的点build-profile.json5 的 modules 数组在不同版本的 IDE 里生成的结构不完全一样。有的版本是纯字符串数组有的版本是对象数组带 name 和 srcPath 字段所以代码里要做兼容处理。我两种都见过写成兼容判断才不会翻车。与此同时我强烈建议在修改前先备份原文件构建结束后恢复。因为构建工作区的配置变更不应被提交到版本库。我在项目中是这么处理的function backupProfile() { const src getBuildProfilePath(); const backupPath src .bak; fs.copyFileSync(src, backupPath); } function restoreProfile() { const src getBuildProfilePath(); const backupPath src .bak; if (fs.existsSync(backupPath)) { fs.copyFileSync(backupPath, src); fs.unlinkSync(backupPath); } }4.3 接入 hvigor 任务流程有了工具函数接下来就是把它挂到 hvigorfile.ts 里。hvigor 在导出配置时允许通过 plugins 字段注入自定义能力。如果只是做构建开始前改配置这种操作可以注册一个非常朴素的插件利用 Node 脚本的执行时机来实现。我的做法是直接在导入阶段执行逻辑但通过判断参数避免每次都改动配置// 根目录 hvigorfile.ts import { appTasks } from ohos/hvigor-ohos-plugin; const { removeModuleFromProfile, backupProfile, restoreProfile } require(./build-script-utils); function getBuildMode() { const argv process.argv; const idx argv.indexOf(-p); while (idx ! -1 idx argv.length - 1) { const param argv[idx 1]; if (param.startsWith(buildMode)) { return param.split()[1]; } // 继续找下一个 -p } const env process.env.EXCLUDE_MODULE_DEBUG_TOOL; if (env true || env 1) { return release; } return debug; } const buildMode getBuildMode(); const excludeModuleName feature_debug; if (excludeModuleName buildMode release) { backupProfile(); const removed removeModuleFromProfile(excludeModuleName); console.log([hvigorfile] 构建模式${buildMode}, 排除模块${excludeModuleName}, 移除结果${removed}); } export default { system: appTasks, plugins: [] };这段代码是根目录 hvigorfile.ts核心逻辑是解析到 release 模式时把指定模块从 build-profile.json5 的 modules 数组里移除。因为 hvigor 在加载完 hvigorfile 之后才会开始 Sync等 Sync 发生时模块已经被排除掉了整个构建流程自然就不会再有任何与该模块相关的任务。调试的时候可能还想看看到底移除没有我在代码里加了日志输出。hvigor 构建时终端能看到这些日志。4.4 路线二解除 entry 对 HAR 模块的依赖如果你不想让模块从工程里消失只是想让 entry 不引用某个 HAR 模块可以改一下思路把目标从 build-profile.json5 换成 entry 模块的 oh-package.json5。我封装了另一个函数function removeDependency(moduleDir, depName) { const pkgPath path.join(ROOT_DIR, moduleDir, oh-package.json5); const pkg readJson5(pkgPath); const deps pkg.dependencies || {}; let removed false; if (deps[depName]) { delete deps[depName]; removed true; } writeJson5(pkgPath, pkg); return removed; }然后在 hvigorfile 里调用时改成移除依赖而不是移除模块。这个方案的好处是调试模块仍留在工程里IDE 里还能正常打开、编译只是 Release 时 entry 不再依赖它。但要注意这种方法有个副作用如果被移除依赖的模块同时还被别的模块引用那么别的模块仍然会把相关内容打进去。所以用路线二之前最好理清楚模块依赖关系确认被排除模块只被 entry 依赖。4.5 构建结束后的恢复无论走路线一还是路线二我都建议在构建完成后立刻恢复配置。hvigorfile 里没有现成的构建结束 hook可以让我简单挂回调但我在实际项目中用了一个取巧办法在 pipeline 的最后一个阶段扫描产物。如果检测到产物生成完毕就调用 restoreProfile() 恢复配置。更简单的做法是利用 Node 的 process.on(exit) 在进程退出前恢复process.on(exit, () { restoreProfile(); });这个做法虽然听起来有点粗暴但在实际项目中确实能覆盖大部分场景构建成功、失败、被用户中断Node 进程退出时都会触发 exit 事件。唯一照顾不到的是 IDE 强制杀进程的情况概率很低。如果担心极端情况也可以做成每 5 秒检查一次是否有 hvigor 进程存活的守护逻辑但工程化讲没必要exit 事件已经够用了。5. 验证和排错确保真的没打进包里5.1 怎么验证排除成功写完脚本后不要急着改代码先构建一次验证效果。我总结了几种验证手段按检查成本从低到高排列第一种看构建日志。脚本里打了日志release 构建时如果提示移除结果为 true说明配置改成功了。第二种看构建产物目录。以 HAP 为例构建完成后在模块的 build/default/outputs/ 目录下面会生成 HAP 文件。如果是排除 HAR 模块HAP 里不该看到该模块的任何产物目录。第三种解包检查。HAP 本质上是个 ZIP 包把扩展名改成 .zip 后可以直接解压。正常情况下HAR 的内容会被合入到对应模块的产物里。我在验证时重点搜索被排除模块的类名、资源目录和 .abc 文件。比如调试模块叫 feature_debug解压后如果搜不到 feature_debug 相关的任何文件那就是真排除了。还有个更狠的验证方式在 release 包里触发一下调试工具的入口看是不是提示找不到模块。这个属于运行时验证我在模拟器里测过确实点不进去了说明依赖链切得干净。5.2 常见问题速查表我在这个过程中整理了一份问题排查表格基本覆盖了可能翻车的场景症状可能原因解决办法模块还是被打进了 HAP模块被其他模块间接依赖用依赖分析工具查一下引用关系确认只有 entry 依赖它Release 构建报错找不到 xxx 模块排除太彻底其他位置还引用着它改用路线二只解除 entry 的依赖保留模块本身IDE 一重新 Sync模块又回来了hvigorfile 的修改没生效或配置被 IDE 重写检查是否 build 前执行脚本确认 IDE 的同步流程确实经过 hvigorfile动态共享包排除后运行时找不到 So 库HSP 被排除但代码里还有动态引用检查 import 路径把动态引用统一清理掉构建产物异常配置没恢复进程被杀exit 事件没触发手动删除 .bak 文件恢复原配置5.3 我踩过的几个坑第一个坑是 json5 的 stringify 格式问题。JSON5.stringify 生成的内容会把原本的注释全部丢掉如果团队里有人在 build-profile.json5 里写了注释来说明模块用途运行一次脚本注释就没了。我后来规定公共配置文件的注释要少写要写就写到单独文档里。第二个坑是版本差异。我在不同版本的 DevEco Studio 工程里测试发现 hvigorfile.ts 的默认导出结构有过变化。新版本的 hvigor 更推荐用插件注册的写法老版本则是直接导出 system 字段。在文章里我给的是兼容性比较强的写法如果你发现自己的工程结构跟代码对不上优先去查当前 IDE 版本对应的文档。第三个坑是命令行参数解析。一开始我只做了 process.argv 的 buildMode 判断结果在 CI 环境里构建命令格式不一样参数位置变了导致判断失效。后来我改成同时支持环境变量并加了兜底逻辑只要检测到 release 编译标志就执行排除。这个兜底逻辑不要太激进否则 debug 构建也可能误伤。第四个坑是恢复时机。我最初把恢复逻辑写在脚本开头准备在下一次构建时恢复上一轮的备份结果发现连续构建时第二轮构建会读取到被修改过的配置状态混乱。改成 process.on(exit) 之后这个问题彻底消失因为每次构建进程结束之前都会恢复。6. 进阶扩展把排除范围扩展到多 Target 组合如果你的工程用到了多 Target 能力情况会稍微复杂一点但思路可以无缝扩展。Target 可以理解为一套独立的构建配置组合每个 Target 可以指定不同的模块集合、产品名和构建参数。我在另一个模拟项目里试过这种用法根据传入的 target 名称决定从 modules 列表里排除哪几个模块。实现方式和前面几乎相同只需要在 getBuildMode() 之外再加一个 getTargetName()function getTargetName() { const argv process.argv; // 类似 buildMode 的解包逻辑可以放到公共函数里统一做 // 这里我以 product 参数为例 for (let i 0; i argv.length - 1; i) { if (argv[i] -p argv[i 1] argv[i 1].startsWith(product)) { return argv[i 1].split()[1]; } } return default; } const targetName getTargetName(); const excludeTargetMap { productA: [feature_debug], productB: [feature_share, feature_debug] }; const excludeList excludeTargetMap[targetName] || []; for (const modName of excludeList) { removeModuleFromProfile(modName); }这样写的好处是团队里新增一个产品变体时不需要去改构建脚本的核心逻辑只需要往 excludeTargetMap 里加一行映射。我把这个映射表放到独立配置里其他人也能轻松维护。另外如果你的模块是 HAR 且要排除时可以在 entry 的 module.json5 里也确认一下依赖声明。HAR 不像 HSP 有独立产物它的存在感完全体现在依赖关系里所以依赖关系的清理必须彻底否则编译期不报错运行期才可能出问题那时候排查成本就高了。关于排除指定模块这件事我最终沉淀下来的经验是先分清模块类型再决定动哪个文件。HAR 动依赖HSP 动发布产物集合通用场景直接动 build-profile.json5 的模块列表。把 hvigorfile 当成一个可以编程的构建入口而不是默认导出配置的静态占位文件很多构建定制需求其实都能在这个文件里解决。这套方案我在项目上用了很长一段时间再也没有出现过正式包带着调试工具的事故团队里其他同事也能通过环境变量和 target 映射表自行控制不需要有人专门守着发布流程。如果你也在头疼模块裁剪和打包控制不妨按这个思路试试。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询