Rollup 高级特性 #

代码分割 #

什么是代码分割? #

代码分割是将代码拆分成多个文件的技术,可以实现:

  • 按需加载
  • 减小初始加载体积
  • 共享公共依赖
  • 优化缓存策略
text
┌─────────────────────────────────────────────────────────────┐
│                    代码分割效果                               │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  单一 Bundle                    代码分割                     │
│  ┌──────────────┐              ┌──────────────┐             │
│  │              │              │   main.js    │             │
│  │   bundle.js  │              │   (50KB)     │             │
│  │   (500KB)    │              ├──────────────┤             │
│  │              │              │   vendor.js  │             │
│  │              │              │   (200KB)    │             │
│  │              │              ├──────────────┤             │
│  │              │              │   lazy.js    │             │
│  │              │              │   (100KB)    │             │
│  └──────────────┘              └──────────────┘             │
│                                                              │
│  初始加载: 500KB               初始加载: 50KB                │
│                                                              │
└─────────────────────────────────────────────────────────────┘

自动代码分割 #

使用动态导入自动触发代码分割:

javascript
// src/main.js
import { add } from './math.js';

console.log(add(1, 2));

// 动态导入
async function loadHeavyModule() {
  const { heavyFunction } = await import('./heavy.js');
  heavyFunction();
}

document.getElementById('btn').addEventListener('click', loadHeavyModule);
javascript
// rollup.config.js
export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    sourcemap: true
  }
};

手动代码分割 #

多入口分割 #

javascript
export default {
  input: {
    main: 'src/main.js',
    admin: 'src/admin.js',
    user: 'src/user.js'
  },
  output: {
    dir: 'dist',
    format: 'es'
  }
};

manualChunks 配置 #

javascript
export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    manualChunks: {
      vendor: ['lodash', 'axios'],
      utils: ['src/utils/index.js']
    }
  }
};

函数式 manualChunks #

javascript
export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    manualChunks(id) {
      // node_modules 打包到 vendor
      if (id.includes('node_modules')) {
        return 'vendor';
      }
      
      // 共享组件打包到 components
      if (id.includes('src/components')) {
        return 'components';
      }
      
      // 工具函数打包到 utils
      if (id.includes('src/utils')) {
        return 'utils';
      }
    }
  }
};

保留模块结构 #

javascript
export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'es',
    preserveModules: true,
    preserveModulesRoot: 'src'
  }
};

动态导入 #

基本用法 #

javascript
// 静态导入
import { add } from './math.js';

// 动态导入
const math = await import('./math.js');
console.log(math.add(1, 2));

条件加载 #

javascript
async function loadModule() {
  if (condition) {
    const module = await import('./module-a.js');
    module.init();
  } else {
    const module = await import('./module-b.js');
    module.init();
  }
}

按需加载组件 #

javascript
// React 示例
const LazyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </React.Suspense>
  );
}
javascript
// Vue 示例
const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'));

动态导入配置 #

javascript
export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    chunkFileNames: 'chunks/[name]-[hash].js',
    dynamicImportVarsOptions: {
      include: ['src/**'],
      exclude: ['node_modules/**']
    }
  }
};

动态路径导入 #

javascript
// 动态路径
async function loadLocale(locale) {
  const messages = await import(`./locales/${locale}.json`);
  return messages.default;
}

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

Tree-shaking 优化 #

Tree-shaking 原理 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Tree-shaking 过程                         │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  源代码                         打包后                       │
│  ┌─────────────────┐           ┌─────────────────┐         │
│  │ export function │           │ export function │         │
│  │   add() { }     │           │   add() { }     │         │
│  │                 │           │                 │         │
│  │ export function │           └─────────────────┘         │
│  │   subtract() { }│                                       │
│  │                 │           subtract 被移除              │
│  │ export function │           multiply 被移除              │
│  │   multiply() { }│                                       │
│  └─────────────────┘                                       │
│                                                              │
│  import { add } from './math'                               │
│                                                              │
└─────────────────────────────────────────────────────────────┘

配置 Tree-shaking #

javascript
export default {
  input: 'src/main.js',
  treeshake: {
    annotations: true,              // 使用注释标记
    moduleSideEffects: true,        // 模块副作用
    propertyReadSideEffects: true,  // 属性读取副作用
    tryCatchDeoptimization: true,   // try-catch 优化
    unknownGlobalSideEffects: true  // 未知全局副作用
  },
  output: {
    file: 'dist/bundle.js',
    format: 'es'
  }
};

副作用标记 #

json
// package.json
{
  "name": "my-library",
  "sideEffects": false
}
json
// 或指定有副作用的文件
{
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js",
    "./src/styles/**"
  ]
}

纯函数标记 #

javascript
// 使用 /* @__PURE__ */ 标记纯函数
export const config = /* @__PURE__ */ createConfig();

// 这段代码会被移除
export const unused = /* @__PURE__ */ (() => {
  console.log('This will be removed');
  return 'unused';
})();

避免 Tree-shaking #

javascript
// 方式一:保留导出
export function usedFunction() {}
export function unusedFunction() {}  // 即使未使用也会保留

// 方式二:使用 namespace 导入
import * as utils from './utils';
// 所有导出都会被保留

// 方式三:动态属性访问
const name = 'functionName';
import { [name] as func } from './module';

缓存策略 #

启用缓存 #

javascript
import { rollup } from 'rollup';

async function build() {
  const bundle = await rollup({
    input: 'src/main.js',
    cache: true  // 启用缓存
  });
  
  await bundle.write({
    file: 'dist/bundle.js',
    format: 'es'
  });
  
  // 保存缓存供下次使用
  return bundle.cache;
}

增量构建 #

javascript
import { rollup } from 'rollup';

let cache;

async function build() {
  const bundle = await rollup({
    input: 'src/main.js',
    cache: cache  // 使用上次构建的缓存
  });
  
  // 更新缓存
  cache = bundle.cache;
  
  await bundle.write({
    file: 'dist/bundle.js',
    format: 'es'
  });
}

持久化缓存 #

javascript
import { rollup } from 'rollup';
import fs from 'fs';

const cacheFile = '.rollup-cache.json';

async function build() {
  let cache;
  
  // 读取缓存
  if (fs.existsSync(cacheFile)) {
    cache = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
  }
  
  const bundle = await rollup({
    input: 'src/main.js',
    cache: cache
  });
  
  await bundle.write({
    file: 'dist/bundle.js',
    format: 'es'
  });
  
  // 保存缓存
  fs.writeFileSync(cacheFile, JSON.stringify(bundle.cache));
}

性能优化 #

并行处理 #

javascript
export default {
  input: 'src/main.js',
  maxParallelFileOps: 20,  // 最大并行文件操作
  output: {
    file: 'dist/bundle.js',
    format: 'es'
  }
};

减少解析时间 #

javascript
import resolve from '@rollup/plugin-node-resolve';

export default {
  plugins: [
    resolve({
      // 只解析需要的模块
      only: ['lodash', 'axios']
    })
  ]
};

外部化大型依赖 #

javascript
export default {
  external: [
    'react',
    'react-dom',
    'lodash',
    'moment'
  ],
  output: {
    globals: {
      react: 'React',
      'react-dom': 'ReactDOM',
      lodash: '_',
      moment: 'moment'
    }
  }
};

使用 preserveModules #

javascript
export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'es',
    preserveModules: true,  // 保持模块结构
    preserveModulesRoot: 'src'
  }
};

压缩优化 #

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

export default {
  plugins: [
    terser({
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log']
      },
      mangle: {
        properties: {
          regex: /^_/
        }
      },
      format: {
        comments: false
      }
    })
  ]
};

高级输出配置 #

条目文件命名 #

javascript
export default {
  input: {
    main: 'src/main.js',
    admin: 'src/admin.js'
  },
  output: {
    dir: 'dist',
    format: 'es',
    entryFileNames: (chunkInfo) => {
      // 根据入口名称命名
      if (chunkInfo.name === 'main') {
        return '[name].js';
      }
      return 'pages/[name].js';
    }
  }
};

代码块命名 #

javascript
export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    chunkFileNames: (chunkInfo) => {
      // 根据模块内容命名
      if (chunkInfo.modules.some(m => m.includes('vendor'))) {
        return 'vendor/[name]-[hash].js';
      }
      return 'chunks/[name]-[hash].js';
    }
  }
};

资源文件命名 #

javascript
export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    assetFileNames: (assetInfo) => {
      // 根据文件类型命名
      if (assetInfo.name.endsWith('.css')) {
        return 'css/[name]-[hash][extname]';
      }
      if (/\.(png|jpe?g|gif|svg)$/.test(assetInfo.name)) {
        return 'images/[name]-[hash][extname]';
      }
      return 'assets/[name]-[hash][extname]';
    }
  }
};

高级插件技巧 #

插件钩子顺序 #

javascript
export default {
  plugins: [
    {
      name: 'plugin-1',
      buildStart: {
        order: 'pre',  // 最先执行
        handler() {
          console.log('Plugin 1 buildStart');
        }
      }
    },
    {
      name: 'plugin-2',
      buildStart: {
        order: 'post',  // 最后执行
        handler() {
          console.log('Plugin 2 buildStart');
        }
      }
    }
  ]
};

插件通信 #

javascript
const sharedState = {};

export default {
  plugins: [
    {
      name: 'plugin-1',
      buildStart() {
        sharedState.data = 'from plugin 1';
      }
    },
    {
      name: 'plugin-2',
      buildEnd() {
        console.log(sharedState.data);  // 'from plugin 1'
      }
    }
  ]
};

使用 this.emitFile #

javascript
function myPlugin() {
  return {
    name: 'emit-file',
    generateBundle(options, bundle) {
      // 发射新文件
      this.emitFile({
        type: 'asset',
        fileName: 'manifest.json',
        source: JSON.stringify({
          files: Object.keys(bundle)
        })
      });
    }
  };
}

模块联邦 #

基本概念 #

模块联邦允许多个独立构建的应用共享代码:

text
┌─────────────────────────────────────────────────────────────┐
│                    模块联邦架构                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐     ┌──────────────┐                     │
│  │   App A      │     │   App B      │                     │
│  │              │     │              │                     │
│  │  ┌────────┐  │     │  ┌────────┐  │                     │
│  │  │Component│◄─┼─────┼──│  Uses  │  │                     │
│  │  │  (exposed)│ │     │  │        │  │                     │
│  │  └────────┘  │     │  └────────┘  │                     │
│  └──────────────┘     └──────────────┘                     │
│                                                              │
│  共享组件在运行时加载,无需重复打包                          │
│                                                              │
└─────────────────────────────────────────────────────────────┘

使用 @module-federation #

bash
npm install @module-federation/rollup --save-dev
javascript
import moduleFederation from '@module-federation/rollup';

export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es'
  },
  plugins: [
    moduleFederation({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './utils': './src/utils'
      },
      remotes: {
        app2: 'app2@http://localhost:3001/remoteEntry.js'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ]
};

高级 Source Map #

内联 Source Map #

javascript
export default {
  output: {
    file: 'dist/bundle.js',
    format: 'es',
    sourcemap: 'inline'
  }
};

隐藏 Source Map #

javascript
export default {
  output: {
    file: 'dist/bundle.js',
    format: 'es',
    sourcemap: 'hidden'  // 生成但不链接
  }
};

Source Map 路径转换 #

javascript
export default {
  output: {
    file: 'dist/bundle.js',
    format: 'es',
    sourcemap: true,
    sourcemapPathTransform: (relativePath) => {
      // 转换路径
      return relativePath.replace('../src/', '');
    },
    sourcemapBaseUrl: 'https://example.com/maps/'
  }
};

实战案例 #

案例一:按路由分割 #

javascript
// router.js
const routes = {
  home: () => import('./pages/Home.js'),
  about: () => import('./pages/About.js'),
  contact: () => import('./pages/Contact.js')
};

async function navigate(page) {
  const module = await routes[page]();
  module.render();
}

案例二:按功能分割 #

javascript
export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    manualChunks(id) {
      if (id.includes('node_modules')) {
        // 第三方库单独打包
        if (id.includes('lodash')) return 'vendor-lodash';
        if (id.includes('axios')) return 'vendor-axios';
        return 'vendor';
      }
      
      // 按功能分割
      if (id.includes('src/features/auth')) return 'feature-auth';
      if (id.includes('src/features/dashboard')) return 'feature-dashboard';
      if (id.includes('src/features/settings')) return 'feature-settings';
    }
  }
};

案例三:SSR 优化 #

javascript
// 服务端配置
export default {
  input: 'src/server.js',
  output: {
    file: 'dist/server.js',
    format: 'cjs'
  },
  external: ['express', 'fs', 'path'],
  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

// 客户端配置
export default {
  input: 'src/client.js',
  output: {
    dir: 'dist/client',
    format: 'es',
    manualChunks: {
      vendor: ['react', 'react-dom']
    }
  }
};

下一步 #

现在你已经掌握了 Rollup 的高级特性,接下来学习 最佳实践 了解实际项目中的应用技巧!

最后更新:2026-03-28