Rollup 最佳实践 #

库开发最佳实践 #

目录结构 #

text
my-library/
├── src/
│   ├── index.ts           # 入口文件
│   ├── components/        # 组件目录
│   │   ├── index.ts
│   │   ├── Button.ts
│   │   └── Input.ts
│   ├── utils/             # 工具函数
│   │   ├── index.ts
│   │   └── helpers.ts
│   └── types/             # 类型定义
│       └── index.ts
├── dist/                  # 输出目录
│   ├── my-lib.cjs.js      # CommonJS
│   ├── my-lib.esm.js      # ES Module
│   ├── my-lib.umd.js      # UMD
│   └── my-lib.d.ts        # 类型声明
├── tests/                 # 测试文件
├── rollup.config.js       # Rollup 配置
├── tsconfig.json          # TypeScript 配置
└── package.json           # 包配置

package.json 配置 #

json
{
  "name": "my-library",
  "version": "1.0.0",
  "description": "A modern JavaScript library",
  "type": "module",
  "main": "dist/my-lib.cjs.js",
  "module": "dist/my-lib.esm.js",
  "browser": "dist/my-lib.umd.js",
  "types": "dist/my-lib.d.ts",
  "exports": {
    ".": {
      "import": "./dist/my-lib.esm.js",
      "require": "./dist/my-lib.cjs.js",
      "browser": "./dist/my-lib.umd.js",
      "types": "./dist/my-lib.d.ts"
    },
    "./style.css": "./dist/style.css"
  },
  "files": [
    "dist",
    "README.md"
  ],
  "sideEffects": [
    "*.css",
    "*.scss"
  ],
  "keywords": ["javascript", "library"],
  "author": "Your Name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/user/my-library"
  },
  "scripts": {
    "dev": "rollup -c -w",
    "build": "rollup -c",
    "test": "vitest",
    "lint": "eslint src",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "devDependencies": {
    "rollup": "^4.0.0",
    "@rollup/plugin-node-resolve": "^15.0.0",
    "@rollup/plugin-commonjs": "^25.0.0",
    "@rollup/plugin-typescript": "^11.0.0",
    "@rollup/plugin-terser": "^0.4.0",
    "typescript": "^5.0.0"
  }
}

完整库配置 #

javascript
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import dts from 'rollup-plugin-dts';

const isProduction = process.env.NODE_ENV === 'production';

const config = [
  {
    input: 'src/index.ts',
    output: [
      {
        file: 'dist/my-lib.cjs.js',
        format: 'cjs',
        sourcemap: true,
        exports: 'named'
      },
      {
        file: 'dist/my-lib.esm.js',
        format: 'es',
        sourcemap: true
      }
    ],
    external: ['react', 'react-dom'],
    plugins: [
      resolve({
        extensions: ['.ts', '.tsx', '.js', '.jsx']
      }),
      commonjs(),
      typescript({
        tsconfig: './tsconfig.json',
        declaration: true,
        declarationDir: 'dist/types'
      }),
      isProduction && terser()
    ].filter(Boolean)
  },
  {
    input: 'dist/types/index.d.ts',
    output: {
      file: 'dist/my-lib.d.ts',
      format: 'es'
    },
    plugins: [dts()]
  }
];

if (isProduction) {
  config.push({
    input: 'src/index.ts',
    output: {
      file: 'dist/my-lib.umd.js',
      format: 'umd',
      name: 'MyLib',
      sourcemap: true,
      globals: {
        react: 'React',
        'react-dom': 'ReactDOM'
      }
    },
    external: ['react', 'react-dom'],
    plugins: [
      resolve({
        extensions: ['.ts', '.tsx', '.js', '.jsx']
      }),
      commonjs(),
      typescript({
        tsconfig: './tsconfig.json'
      }),
      terser()
    ]
  });
}

export default config;

入口文件设计 #

typescript
// src/index.ts
export { Button } from './components/Button';
export { Input } from './components/Input';
export { formatDate, debounce } from './utils/helpers';

export type { ButtonProps } from './components/Button';
export type { InputProps } from './components/Input';

应用开发最佳实践 #

目录结构 #

text
my-app/
├── src/
│   ├── main.js            # 入口文件
│   ├── App.js             # 主应用
│   ├── components/        # 组件
│   ├── pages/             # 页面
│   ├── utils/             # 工具
│   ├── styles/            # 样式
│   └── assets/            # 静态资源
├── public/
│   └── index.html
├── dist/
├── rollup.config.js
└── package.json

开发环境配置 #

javascript
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import postcss from 'rollup-plugin-postcss';
import serve from 'rollup-plugin-serve';
import livereload from 'rollup-plugin-livereload';

export default {
  input: 'src/main.js',
  output: {
    file: 'dist/bundle.js',
    format: 'iife',
    name: 'App',
    sourcemap: true
  },
  plugins: [
    resolve({
      browser: true
    }),
    commonjs(),
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**'
    }),
    postcss({
      extract: true,
      minimize: false,
      sourceMap: true
    }),
    serve({
      contentBase: ['dist', 'public'],
      port: 3000,
      open: true
    }),
    livereload({
      watch: 'dist'
    })
  ],
  watch: {
    clearScreen: false,
    include: 'src/**'
  }
};

生产环境配置 #

javascript
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import postcss from 'rollup-plugin-postcss';
import terser from '@rollup/plugin-terser';
import { visualizer } from 'rollup-plugin-visualizer';
import copy from 'rollup-plugin-copy';
import del from 'rollup-plugin-delete';

export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    entryFileNames: '[name].[hash].js',
    chunkFileNames: 'chunks/[name].[hash].js',
    assetFileNames: 'assets/[name].[hash][extname]',
    sourcemap: true,
    manualChunks: {
      vendor: ['react', 'react-dom', 'react-router-dom']
    }
  },
  plugins: [
    del({ targets: 'dist/*' }),
    copy({
      targets: [
        { src: 'public/index.html', dest: 'dist' }
      ]
    }),
    resolve({
      browser: true
    }),
    commonjs(),
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**'
    }),
    postcss({
      extract: 'style.css',
      minimize: true
    }),
    terser({
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }),
    visualizer({
      filename: 'dist/stats.html',
      gzipSize: true
    })
  ]
};

性能优化最佳实践 #

1. 合理使用 external #

javascript
export default {
  external: (id) => {
    // 排除所有 node_modules
    if (id.includes('node_modules')) {
      return true;
    }
    
    // 排除特定大型库
    const largeLibs = ['lodash', 'moment', 'axios'];
    return largeLibs.some(lib => id.startsWith(lib));
  }
};

2. 优化代码分割 #

javascript
export default {
  output: {
    manualChunks(id) {
      // 第三方库分割
      if (id.includes('node_modules')) {
        // React 生态
        if (id.includes('react') || id.includes('react-dom')) {
          return 'vendor-react';
        }
        
        // 工具库
        if (id.includes('lodash')) {
          return 'vendor-lodash';
        }
        
        // 其他第三方库
        return 'vendor';
      }
      
      // 业务代码分割
      if (id.includes('src/features/')) {
        const match = id.match(/src\/features\/([^/]+)/);
        if (match) {
          return `feature-${match[1]}`;
        }
      }
    }
  }
};

3. Tree-shaking 优化 #

javascript
// 使用 ES 模块导入
import { debounce } from 'lodash-es';  // ✅ 支持 Tree-shaking

// 避免 CommonJS 导入
const _ = require('lodash');  // ❌ 不支持 Tree-shaking

// 使用具名导入
import { Button } from './components';  // ✅

// 避免命名空间导入
import * as Components from './components';  // ❌ 所有都会被打包

4. 压缩优化 #

javascript
import terser from '@rollup/plugin-terser';

export default {
  plugins: [
    terser({
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log', 'console.info'],
        passes: 3  // 多次压缩
      },
      mangle: {
        properties: {
          regex: /^_/,  // 混淆以 _ 开头的属性
          reserved: ['_id']  // 保留特定属性
        }
      },
      format: {
        comments: false  // 移除注释
      }
    })
  ]
};

常见问题解决方案 #

问题一:CommonJS 模块导入问题 #

javascript
// 问题
import lodash from 'lodash';  // 可能无法正确解析

// 解决方案
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  plugins: [
    resolve({
      preferBuiltins: true
    }),
    commonjs({
      transformMixedEsModules: true
    })
  ]
};

问题二:动态导入路径问题 #

javascript
// 问题
const module = await import(`./modules/${name}.js`);  // 可能无法解析

// 解决方案
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';

export default {
  plugins: [
    dynamicImportVars({
      include: ['src/**'],
      exclude: ['node_modules/**']
    })
  ]
};

问题三:CSS 样式问题 #

javascript
// 问题:CSS 未被提取

// 解决方案
import postcss from 'rollup-plugin-postcss';

export default {
  plugins: [
    postcss({
      extract: true,  // 提取到单独文件
      minimize: true,
      sourceMap: true,
      extensions: ['.css', '.scss', '.sass', '.less']
    })
  ]
};

问题四:图片资源问题 #

javascript
import url from '@rollup/plugin-url';
import image from '@rollup/plugin-image';

export default {
  plugins: [
    url({
      include: ['**/*.svg', '**/*.png', '**/*.jpg', '**/*.gif'],
      limit: 8192,  // 小于 8KB 转 Base64
      emitFiles: true,
      fileName: 'assets/[name]-[hash][extname]'
    }),
    image({
      exclude: ['node_modules/**']
    })
  ]
};

问题五:Node.js 内置模块问题 #

javascript
import { builtinModules } from 'module';
import resolve from '@rollup/plugin-node-resolve';

export default {
  external: [
    ...builtinModules,
    ...builtinModules.map(m => `node:${m}`)
  ],
  plugins: [
    resolve({
      preferBuiltins: true
    })
  ]
};

TypeScript 最佳实践 #

tsconfig.json #

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "lib": ["ES2020", "DOM"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationDir": "dist/types",
    "outDir": "dist",
    "rootDir": "src",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

类型声明最佳实践 #

typescript
// src/types/index.ts
export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick?: (event: MouseEvent) => void;
  children: React.ReactNode;
}

export interface InputProps {
  value?: string;
  onChange?: (value: string) => void;
  placeholder?: string;
  disabled?: boolean;
  error?: string;
}

测试最佳实践 #

使用 Vitest #

javascript
// vitest.config.js
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html']
    }
  }
});

测试示例 #

javascript
// tests/utils.test.js
import { describe, it, expect } from 'vitest';
import { formatDate, debounce } from '../src/utils';

describe('formatDate', () => {
  it('should format date correctly', () => {
    const date = new Date('2024-01-01');
    expect(formatDate(date)).toBe('2024-01-01');
  });
});

describe('debounce', () => {
  it('should debounce function calls', async () => {
    let count = 0;
    const fn = debounce(() => count++, 100);
    
    fn();
    fn();
    fn();
    
    expect(count).toBe(0);
    
    await new Promise(resolve => setTimeout(resolve, 150));
    
    expect(count).toBe(1);
  });
});

发布最佳实践 #

版本管理 #

json
// package.json
{
  "scripts": {
    "release": "standard-version",
    "release:minor": "standard-version --release-as minor",
    "release:major": "standard-version --release-as major"
  }
}

发布前检查 #

json
// package.json
{
  "scripts": {
    "prepublishOnly": "npm run lint && npm test && npm run build",
    "prepack": "npm run build"
  }
}

CI/CD 配置 #

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test
      
      - name: Build
        run: npm run build
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

文档最佳实践 #

README 模板 #

markdown
# My Library

A modern JavaScript library for building awesome applications.

## Installation

```bash
npm install my-library

Quick Start #

javascript
import { Button } from 'my-library';

function App() {
  return <Button>Click me</Button>;
}

Documentation #

License #

MIT

text

### API 文档

```typescript
/**
 * Formats a date to a string
 * @param date - The date to format
 * @param format - The format string (default: 'YYYY-MM-DD')
 * @returns The formatted date string
 * @example
 * ```ts
 * formatDate(new Date(), 'YYYY-MM-DD')
 * // => '2024-01-01'
 * ```
 */
export function formatDate(date: Date, format?: string): string;

总结 #

核心原则 #

  1. 简洁优先:保持配置简单,避免过度复杂
  2. 性能至上:合理使用 Tree-shaking 和代码分割
  3. 类型安全:使用 TypeScript 提供类型支持
  4. 测试覆盖:编写充分的单元测试
  5. 文档完善:提供清晰的使用文档

检查清单 #

  • [ ] 配置文件结构清晰
  • [ ] 正确设置 external 依赖
  • [ ] 启用 Tree-shaking
  • [ ] 配置代码分割
  • [ ] 生成 Source Map
  • [ ] 压缩生产代码
  • [ ] 编写单元测试
  • [ ] 完善文档
  • [ ] 配置 CI/CD
  • [ ] 版本管理规范

下一步 #

恭喜你完成了 Rollup 的学习!现在你可以:

  1. 开始构建你的第一个库
  2. 将现有项目迁移到 Rollup
  3. 探索更多 Rollup 插件
  4. 参与社区贡献

祝你使用 Rollup 愉快!

最后更新:2026-03-28