Skip to content

组件的构建

当组件在项目里已经跑得不错,下一步通常就是把它提炼成一个可复用、可发布的包。

这时“构建”就开始变得重要。

因为你不再只是关心“当前项目能不能跑”,而是要回答这些问题:

  • 别人的项目能不能稳定消费你的组件
  • 你的组件要以什么格式输出
  • 类型、样式和运行时依赖要怎样一起交付
  • 你的包是否会把不该暴露的实现细节泄漏出去

所以组件构建的本质,不只是“把代码打包一下”,而是把组件从项目内部实现,转换成可分发的公共产物

为什么要构建

在单个项目里直接复制组件当然也能用,但这会带来明显问题:

  • 组件更新要反复复制
  • 不同项目会逐渐产生分叉
  • 类型、样式、依赖版本更难统一

把组件提炼成单独的 npm 包之后,分发和升级才会有稳定入口。

能不能直接发布 .vue 源码

答案是:技术上可以,工程上通常不推荐。

为什么“可以”

从技术角度看,npm 并不限制你上传什么文件。你完全可以发布原始的 .vue 文件,或者包含 JSX 的 .tsx/.jsx 文件。

只要使用者项目里已经配置好了相应的编译支持,例如:

  • @vitejs/plugin-vue
  • @vitejs/plugin-vue-jsx
  • vue-loader

那么这些文件理论上就可以被继续编译和运行。

所以“能不能发布源码”这个问题,单从技术可行性上说,答案是能。

为什么“不推荐”

问题出在“能跑”和“适合作为公共包分发”之间差得很远。

1. 过度依赖使用方的编译环境

一旦你发布 .vue 源码,使用者项目就必须具备兼容的编译器版本和配置。

例如你在源码里用了 vue@3.3+ 才支持的 defineOptions,而使用者的编译环境停留在更低版本,就会直接出问题。

2. 构建成本转嫁给下游

每个使用你组件库的项目,都要重新编译这些源码。
这会增加下游构建时间,也会放大不同工具链之间的差异。

3. 类型交付不完整

如果你不额外处理类型声明,使用者往往拿不到完整、稳定的 .d.ts 支持。

4. 公共 API 边界变模糊

发布源码通常会把内部目录结构也一起暴露出去,使用者更容易误 import 你不打算公开维护的内部模块。

5. Node / SSR / 测试环境兼容性更差

不是所有环境都会原生处理 .vue 文件。
源码发布在服务端、测试工具、老旧构建链路里更容易踩坑。

“预编译”解决了什么

把组件预编译成 .js / .css / .d.ts 后,本质上是在提前帮下游完成这些事:

  • SFC 解析
  • 模板编译
  • 样式提取
  • 类型声明生成

这能显著降低消费者的环境要求,也能让你的包边界更清晰。

但要注意:

预编译并不等于自动兼容所有低版本 Vue。

最终能不能运行,仍然取决于:

  • 运行时依赖是否兼容
  • helper 是否存在
  • 输出格式是否与使用方环境匹配

例如某些新宏虽然被编译掉了,但若产物依赖较新的运行时 helper,低版本 Vue 运行时依然可能不兼容。

开始构建

在打包 Vue 组件库时,优先推荐使用 Vite 的库模式

这是当前 Vue 生态里相对自然、维护成本较低的一条路线。

快速示例见本项目的 packages/ice-ui 目录

基础配置长什么样

一个通用的库模式配置大致如下:

js
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'

const __dirname = dirname(fileURLToPath(import.meta.url))

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'lib/index.ts'),
      name: 'ui',
      fileName: 'index',
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
})

这里最值得理解的两个点

1. lib.entry

这是你对外的库入口,决定最终产物从哪里开始收集模块依赖。

2. external: ['vue']

这非常重要。

因为组件库通常不应该把 vue 自己一并打进产物里,否则就会出现:

  • 包体积膨胀
  • 运行时重复实例
  • 消费方和组件库各自持有不同 Vue 副本

所以大多数情况下,vue 应该作为外部依赖保留。

为什么只靠这段配置还不够

上面的配置只能很好地处理 ts / js 文件。

如果你的组件库包含 .vue 组件,Vite 还需要对应插件来理解 SFC。

Vue 3

如果组件库是 Vue 3 主线,通常需要:

  • @vitejs/plugin-vue
  • 如果用了 JSX,再加 @vitejs/plugin-vue-jsx

Vue 2

Vue 2 已进入 EOL,本节仅用于维护历史项目。

  • vite-plugin-vue2
  • vite-plugin-vue2-jsx

构建产物通常长什么样

在 Vite 库模式下,常见输出会包括:

  • esm 格式的 JavaScript
  • umd 或兼容构建格式
  • 组件样式文件
  • 类型声明文件

例如:

bash
index.css
index.d.ts
index.js
index.umd.cjs

这些文件分别解决不同问题:

  • index.js:现代构建工具和 ESM 消费
  • index.umd.cjs:兼容 CommonJS / 某些传统环境
  • index.css:样式交付
  • index.d.ts:类型交付

类型声明为什么不能省

默认情况下,Vite 不会自动生成 .d.ts

如果你想让组件库在 TypeScript 项目里有完整的智能提示和类型校验,通常需要额外使用 vite-plugin-dts

常见配置如下:

ts
plugins: [
  dts({
    tsconfigPath: './tsconfig.app.json',
    entryRoot: './lib',
  }),
]

这里最重要的不是记配置,而是记住:
公共组件库的交付物不只有运行时代码,还包括类型契约。

构建阶段真正要保证什么

一个“能发布”的组件库构建,至少要保证这几件事:

  1. 消费方不需要理解你的内部源码结构
  2. vue 这类宿主依赖不会被错误打包进去
  3. 样式可以被稳定引入
  4. 类型声明完整可用
  5. 输出格式能匹配主要使用场景

如果这些做不到,构建就还只是“能生成文件”,不算真正完成交付。

一句话理解

组件构建的目标,不是把源码机械压缩成几个文件,而是把组件变成一个对外稳定、可安装、可类型推断、可长期维护的公共接口

下一步

构建完成之后,下一步就应该进入测试和发布:

  1. 组件的测试
  2. 组件的发布

这样整条组件库链路才算真正闭合。

Released under the CC BY-NC-SA 4.0 License.