组件的构建
为什么要构建(编译)
在经过一系列迭代之后, 我们满足了大部分需求,且在项目中运行良好。
接下来为了给更多的项目使用,我们需要把这个组件提出来,变成一个单独的 npm
包。
为什么要提炼成单独的 npm 包?
不然你为了共享就需要 cv 整个组件库到不同的项目里,这样每次更新都要重新 cv,而且容易出错
那么,我们可以直接把这些 vue
/ vue jsx
组件不经过任何编译,就直接发布 .vue
文件到 npm
上吗?
答案是既可以,也不可以,而且极其不推荐这么做!
为什么?
首先我们来回答一下,为什么可以
为什么“可以”这么做?
从技术角度看,npm
并不限制你发布什么类型的文件。你完全可以上传原始的 .vue
文件或包含 JSX 的 .tsx/.jsx
文件。
那么消费者在安装你了这个包之后,只要项目配置了相应的编译支持,例如通过 vite
的 @vitejs/plugin-vue
, @vitejs/plugin-vue-jsx
或者 webpack
的 vue-loader
去直接加载这些 .vue
文件,那么它们是可以被正确解析的。
但是你有没有发现,这样无形中添加了额外的编译步骤,同时也非常依赖项目中的编译器版本?
比如你 .vue
文件里面使用了 vue@3.3+
版本才加入的 defineOptions
编译宏,但是项目中使用的 vue
编译器版本是 3.2
,那么就会直接报错用不了了。
但是预先把 .vue
文件编译成 js
文件,这个就可以兼容老版本的 vue
了
比如你在 .vue
文件中使用了 defineOptions
defineOptions({
inheritAttrs: false,
customOptions: {
/* ... */
},
data() {
return {
version: '3.3+'
}
}
})
编译结果为:
const __sfc__ = /*@__PURE__*/Object.assign({
inheritAttrs: false,
customOptions: {
/* ... */
},
data() {
return {
version: '3.3+'
}
}
}, {
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();
const __returned__ = { }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
});
显然,这个编译结果,在低版本中 vue3
中也能兼容。
但为什么“也不可以”这么做(强烈不推荐)?
1. 依赖环境不一致
不同项目使用的打包工具、Babel
/ Vite
配置、vue
版本、tsconfig
等配置可能不一样。这会导致组件在某些项目中无法正确加载、解析或渲染。
2. 构建性能差
每个使用你这个 npm
包的项目都需要对 .vue
文件进行编译,增加了构建时间,浪费计算资源。发布前预编译, 直接引入产物跳过编译,能大大提高下游使用者的效率。
3. 无法 tree-shaking
源码形式往往不具备良好的 tree-shaking
能力,可能导致打包出来的文件变大,最终影响页面加载性能。
4. 类型定义丢失或不准确
如果你用 TypeScript
开发组件但不编译,使用者可能得不到准确的类型提示。尤其是 .vue
文件本身包含 script
/template
/style
,类型推导更加复杂,依赖 vscode vue
插件感应。但是预编译出对应的 d.ts
类型定义文件,那么编辑器原生就支持这个智能感应,无需依赖其他插件。
5. 发布 npm 包会暴露内部结构
发布源码等于把你内部的组织结构完全暴露出来。使用者很容易误用“私有”接口或非 API 级别的代码,增加维护负担。
6. 无法在 Node 中直接执行
有些服务端渲染或测试环境(如 jest
)并不会处理 .vue
文件,这会导致包无法运行或测试失败。
开始构建
在打包 Vue
组件库时,请使用 Vite的库模式 来进行构建,因为这是官方维护的推荐方式。
快速示例见本项目的
packages/ice-ui
目录
Vite 构建
通用的库文件配置
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'),
// 公开的全局变量 umd/iife 用
name: 'ui',
// 包文件输出的名称
fileName: 'index',
},
rollupOptions: {
// 把 vue 作为 外部依赖,不加这个,vite/rollup 会把 vue 所有引用到的源代码,
// 全部打入产物中去,这没有必要因为组件本来就需要在已经有 vue 的环境运行
external: ['vue'],
output: {
// umd 用,假设全局已经有 vue 对象
globals: {
vue: 'Vue',
},
},
},
},
})
但是这个这个配置,只能打包 ts
/js
文件,不能打包 vue
组件
因为目前 vite
目前还不认识 .vue
文件是什么,想让它认识需要注册插件
这里就要分为 vue3
和 vue2
两种情况
Vue3
想要通过编写..vue
文件来构建 vue3
组件,需要使用 @vitejs/plugin-vue
,加入你还想使用 jsx
来构建组件库,那还需要安装 @vitejs/plugin-vue-jsx
Vue2
同上
构建产物
在 vite
库模式构建产物中,默认会生成 esm
和 umd
格式的 js
文件,假如写了样式的话还会生成 css
其中 umd
格式是给 cdn
/传统 ssr
场景下准备的产物,esm
格式是给正常引入 esm
包/和现代 ssr
场景下准备的产物
生成 .d.ts
默认是不会生成 .d.ts
类型定义文件的,需要安装 vite-plugin-dts
来生成 .d.ts
一般场景下的配置如下
plugins: [
dts(
{
// 生效的 tsconfig.json 路径, 不设置默认 tsconfig.json
tsconfigPath: './tsconfig.app.json',
// 组件的目录
entryRoot: './lib',
},
),
],
生成结果
通过这些配置,你再运行 vite build
之后得到的产物结构就如下所示
index.css
index.d.ts
index.js
index.umd.cjs
....
接下来就需要去进行测试验证了