首页
/ Halo 主题 UI 插件打包:ui-plugin-bundler-kit 的 provider 机制与 Vite/Rsbuild 主题构建配置实战

Halo 主题 UI 插件打包:ui-plugin-bundler-kit 的 provider 机制与 Vite/Rsbuild 主题构建配置实战

2026-09-08 16:17:45作者:宣海椒Queenly

@halo-dev/ui-plugin-bundler-kit 是 Halo 官方提供的前端构建配置工具包,为插件(Plugin)与主题(Theme)编写的 Console / User Center UI 插件统一生成构建产物。本文围绕 Halo 为“主题内嵌 UI 插件”这一能力引入的 provider: "plugin" | "theme" 选择模型,讲解主题构建的资源契约、manifest 默认路径、模块名与资源公共路径推导规则,并给出基于 Vite 与 Rsbuild 的可直接落地的配置示例。读完本文,你将掌握如何把一个 ui-plugin 前端工程挂载到某个主题名下、构建出符合 Halo 主题运行时要求、并与插件构建完全同构的 UI 资源。

为什么需要 provider:主题同样要“提供” Console / UC 界面

Halo 主题不仅能决定前台页面外观,还能通过一套与插件 UI 相同的 PluginModule 前端模块契约,为 Console 与 User Center 提供“由主题携带”的 UI 插件资源。其运行时的资源存放与暴露模型与插件不同(见该能力的设计文档):

  • 主题包内的产物存放在主题包资源树下:
{themeRoot}/{themeName}/ui-plugin/dist/**
  • Halo 运行时通过如下 URL 暴露这些静态资源:
/themes/{themeName}/ui-plugin/assets/**

然而在早期版本中,@halo-dev/ui-plugin-bundler-kit 只面向插件工程:它默认读取插件 manifest、生成插件产物文件名并配置插件资源的 public path。主题作者要么手工改写 Vite/Rsbuild 配置,要么直接放弃。因此 Halo 在 ui-plugin-bundler-provider 这一能力中要求:在统一的现代配置 API 上同时支持插件与主题两类 provider,让主题也能用同一套打包助手直接产出符合主题运行时契约的产物。对应的能力规格当前收录在 openspec/specs/ui-plugin-bundler-provider/spec.md,而实际实现位于 ui/packages/ui-plugin-bundler-kit

provider 模型:一个入口、两种提供方、默认保持插件行为

规格中最核心的取舍是:不新增 themeViteConfig / themeRsbuildConfig 之类的独立导出,而是在现代 Vite 与 Rsbuild 配置助手 viteConfig / rsbuildConfig 上增加一个可选的 provider 字段:

type Provider = "plugin" | "theme";

三条被明确约束的行为规则如下:

  1. 默认 provider 仍是 "plugin":调用方省略 provider 时,助手必须使用插件 provider 的默认值,保证存量工程零改动。
  2. 主题显式选用 provider: "theme" 后,助手切换为主题默认值。
  3. 遗留 helper 保持不变:被废弃的 HaloUIPluginBundlerKit 不提供任何主题行为。

这一语义在源码中可以直接印证。Vite 与 Rsbuild 助手都通过 getProvider() 读取配置并回退到插件:

// ui/packages/ui-plugin-bundler-kit/src/vite.ts
type Provider = "plugin" | "theme";

function getProvider(config?: ViteUserConfig): Provider {
  return config?.provider || "plugin";
}

Rsbuild 端 rsbuild.ts 的实现与此完全对称,而 manifest 路径也随 provider 一起解析:

function getManifestPath(provider: Provider, config?: ViteUserConfig) {
  if (config?.manifestPath) {
    return config.manifestPath;
  }
  return provider === "theme"
    ? DEFAULT_THEME_MANIFEST_PATH   // ../theme.yaml
    : DEFAULT_PLUGIN_MANIFEST_PATH; // ../src/main/resources/plugin.yaml
}

为什么不做自动探测

设计文档明确放弃了“根据目录里存在 theme.yaml 还是 plugin.yaml 自动推断 provider”的方案:自动探测会让构建行为随目录布局漂移,并可能意外影响存量插件工程。显式传参保证行为可预期,这也是 provider 字段语义简单、便于测试的原因。

遗留 helper 为什么不支持主题

被废弃的 legacy.ts 中的 HaloUIPluginBundlerKit 仍承担插件 IIFE 产物在旧生产链路下的兼容职责。若为它新增主题分支,一方面会扩大兼容矩阵,另一方面也会诱导新主题工程去采用已废弃的 API,因此规格约定它“不支持主题 provider”。

plugin provider 兼容性:存量行为原样保留

为插件场景服务的行为必须逐项保持不变,这是 provider 改造的底线:

  • 默认 manifest 路径:未传 manifestPath 时读取 ../src/main/resources/plugin.yaml。该常量定义于 constants/halo-plugin.ts
const DEFAULT_PLUGIN_MANIFEST_PATH = "../src/main/resources/plugin.yaml";
const DEFAULT_THEME_MANIFEST_PATH = "../theme.yaml";
  • 构建位置兼容:插件产物输出目录与“UI bundle 位置”有关,Halo 2.25.0 起插件 UI 资源迁入 ui 目录(此前为 console)。工具函数 utils/halo-plugin.ts 中的 getHaloPluginBundleLocation() 会依据 manifest spec.requires 可推导出的最低 Halo 版本决定落到 ui 还是 console,从而决定开发期输出目录 ../build/resources/main/{ui|console}
  • 输出产物约定:插件 provider 保留既有的 IIFE 库输出、全局模块名、外部依赖(EXTERNALS)与全局变量映射(GLOBALS),入口固定为 main.js、样式为 style.css

theme provider 的 manifest:从 ../theme.yaml 推导模块名与版本要求

当 provider 为 theme 时,manifest 默认路径切换为 ../theme.yaml,且仍可通过顶层 manifestPath 覆盖以适配非标准布局。

为保证不引入对整套 API-Client 生成模型的依赖,打包工具在内部定义了一个极简的 HaloThemeManifest 类型,只取 bundler 所需的最小字段——元数据名称与可选版本要求:

// ui/packages/ui-plugin-bundler-kit/src/utils/halo-plugin.ts
const THEME_MODULE_NAME_PREFIX = "theme:";

export interface HaloThemeManifest {
  metadata: { name: string };
  spec?: { requires?: string };
}

export function getHaloThemeModuleName(manifest: HaloThemeManifest) {
  return `${THEME_MODULE_NAME_PREFIX}${getManifestName(manifest)}`;
}

也就是说,当 theme.yamlmetadata.nameearth 时,打包器会把前端模块(全局/模块名)配置为 theme:earth,资源公共路径配置为 /themes/earth/ui-plugin/assets/

export function getHaloThemeAssetPublicPath(manifest: HaloThemeManifest) {
  return `/themes/${getManifestName(manifest)}/ui-plugin/assets/`;
}

完整校验仍由 Halo 运行时负责——打包工具只“尽力校验最少字段”,一旦缺字段,配置生成阶段即会失败。

theme provider 的构建输出默认值(Vite / Rsbuild)

主题 provider 的默认值(来源见 vite.tsrsbuild.ts 中各自的 getThemeProviderDefaults()):

维度 Vite Rsbuild
输出根目录 dist(dev/prod 一致) dist(dev/prod 一致)
模块/全局名 theme:{metadata.name} theme:{metadata.name}
资源公共路径(IIFE 场景) base: /themes/{name}/ui-plugin/assets/ output.publicPath: /themes/{name}/ui-plugin/assets/
主产物文件名 main.js + style.css main.js + style.css

源码中主题默认输出目录与模块名:

function getThemeProviderDefaults(manifestPath: string) {
  const manifest = getHaloThemeManifest(manifestPath);
  return {
    moduleName: getHaloThemeModuleName(manifest),
    outDir: { prod: DEFAULT_THEME_OUT_DIR, dev: DEFAULT_THEME_OUT_DIR },
    legacyBase: getHaloThemeAssetPublicPath(manifest),
    requires: getManifestRequires(manifest),
  };
}

Vite 端在 IIFE 格式下通过 lib.fileName: () => "main.js"cssFileName: "style" 固定产物文件名(见 vite.ts),Rsbuild 端则通过 output.filename 在 main chunk 上分别产出 main.jsstyle.css。两者都内置唯一的 Vue 编译插件(@vitejs/plugin-vue / @rsbuild/plugin-vue),无需调用方再注册一份。

复用插件 UI 的共享外部依赖

主题 UI 与插件 UI 运行在相同的 Console/UC 环境、遵循同一个 PluginModule 契约,因此主题 provider 复用插件 UI 的 external 与 globals 默认映射,避免出现重复的 Vue/运行时拷贝,并使产物体积与插件 UI 保持一致。

用户配置覆盖默认值

调用方传入的原生 vite / rsbuild 配置始终在 provider 默认值之后按构建器原生合并语义合入(Vite 用 mergeConfig,Rsbuild 用 mergeRsbuildConfig),例如 vite.ts

return defineConfig((env) => {
  const presetsConfig = presetsConfigFn(env);
  const userConfig =
    typeof config?.vite === "function" ? config.vite(env) : config?.vite || {};
  return mergeConfig(presetsConfig, userConfig);
});

规格同时强调:一旦用户覆盖改变了最终输出契约(入口、格式、public path、externals、文件名、资源或 manifest 一致性),产物正确性由调用方负责,工具包也不会声称该产物仍保有默认预设契约。

推荐的工程布局与最小化配置

主题 UI 插件工程应放在主题的 ui-plugin/ 目录下(布局说明见 包 README):

theme-root/
├── theme.yaml            # metadata.name 将决定模块名与资源 URL
└── ui-plugin/
    ├── package.json
    ├── src/index.ts      # 默认入口,default export PluginModule
    └── vite.config.ts    # 或 rsbuild.config.ts

安装依赖:

pnpm add @halo-dev/ui-plugin-bundler-kit
# Vite 用户
pnpm add vite @vitejs/plugin-vue
# Rsbuild 用户
pnpm add @rsbuild/core @rsbuild/plugin-vue

Vite 配置

// ui-plugin/vite.config.ts
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit/vite";

export default viteConfig({
  provider: "theme",
  vite: {
    // 自定义 Vite 配置;Vue 插件已内置,不要再注册 @vitejs/plugin-vue
  },
});

Rsbuild 配置

// ui-plugin/rsbuild.config.ts
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit/rsbuild";

export default rsbuildConfig({
  provider: "theme",
  rsbuild: {
    // 自定义 Rsbuild 配置;Vue 插件已内置,不要再注册 @rsbuild/plugin-vue
  },
});

主题 provider 会读取上级的 ../theme.yaml,输出到 dist,以 theme:{metadata.name} 注册模块,并让静态资源命中 /themes/{metadata.name}/ui-plugin/assets/Halo 只从主题包的 ui-plugin/dist/** 读取 UI 资源,因此成品主题中应保留 ui-plugin/dist 完整目录。

推荐脚本

{
  "scripts": {
    "dev": "vite dev --mode=development --watch",
    "build": "vite build"
  }
}

Rsbuild 场景对应 rsbuild dev --env-mode=development --watchrsbuild build

配置项速查

Vite 与 Rsbuild 助手接受同一组顶层配置(字段定义见 vite.tsrsbuild.ts):

字段 类型 默认值 说明
provider "plugin" | "theme" "plugin" UI 插件提供方类型
manifestPath string 插件:../src/main/resources/plugin.yaml;主题:../theme.yaml manifest 路径,覆盖默认布局
format "auto" | "iife" | "esm" "auto" 产物格式选择(见下节)
targetHaloVersion string 显式 ESM 且无法从 spec.requires 推导目标时必填
vue Vue 插件选项 透传给内置的 Vue 编译插件
vite / rsbuild 原生构建配置或函数 {} 在 provider 默认值之后合并

从源码看格式选择:auto / iife / esm 与 2.26.0 阈值

本仓库当前版本的打包器在“Provider format selection”之上进一步演进出了 format 三态选择(当前规格见 openspec/specs/ui-plugin-bundler-provider/spec.md)。auto(默认)会结合 manifest 的 spec.requires 自动决策:

  • spec.requires 是形如 MAJOR.MINOR.PATCH>=MAJOR.MINOR.PATCH 的简单稳定版本,且最低版本不低于 Halo 2.26.0 时,输出 ESM
  • 缺失、非法、通配或复合范围导致无法推导目标时,回退输出 IIFE 并打印解释性警告;
  • 目标低于 2.26.0 时同样选择兼容的 IIFE。

阈值常量与解析逻辑位于 utils/halo-plugin.ts

const UI_BUNDLE_MIN_HALO_VERSION = "2.25.0";
const ESM_PROVIDER_MIN_HALO_VERSION = "2.26.0";

需要刻意强推 ESM 而版本范围无法推导目标时,可显式给出目标版本:

export default viteConfig({
  provider: "theme",
  format: "esm",
  targetHaloVersion: "2.26.0",
  vite: {},
});

若显式选择 ESM 但 spec.requires 允许低于 2.26.0 的 Halo 安装该产物,打包器会输出强兼容性警告,但不会改写 manifest 或悄悄回退 IIFE。ESM 构建成功后会额外生成保留文件名 ui-plugin.json(记录 format、实际带内容哈希的入口与可选的启动样式表),该文件名被 bundler kit 保留,不应手工创建或复制。

覆盖默认 public path 的风险边界

设计文档特别提醒:主题资源公共路径默认值 /themes/{name}/ui-plugin/assets/ 是可被用户配置覆盖的,但错误地覆盖它会破坏动态 chunk 与已发射资源的相对寻址。如果你确实需要自定义(例如非标准目录布局),应同步修改主题侧的资源映射,并自行承担缓存与路由一致性责任。此外,工具内置的 Vue 编译插件只此一个:需要通过 vue 顶层字段下发编译器选项(如自定义元素 isCustomElement),若再往 plugins 数组塞一份 @vitejs/plugin-vue@rsbuild/plugin-vue,SFC 转换会被执行两次。

测试与文档佐证

该能力的单元测试覆盖在 ui/packages/ui-plugin-bundler-kit/src/tests

  • provider.spec.ts:验证 plugin/theme 两套 provider 生成的 Vite/Rsbuild 配置形状;
  • halo-plugin.spec.ts:覆盖 manifest 工具、bundle location 选择、自定义 manifest 路径、函数式用户配置合并等;
  • legacy.spec.ts:守住遗留 HaloUIPluginBundlerKit 的既有输出默认值。

结合仓库可进一步确认的调用链是:viteConfig(provider: "theme")getManifestPath() 返回 ../theme.yamlgetThemeProviderDefaults() 解析 HaloThemeManifest → 推导出模块名 theme:{name}、输出 dist、base /themes/{name}/ui-plugin/assets/mergeConfig() 合入用户 vite 配置。整条链路在 ui/packages/ui-plugin-bundler-kit/src/vite.tsrsbuild.ts 中均有可直接阅读的实现对应。

总而言之:给主题接入 UI 插件资源,只需让前端工程位于 theme-root/ui-plugin/、为 viteConfig/rsbuildConfig 显式声明 provider: "theme",再按约定保留 dist 产物目录——其余 manifest 解析、模块命名、资源公共路径与共享依赖外置都由 @halo-dev/ui-plugin-bundler-kit 一站完成。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391