库模式开发
除了构建 Web 应用,Vite 还可以用来开发 JavaScript 库。库模式(Library Mode)是 Vite 提供的一种特殊构建模式,专门用于打包发布到 npm 的库文件。
什么时候需要库模式?
如果你正在开发以下类型的项目,库模式是合适的选择:
- UI 组件库:如按钮、表单、布局等可复用组件
- 工具函数库:如日期处理、字符串操作、数据验证等
- 框架插件:如 Vue 插件、React Hooks 等
- SDK 或 API 客户端:封装第三方服务接口
库模式与应用模式的关键区别在于:
| 特性 | 应用模式 | 库模式 |
|---|---|---|
| 入口文件 | index.html | JavaScript/TypeScript 文件 |
| 输出格式 | 针对浏览器优化 | 多种格式(ESM、UMD、CJS) |
| 依赖处理 | 打包所有依赖 | 外部化依赖 |
| CSS 处理 | 提取并压缩 | 单独输出 CSS 文件 |
| package.json | 应用配置 | 库发布配置 |
基本配置
项目结构
一个典型的库项目结构如下:
my-lib/
├── src/
│ ├── index.ts # 主入口
│ ├── components/ # 组件目录
│ │ ├── Button.ts
│ │ └── Input.ts
│ └── utils/ # 工具函数
│ └── helpers.ts
├── vite.config.ts # Vite 配置
├── package.json # 包配置
├── tsconfig.json # TypeScript 配置
└── README.md
vite.config.ts 配置
使用 build.lib 选项启用库模式:
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
build: {
lib: {
// 入口文件
entry: resolve(__dirname, 'src/index.ts'),
// 库的全局变量名(UMD 格式使用)
name: 'MyLib',
// 输出文件名(会自动添加格式后缀)
fileName: 'my-lib',
},
rollupOptions: {
// 外部化依赖,不要打包进库
external: ['vue', 'react'],
output: {
// 为外部化的依赖提供全局变量
globals: {
vue: 'Vue',
react: 'React',
},
},
},
},
})
入口文件编写
入口文件负责导出库的公共 API:
// src/index.ts
// 导出组件
export { Button } from './components/Button'
export { Input } from './components/Input'
// 导出工具函数
export { formatDate, parseDate } from './utils/helpers'
// 导出类型
export type { ButtonProps, InputProps } from './types'
// 默认导出(可选)
export { default as MyLib } from './MyLib'
设计原则:
- 只导出库的公共 API,内部实现细节不要暴露
- 合理组织导出结构,便于用户按需导入
- 类型定义和实现一起导出,提供良好的开发体验
构建输出
运行 vite build 后,默认会生成两种格式:
vite build
# 输出:
# dist/my-lib.js (ES Module 格式)
# dist/my-lib.umd.cjs (UMD 格式)
输出格式详解
ES Module (ESM)
现代 JavaScript 的标准模块格式,推荐用于:
- 现代浏览器
- Node.js(ESM 支持)
- 打包工具(Webpack、Vite、Rollup 等)
// 用户使用
import { Button } from 'my-lib'
UMD (Universal Module Definition)
兼容多种模块系统的格式,适用于:
- 通过
<script>标签直接引入 - AMD 环境(RequireJS)
- CommonJS 环境(旧版 Node.js)
<!-- 浏览器中使用 -->
<script src="https://unpkg.com/my-lib/dist/my-lib.umd.cjs"></script>
<script>
// 全局变量 MyLib 可用
const button = new MyLib.Button()
</script>
CommonJS (CJS)
Node.js 传统模块格式,用于:
- 旧版 Node.js 环境
- 不支持 ESM 的工具
// 用户使用
const { Button } = require('my-lib')
配置输出格式
通过 formats 选项指定输出格式:
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
fileName: 'my-lib',
// 可选值:'es' | 'umd' | 'cjs' | 'iife'
formats: ['es', 'cjs'],
},
},
})
不同格式组合的默认输出:
| formats | 输出文件 |
|---|---|
['es', 'umd'] | my-lib.js, my-lib.umd.cjs |
['es', 'cjs'] | my-lib.js, my-lib.cjs |
['es', 'umd', 'cjs'] | my-lib.js, my-lib.umd.cjs, my-lib.cjs |
['iife'] | my-lib.iife.js |
IIFE 格式
立即执行函数表达式,适合直接在浏览器中使用:
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
fileName: 'my-lib',
formats: ['iife'],
},
},
})
package.json 配置
正确配置 package.json 对于库的发布和使用至关重要。
基本配置
{
"name": "my-lib",
"version": "1.0.0",
"description": "A modern JavaScript library",
"author": "Your Name",
"license": "MIT",
"type": "module",
"files": [
"dist",
"README.md",
"LICENSE"
],
"main": "./dist/my-lib.umd.cjs",
"module": "./dist/my-lib.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.umd.cjs",
"types": "./dist/index.d.ts"
},
"./style.css": "./dist/style.css"
},
"sideEffects": [
"**/*.css"
],
"keywords": ["vue", "components", "ui"],
"repository": {
"type": "git",
"url": "https://github.com/user/my-lib.git"
},
"peerDependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"vite": "^8.0.0",
"typescript": "^5.0.0",
"vue": "^3.0.0"
}
}
关键字段说明
type:指定包的模块类型。设为 "module" 表示使用 ESM。
files:发布到 npm 时包含的文件。通常只需要 dist 目录。
main:CommonJS 入口,供 require() 使用。
module:ES Module 入口,供 import 使用。
types:TypeScript 类型定义文件入口。
exports:现代化的入口定义方式,支持条件导出。
{
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.umd.cjs",
"types": "./dist/index.d.ts"
},
"./style.css": "./dist/style.css",
"./utils": {
"import": "./dist/utils.js",
"require": "./dist/utils.cjs"
}
}
}
sideEffects:告诉打包工具哪些文件有副作用,用于 tree-shaking。
{
"sideEffects": false,
// 或者
"sideEffects": ["**/*.css", "**/*.vue"]
}
peerDependencies:对等依赖,由使用者安装。
{
"peerDependencies": {
"vue": "^3.0.0",
"react": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
}
多入口配置
对于大型库,可能需要多个独立的入口点。
配置方式
// vite.config.ts
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
build: {
lib: {
entry: {
// 主入口
'my-lib': resolve(__dirname, 'src/index.ts'),
// 独立模块入口
'utils': resolve(__dirname, 'src/utils/index.ts'),
'components': resolve(__dirname, 'src/components/index.ts'),
},
name: 'MyLib',
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue',
},
},
},
},
})
package.json 配置
{
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.js",
"require": "./dist/utils.cjs",
"types": "./dist/utils/index.d.ts"
},
"./components": {
"import": "./dist/components.js",
"require": "./dist/components.cjs",
"types": "./dist/components/index.d.ts"
},
"./style.css": "./dist/style.css"
}
}
用户使用方式
// 导入主入口
import { something } from 'my-lib'
// 导入独立模块(支持 tree-shaking)
import { formatDate } from 'my-lib/utils'
import { Button } from 'my-lib/components'
// 导入样式
import 'my-lib/style.css'
CSS 处理
如果库包含样式,需要特别注意处理方式。
CSS 输出
Vite 会自动将库中导入的 CSS 提取为单独的文件:
// vite.config.ts
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
fileName: 'my-lib',
// 自定义 CSS 文件名
cssFileName: 'style',
},
},
})
构建后会生成 dist/style.css。
在库中使用 CSS
// src/index.ts
import './styles/index.css' // 导入全局样式
export { Button } from './components/Button'
或者在每个组件中单独导入:
// src/components/Button.ts
import './Button.css'
export function Button() {
// ...
}
用户引入样式
// package.json
{
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.cjs"
},
"./style.css": "./dist/style.css"
}
}
// 用户代码
import { Button } from 'my-lib'
import 'my-lib/style.css'
CSS Modules
如果使用 CSS Modules:
// src/components/Button.ts
import styles from './Button.module.css'
export function Button() {
return `<button class="${styles.button}">Click me</button>`
}
注意:CSS Modules 的类名是动态生成的,需要在库的文档中说明。
CSS 预处理器
Vite 支持在库中使用 Sass、Less 等预处理器:
// 安装 Sass
// npm install -D sass
// src/styles/index.scss
$primary-color: #42b983;
.button {
background: $primary-color;
}
// src/index.ts
import './styles/index.scss'
TypeScript 类型定义
对于 TypeScript 库,需要生成并发布类型定义文件。
配置 tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationDir": "./dist",
"emitDeclarationOnly": true,
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
构建脚本
{
"scripts": {
"build": "vite build && tsc --emitDeclarationOnly",
"build:types": "tsc --emitDeclarationOnly"
}
}
使用 vite-plugin-dts
更方便的方式是使用 vite-plugin-dts 插件:
npm install -D vite-plugin-dts
// vite.config.ts
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
dts({
// 配置选项
insertTypesEntry: true, // 自动在 package.json 的 types 字段添加入口
}),
],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
fileName: 'my-lib',
},
},
})
外部化依赖
库模式最重要的原则是:不要把依赖打包进库里。
为什么要外部化?
假设你的库依赖 lodash:
- 不外部化:
lodash的全部代码会被打包进你的库,导致体积膨胀 - 外部化:用户安装你的库时,自己安装
lodash,库体积保持精简
配置方式
// vite.config.ts
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
fileName: 'my-lib',
},
rollupOptions: {
// 外部化所有依赖
external: [
'vue',
'react',
'lodash-es',
// 正则匹配所有 node_modules
/^node:.*/,
],
output: {
globals: {
vue: 'Vue',
react: 'React',
'lodash-es': '_',
},
},
},
},
})
自动外部化依赖
使用 rollup-plugin-node-externals 自动外部化所有依赖:
npm install -D rollup-plugin-node-externals
import { defineConfig } from 'vite'
import externals from 'rollup-plugin-node-externals'
export default defineConfig({
plugins: [externals()],
build: {
lib: {
// ...
},
},
})
peerDependencies vs dependencies
peerDependencies:框架类依赖(vue、react),要求用户必须安装。
dependencies:库运行必需的依赖,会自动安装,但要外部化不打包。
{
"peerDependencies": {
"vue": "^3.0.0"
},
"dependencies": {
"lodash-es": "^4.17.21"
},
"devDependencies": {
"vite": "^8.0.0"
}
}
开发与测试
开发模式
在开发库时,通常需要一个测试/演示页面:
my-lib/
├── src/
│ └── index.ts # 库源码
├── example/ # 演示/测试页面
│ ├── index.html
│ └── main.ts
└── vite.config.ts
<!-- example/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>MyLib Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
// example/main.ts
import { Button, Input } from '../src'
import '../src/styles/index.css'
// 使用库组件进行测试
document.getElementById('app').innerHTML = `
${Button({ text: 'Click Me' })}
`
package.json 脚本
{
"scripts": {
"dev": "vite serve example",
"build": "vite build",
"build:watch": "vite build --watch",
"preview": "vite preview",
"type-check": "tsc --noEmit",
"lint": "eslint src --ext .ts,.tsx",
"test": "vitest",
"prepublishOnly": "npm run build && npm run type-check"
}
}
单元测试
使用 Vitest 进行单元测试:
npm install -D vitest @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
})
// src/__tests__/formatDate.test.ts
import { describe, it, expect } from 'vitest'
import { formatDate } from '../utils/formatDate'
describe('formatDate', () => {
it('should format date correctly', () => {
const date = new Date('2024-01-15')
expect(formatDate(date, 'YYYY-MM-DD')).toBe('2024-01-15')
})
})
发布流程
1. 检查构建产物
# 构建
npm run build
# 检查输出
ls -la dist/
# 应该看到:
# my-lib.js
# my-lib.umd.cjs
# style.css
# index.d.ts
2. 版本管理
# 更新版本号
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
3. 发布到 npm
# 登录 npm
npm login
# 发布
npm publish
# 发布到 npm(公开包)
npm publish --access public
# 发布到私有仓库
npm publish --registry=https://your-private-registry.com
4. CI/CD 自动发布
# .github/workflows/publish.yml
name: Publish to npm
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npm test
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
常见问题
环境变量处理
在库模式中,import.meta.env.* 会在构建时被静态替换:
// 源代码
const version = import.meta.env.VITE_VERSION
// 构建后
const version = "1.0.0"
如果希望环境变量由使用者决定,使用 process.env:
// 不会被替换,由运行时决定
const nodeEnv = process.env.NODE_ENV
样式隔离
库的 CSS 可能与用户项目的样式冲突。解决方案:
- 使用 CSS 前缀:
.my-lib-button { }
.my-lib-input { }
-
使用 CSS Modules:类名自动生成唯一标识。
-
使用 BEM 命名规范:减少冲突概率。
Tree-shaking 优化
确保库支持 tree-shaking:
- 使用 ESM 格式
- 配置
sideEffects - 避免默认导出整个对象
// ❌ 不好:无法 tree-shake
export default {
Button,
Input,
formatDate,
}
// ✅ 好:支持 tree-shake
export { Button }
export { Input }
export { formatDate }
处理 Node.js 内置模块
如果库需要使用 Node.js 内置模块,需要特殊处理:
// vite.config.ts
export default defineConfig({
build: {
lib: {
// ...
},
rollupOptions: {
external: ['fs', 'path', 'crypto'],
output: {
globals: {
fs: '{}', // 浏览器环境中为空对象
path: '{}',
},
},
},
},
})
或者使用 polyfill:
import { defineConfig } from 'vite'
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'
export default defineConfig({
plugins: [
{
name: 'node-polyfill',
config() {
return {
optimizeDeps: {
esbuildOptions: {
plugins: [NodeGlobalsPolyfillPlugin()],
},
},
}
},
},
],
})
最佳实践总结
项目结构
my-lib/
├── src/
│ ├── index.ts # 主入口,导出公共 API
│ ├── components/ # 组件
│ ├── utils/ # 工具函数
│ ├── types/ # 类型定义
│ └── __tests__/ # 测试文件
├── example/ # 演示/测试页面
├── dist/ # 构建输出(发布时包含)
├── vite.config.ts
├── tsconfig.json
├── package.json
└── README.md
核心原则
- 保持轻量:外部化所有依赖,不要打包进库
- 多格式输出:提供 ESM、UMD/CJS 格式
- 类型支持:提供 TypeScript 类型定义
- Tree-shaking:使用命名导出,配置
sideEffects - 文档完善:README 包含安装、使用、API 文档
package.json 检查清单
-
name- 包名符合规范 -
version- 版本号正确 -
type- 模块类型("module") -
files- 包含正确的发布文件 -
main- CommonJS 入口 -
module- ESM 入口 -
types- 类型定义入口 -
exports- 现代化导出配置 -
sideEffects- Tree-shaking 配置 -
peerDependencies- 对等依赖 -
repository- 仓库地址 -
license- 开源许可证