Chapter 09

库模式(Library Mode)

用 Vite 打包一个要发布到 npm 的库——输出多种模块格式、声明文件、外部化依赖,一步到位。

9.1 应用 vs 库

维度应用构建库构建
产物index.html + 资源dist/index.js + .d.ts
入口index.htmlsrc/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 声明为 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/hooksmy-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 小结