9.1 应用 vs 库
| 维度 | 应用构建 | 库构建 |
|---|---|---|
| 产物 | index.html + 资源 | dist/index.js + .d.ts |
| 入口 | index.html | src/index.ts(JS/TS 模块) |
| 是否内联依赖 | 内联(node_modules 打进 bundle) | 外部化(依赖由用户提供) |
| 模块格式 | ES module(浏览器) | ESM + CJS + UMD(适配多种消费者) |
| Minify | 通常开 | 通常关(留给用户) |
9.2 最小库配置
// vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib', // UMD / IIFE 全局变量名
formats: ['es', 'cjs', 'umd'],
fileName: (format) => `my-lib.${format}.js`,
},
rollupOptions: {
external: ['vue'], // 不把 vue 打进库
output: {
globals: { vue: 'Vue' }, // UMD 下 vue 绑定到 window.Vue
},
},
},
});
9.3 格式的选择
- es(ES Module)
- 现代打包器和浏览器首选,支持 tree-shaking。几乎所有库都要输出。
- cjs(CommonJS)
- 给老 Node 项目和某些工具用。若库只面向 ESM 生态(如只给 Vite/Next 用)可省略。
- umd(Universal Module Definition)
- 能在 AMD / CommonJS / 浏览器全局用的万用格式——给
<script>直接引的场景。 - iife(Immediately Invoked Function Expression)
- 浏览器 script 标签直接跑,无 AMD/CJS 检测——比 UMD 更简单的浏览器分发。
9.4 external 与 peerDependencies
假设你在写 React 组件库:用户项目里已经有 React,你不应该把 React 再打进库,否则:
- 包体积翻倍
- 两份 React 冲突(hook 失效)
正确做法:把 React 声明为 peerDependencies + 构建时 external:
// package.json
{
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"devDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
// vite.config.ts
rollupOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'],
}
更通用的做法:自动把所有 peerDependencies 标为 external:
import pkg from './package.json' with { type: 'json' };
rollupOptions: {
external: [
...Object.keys(pkg.peerDependencies ?? {}),
// regex 匹配子路径,如 react/jsx-runtime
/^react($|\/)/, /^react-dom($|\/)/,
],
}
9.5 生成类型声明(.d.ts)
TS 库必须输出 .d.ts 才能让用户享受类型提示。用 vite-plugin-dts:
$ npm i -D vite-plugin-dts
import dts from 'vite-plugin-dts';
defineConfig({
plugins: [
dts({
insertTypesEntry: true,
include: ['src/**/*.ts', 'src/**/*.tsx'],
exclude: ['src/**/*.test.ts'],
}),
],
});
构建后 dist/ 会多出 index.d.ts 与各模块的 .d.ts,package.json 里记得配置 types 字段。
9.6 package.json:现代双格式 exports
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"main": "./dist/my-lib.cjs.js",
"module": "./dist/my-lib.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/my-lib.es.js",
"require": "./dist/my-lib.cjs.js"
},
"./styles.css": "./dist/style.css"
},
"files": ["dist"],
"sideEffects": false
}
为什么要 exports 字段?
Node 和现代打包器优先读 exports(package.json 子路径导出地图)。它能:同一个 import 根据消费环境(ESM/CJS/TS)返回不同入口;限制只有指定子路径可导入,防止用户依赖内部文件。
9.7 样式处理
库自带 CSS 时,vite build --mode lib 默认会抽成 dist/style.css。让用户显式 import:
// 用户代码
import 'my-lib';
import 'my-lib/styles.css';
或把 CSS 注入 JS 自动加载(不推荐用于组件库,会限制定制)。
9.8 多入口
库有多个独立子模块(如 my-lib/hooks、my-lib/components):
build: {
lib: {
entry: {
index: 'src/index.ts',
hooks: 'src/hooks/index.ts',
utils: 'src/utils/index.ts',
},
formats: ['es', 'cjs'],
},
}
package.json 的 exports 也要加对应子路径:
"exports": {
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
"./hooks": { "import": "./dist/hooks.js", "types": "./dist/hooks.d.ts" },
"./utils": { "import": "./dist/utils.js", "types": "./dist/utils.d.ts" }
}
9.9 发布到 npm
$ npm run build
$ npm publish --access public # 公开包首发加 --access public
# 验证将要发布什么
$ npm pack --dry-run
files 字段控制被打包进 tarball 的文件——只放 dist/,源码不必包含(有 source map 即可调试)。
9.10 完整例子
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import pkg from './package.json' with { type: 'json' };
export default defineConfig({
plugins: [react(), dts({ insertTypesEntry: true })],
build: {
lib: {
entry: 'src/index.ts',
name: 'MyComponents',
fileName: (fmt) => `index.${fmt}.js`,
formats: ['es', 'cjs'],
},
sourcemap: true,
rollupOptions: {
external: [
...Object.keys(pkg.peerDependencies ?? {}),
/^react($|\/)/,
/^react-dom($|\/)/,
],
},
},
});
9.11 小结
- 库模式靠
build.lib+rollupOptions+external三件套配置。 - peerDependencies 里的包务必 external,避免重复打包和版本冲突。
vite-plugin-dts生成类型声明;package.json 的exports字段配置现代子路径。- 多入口用
entry对象形式;样式文件抽成独立 style.css 让用户显式引入。