~/ ?.log $
返回文章列表
10 min read
更新于 2026年3月6日

我不知道的打包工具(05)— 构建工具选型:如何根据项目类型选择打包方案

npm init 之后的第一个问题往往不是"写什么业务逻辑",而是"用什么打包工具"。Webpack、Vite、Rollup、esbuild、tsup、Father、unbuild——名单太长了。网上的"工具对比"文章通常罗列一堆功能矩阵,但不回答那个最实际的问题:你的项目到底…

npm init 之后的第一个问题往往不是”写什么业务逻辑”,而是”用什么打包工具”。Webpack、Vite、Rollup、esbuild、tsup、Father、unbuild——名单太长了。网上的”工具对比”文章通常罗列一堆功能矩阵,但不回答那个最实际的问题:你的项目到底该选哪个?

问题的关键在于——选型的第一步不是比较工具,而是识别项目类型。不同类型的项目对构建工具的核心诉求完全不同,搞清楚自己在做什么,答案自然浮现。

一、四种项目类型,四条选型路线

前端项目大致可以分为四类,每类的构建需求差异巨大。

(1)SPA / MPA 应用。 核心需求是代码分割、HMR 热更新、dynamic import 懒加载、HTML 模板注入。这类项目的构建产物只有一个消费者——浏览器。新项目用 Vite,存量项目留在 Webpack,这是目前最务实的选择。

(2)TypeScript 工具库。 比如一个日期处理库、一个状态管理库。核心需求是 ESM + CJS 双格式输出、自动生成 .d.ts 类型声明、零配置上手。tsup 和 unbuild 都能在一个配置文件内搞定这些事。

(3)UI 组件库。 比如一个 React/Vue 组件库。核心需求比工具库多两层:Bundless 逐文件编译(保留目录结构,让消费端按组件粒度 Tree Shaking)、样式隔离(CSS Modules 或 CSS-in-JS)、按需加载。Father 就是为这个场景设计的。

(4)Monorepo 工具包。 一个仓库里有 10 个 package,互相引用。核心需求是 workspace 感知、依赖拓扑排序、增量构建。unbuild 的 stub 模式(开发时直接 jiti 加载源码,不需要构建)配合 turborepo 的任务编排,是目前 monorepo 场景下的高效方案。

说白了,项目类型决定了你需要什么能力,然后才是哪个工具提供了这些能力。 反过来先看工具功能列表再套项目,容易选错。

二、Vite vs Webpack——不是谁更好,是场景不同

很多人以为 Vite 出来之后 Webpack 就该淘汰了,但实际上两者的适用场景有明确分界。

Vite 的优势集中在开发体验:ESM 原生加载让开发服务器启动速度几乎与项目规模无关,esbuild 预构建处理第三方依赖,HMR 更新粒度到模块级别。对于新启动的应用项目,Vite 是默认选择。

Webpack 的优势在存量生态:Module Federation(微前端方案)、成熟的 Loader 链(处理各种非标格式)、以及企业项目中大量已有的构建流水线。把一个跑了三年的 Webpack 项目迁移到 Vite,收益不一定覆盖迁移成本。

下面两段配置展示了同一个 React 应用在两种工具下的典型写法:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: { manualChunks: { vendor: ['react', 'react-dom'] } },
    },
  },
});

Vite 的配置围绕插件和 Rollup 输出选项展开,默认行为覆盖了大部分场景,显式配置的部分很少。

// webpack.config.js
const HtmlPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.tsx',
  module: {
    rules: [
      { test: /\.tsx?$/, use: 'ts-loader' },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ],
  },
  optimization: {
    splitChunks: { chunks: 'all' },
  },
  plugins: [new HtmlPlugin({ template: './index.html' })],
};

Webpack 需要显式声明每种文件类型的处理方式。配置更冗长,但每一步都是可控的——遇到非标需求时,这种显式声明反而更容易调试。

还有一个时效性信息值得关注:Vite 8 Beta(2025 年 12 月发布)已将 Rolldown 设为默认打包引擎,开发与生产共用同一引擎,之前”开发正常、生产报错”的双引擎一致性问题正在被根本性地解决。

三、tsup vs Rollup vs unbuild——库打包三件套

如果项目是一个 npm 包(不是应用),选择范围缩窄到这三个。

tsup 追求极致简单。底层用 esbuild 编译,配合 Rollup 的 dts 插件生成类型声明。一个 tsup.config.ts 就能输出 ESM + CJS 双格式加类型文件:

// tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  dts: true,
  splitting: true,
  clean: true,
});

六行配置,npx tsup 就能跑。对于大多数 TypeScript 工具库,tsup 是最快出活的方案。

Rollup 的 Tree Shaking 精确度是三者中最高的,因为它的静态分析直接操作 ESM 的 AST。对于极度在意产物体积的库(比如给移动端用的 SDK),Rollup 配合手动调优的插件链能压出最小的包。代价是配置复杂度高——TypeScript 支持、CommonJS 桥接、类型声明生成都需要手动接插件。

unbuild 由 unjs 团队维护,最大的特色是 stub 模式:开发阶段不构建,而是通过 jiti(一个 TypeScript 即时加载器)直接从源码引用。改了代码不需要重新 build,monorepo 里多个包互相引用时体验极好。正式构建时底层使用 Rollup。

这里有一个很多人会忽略的细节——tsup 的 Tree Shaking 能力弱于 Rollup。esbuild 的 Tree Shaking 只做基础的导出移除,对副作用标记(sideEffects)的处理不如 Rollup 精细。如果库的体积是核心指标,需要认真评估这个差异。

四、选型总览表

维度ViteWebpacktsupRollupunbuildFather
构建速度慢(大项目)极快中等中等
Tree ShakingRollup 级需配置基础最精确Rollup 级依赖引擎
多格式输出非核心需多配置ESM+CJS灵活ESM+CJSESM+CJS+UMD
类型声明非内置非内置内置需插件内置内置
插件生态Rollup 兼容最丰富有限丰富有限Umi 生态
配置复杂度极低中高极低
适合项目应用存量应用TS 工具库极致体积库Monorepo 包UI 组件库

五、具体建议

把上面的分析压缩成可操作的决策路径:

新建一个 SPA 应用——用 Vite。框架脚手架(create-vitecreate-next-app)已经帮你选好了。

维护一个存量 Webpack 应用——留在 Webpack。升级到 Webpack 5 + SWC Loader 就能获得可观的提速,迁移到 Vite 的 ROI 需要评估。

发布一个 TypeScript npm 包——用 tsup。零配置,esbuild 驱动,ESM + CJS + .d.ts 一步到位。

发布一个 React/Vue 组件库——用 Father。Bundless + Bundle 双模式、样式隔离、按需加载,这些组件库的刚需在 Father 里是开箱即用的。

管理一个 Monorepo 里的多个包——用 unbuild + turborepo。stub 模式省去开发时的重复构建,turborepo 负责任务编排和缓存。

需要极致产物体积——用 Rollup 手动调优。Tree Shaking 精确度最高,但配置成本也最高,适合对体积有硬指标的场景(移动端 SDK、嵌入式脚本)。

如果你只记住一句话:没有”最好”的构建工具,只有”最匹配项目类型”的工具。选型的本质是先搞清楚自己在做什么类型的项目。


本系列其他文章:

相关主题:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;