跳到主要内容

库模式开发

除了构建 Web 应用,Vite 还可以用来开发 JavaScript 库。库模式(Library Mode)是 Vite 提供的一种特殊构建模式,专门用于打包发布到 npm 的库文件。

什么时候需要库模式?

如果你正在开发以下类型的项目,库模式是合适的选择:

  • UI 组件库:如按钮、表单、布局等可复用组件
  • 工具函数库:如日期处理、字符串操作、数据验证等
  • 框架插件:如 Vue 插件、React Hooks 等
  • SDK 或 API 客户端:封装第三方服务接口

库模式与应用模式的关键区别在于:

特性应用模式库模式
入口文件index.htmlJavaScript/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 可能与用户项目的样式冲突。解决方案:

  1. 使用 CSS 前缀
.my-lib-button { }
.my-lib-input { }
  1. 使用 CSS Modules:类名自动生成唯一标识。

  2. 使用 BEM 命名规范:减少冲突概率。

Tree-shaking 优化

确保库支持 tree-shaking:

  1. 使用 ESM 格式
  2. 配置 sideEffects
  3. 避免默认导出整个对象
// ❌ 不好:无法 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

核心原则

  1. 保持轻量:外部化所有依赖,不要打包进库
  2. 多格式输出:提供 ESM、UMD/CJS 格式
  3. 类型支持:提供 TypeScript 类型定义
  4. Tree-shaking:使用命名导出,配置 sideEffects
  5. 文档完善:README 包含安装、使用、API 文档

package.json 检查清单

  • name - 包名符合规范
  • version - 版本号正确
  • type - 模块类型("module"
  • files - 包含正确的发布文件
  • main - CommonJS 入口
  • module - ESM 入口
  • types - 类型定义入口
  • exports - 现代化导出配置
  • sideEffects - Tree-shaking 配置
  • peerDependencies - 对等依赖
  • repository - 仓库地址
  • license - 开源许可证

参考资源