组件的构建
为什么要构建(编译)
在经过一系列迭代之后, 我们满足了大部分需求,且在项目中运行良好。
接下来为了给更多的项目使用,我们需要把这个组件提出来,变成一个单独的 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 文件里面使用了 [email protected]+ 版本才加入的 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
....接下来就需要去进行测试验证了