Webpack & Vite 深度解析

学习目标: 彻底掌握现代前端打包工具的核心原理,从 Webpack 的底层机制到 Vite 的革命性设计,再到 Rollup/esbuild 的各自定位,建立完整的工程化认知体系。


一、Webpack 核心原理

1.1 整体工作流程(5个阶段详解)

Webpack 的构建过程可以分为 5 个主要阶段,每个阶段都有对应的钩子(Hook)供插件介入。

阶段一:初始化(Initialization)

arduino 复制代码
webpack.config.js
      ↓
  读取配置(merge 默认配置 + 用户配置 + CLI 参数)
      ↓
  创建 Compiler 对象(核心编译器实例,全局唯一)
      ↓
  注册所有内置 Plugin(如 HtmlWebpackPlugin、DefinePlugin)
      ↓
  调用 compiler.hooks.initialize

核心操作:

arduino 复制代码
// Webpack 内部伪代码
function webpack(config) {
  // 1. 合并配置(Shell 参数 > 用户配置 > 默认配置)
  const mergedConfig = mergeConfig(defaultConfig, config, shellArgs);

  // 2. 创建 Compiler 对象(继承自 Tapable,拥有完整的 hooks 系统)
  const compiler = new Compiler(mergedConfig.context, mergedConfig);

  // 3. 注册所有插件(调用每个 plugin 的 apply 方法)
  mergedConfig.plugins.forEach(plugin => plugin.apply(compiler));

  // 4. 初始化完毕,返回 compiler
  return compiler;
}

Compiler 对象职责:

  • 保存完整的 webpack 配置
  • 管理文件系统(inputFileSystem / outputFileSystem)
  • 触发各阶段 hooks(beforeRun → run → beforeCompile → compile → make → finishMake → afterCompile → emit → done)
  • 负责文件监听(watch 模式下的 watchRun)

阶段二:编译(Compilation)

这是 Webpack 最核心的阶段,从入口文件出发,递归构建完整的依赖图。

bash 复制代码
compiler.hooks.make.callAsync()
      ↓
  创建 Compilation 对象(当次编译的快照,包含 modules/chunks/assets)
      ↓
  从 entry 配置中确定入口模块
      ↓
  递归构建依赖图(Dependency Graph)
      ↓
  对每个模块:解析 → 加载(Loader)→ 构建 → 分析依赖
javascript 复制代码
// 从 entry 开始递归构建的伪代码
class Compilation {
  buildModule(module, callback) {
    // 1. 读取文件内容
    const source = this.readFile(module.resource);

    // 2. 依次执行 Loader 链,转换源码
    const transformedSource = runLoaders(this.loaders, source);

    // 3. 用 acorn 解析 AST,找出所有 import/require
    const ast = acorn.parse(transformedSource);
    const dependencies = extractDependencies(ast);

    // 4. 递归处理每个依赖
    dependencies.forEach(dep => {
      this.buildModule(dep);
    });
  }
}

阶段三:模块解析(Module Resolution + Loader 链)

每个模块的构建过程:

javascript 复制代码
import './foo.css'
      ↓
  Resolver(解析模块路径:相对/绝对/node_modules 三种策略)
      ↓
  匹配 module.rules(确定使用哪些 Loader)
      ↓
  Loader 链(从右到左执行 pitch,从左到右执行 normal)
      ↓
  返回 JavaScript 字符串(Webpack 只认识 JS/JSON)
      ↓
  Parser(acorn 解析 AST,找依赖)

阶段四:生成(Seal / Emit)

arduino 复制代码
所有模块构建完毕
      ↓
  seal:冻结 Compilation,不再接受新模块
      ↓
  分组:按 entry + dynamic import 分割成 Chunk
      ↓
  优化:Tree Shaking / splitChunks / minification
      ↓
  template:将 Chunk 渲染成最终 Bundle 字符串
      ↓
  生成 assets(key = 文件名, value = 文件内容)

阶段五:写入磁盘(Emit)

markdown 复制代码
compiler.hooks.emit.callAsync(compilation)
      ↓
  遍历 compilation.assets,通过 outputFileSystem 写文件
      ↓
  compiler.hooks.afterEmit
      ↓
  compiler.hooks.done(构建完成)

完整 hooks 时序图:

erlang 复制代码
beforeRun → run → normalModuleFactory(工厂创建)
  → beforeCompile → compile → make(递归构建)
    → finishMake → afterCompile → shouldEmit
      → emit → afterEmit → done

1.2 核心概念深度

Module / Chunk / Bundle 三者关系

这是 Webpack 最容易混淆的三个概念:

概念 含义 对应文件 生成时机
Module 每一个被解析的文件 任意格式(JS/CSS/图片...) 编译阶段(make)
Chunk 一组 Module 的集合 逻辑上的代码块 Seal 阶段分组
Bundle 最终输出的文件 dist/*.js Emit 阶段写磁盘

关系示意:

css 复制代码
源码文件(Module)
  app.js (Module)
  ├── utils.js (Module)
  ├── lodash (Module × N 个子模块)
  └── route-a.js (dynamic import → 独立 Chunk)

       ↓ Seal 阶段

Chunk 分组:
  main-chunk = [app.js + utils.js + lodash]
  route-a-chunk = [route-a.js]

       ↓ Emit 阶段

Bundle 输出:
  main.bundle.js
  route-a.bundle.js

关键规则:

  • 一个 Entry 至少产生一个 initial Chunk
  • dynamic import() 产生 async Chunk
  • 一个 Chunk 可以包含多个 Module
  • 一个 Module 可以属于多个 Chunk(splitChunks 提取公共模块时)

Dependency Graph(依赖图)构建过程

Webpack 通过 AST 静态分析构建有向无环图(DAG):

markdown 复制代码
1. 入口文件 → 添加到待处理队列
2. 取出队首模块 → 读取文件 → Loader 转换
3. 用 acorn 解析 AST → 遍历所有 ImportDeclaration / require()
4. 对每个依赖:
   a. 解析路径(Resolver)
   b. 如果未处理过 → 加入队列
   c. 如果已处理 → 直接引用(避免循环依赖死循环)
5. 为当前模块记录依赖关系(parentModule → childModule)
6. 重复 2-5 直到队列为空

循环依赖处理:

javascript 复制代码
// a.js
import { foo } from './b.js';
export const bar = 'bar';

// b.js
import { bar } from './a.js'; // 循环!
export const foo = 'foo';

Webpack 会正常构建,但运行时 barb.js 首次执行时为 undefined(因为 a.js 还没执行完)。这是 ES Module 的"活绑定"(live binding)特性决定的。

Tree Shaking 原理(为何 CJS 不支持)

ESM 静态分析 vs CJS 动态特性:

javascript 复制代码
// ✅ ESM - 静态结构,可静态分析
import { add } from './math';  // 编译时确定,import 的是哪个具体绑定

// ❌ CJS - 动态结构,无法静态分析
const { add } = require('./math');  // 运行时才知道取哪个属性
const method = 'add';
require('./math')[method]();  // 完全动态,无法预知

Tree Shaking 实现原理(三步走):

yaml 复制代码
Step 1: 标记(Mark)
  Webpack 从 entry 出发,遍历所有 ESM 模块
  对每个 export,标记它是否被实际使用(used / unused)

Step 2: 分析副作用(Side Effects)
  package.json sideEffects: false → 告知所有模块无副作用,可安全删除
  sideEffects: ["*.css"] → CSS 文件有副作用(改全局样式),保留

Step 3: 清除(Shake)
  production 模式下,Terser/esbuild 进行 DCE
  删除所有 unused export 的代码

为什么 CJS 不支持:

  1. require() 是函数调用,可以动态传参
  2. module.exports 可以在任何地方被赋值
  3. 无法在编译时确定哪些导出会被使用
  4. 必须在运行时才能确定完整的导出对象

ESM 为什么支持:

  1. import 是语法关键字,不是函数调用
  2. 导入绑定是静态的(编译时确定)
  3. export 声明必须在模块顶层
  4. 工具链可以在不运行代码的情况下分析出哪些导出被使用

Code Splitting(splitChunks 配置详解 + dynamic import)

两种分割方式:

方式1:Dynamic Import(动态导入)

dart 复制代码
// 点击按钮时才加载路由组件
button.addEventListener('click', async () => {
  const { default: RouteA } = await import('./route-a');
  // import() 返回 Promise<Module>
  // Webpack 会自动将 route-a 拆分成独立 Chunk
});

// React 中的应用
const LazyComp = React.lazy(() => import('./HeavyComponent'));

Webpack 处理 dynamic import 的原理:

javascript 复制代码
// 原始代码
const mod = await import('./foo');

// Webpack 编译后(简化版)
// 1. 将 foo.js 编译成独立 chunk(foo.bundle.js)
// 2. 运行时通过 JSONP/import() 加载
const mod = await __webpack_require__.e(/* chunkId */ "foo")
  .then(__webpack_require__.bind(__webpack_require__, "./foo.js"));

方式2:SplitChunksPlugin(自动分割)

javascript 复制代码
// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      // all:对所有 chunks 生效(推荐)
      // initial:只对同步 chunks
      // async:只对异步 chunks(默认)
      chunks: 'all',

      // 最小文件大小(字节),小于此值不分割
      minSize: 20000,

      // 最大文件大小,超过会继续分割
      maxSize: 0,

      // 最少被几个 chunk 引用才分割(默认1)
      minChunks: 1,

      // 最大并发请求数(HTTP/1.1 时代有意义)
      maxAsyncRequests: 30,
      maxInitialRequests: 30,

      // 分割出来的 chunk 名称分隔符
      automaticNameDelimiter: '~',

      // 缓存组(精细控制)
      cacheGroups: {
        // 将 node_modules 的代码单独打包
        vendors: {
          test: /[\/]node_modules[\/]/,
          priority: -10,  // 优先级(数字越大越优先)
          reuseExistingChunk: true,  // 如果已有相同内容的 chunk,复用
          name: 'vendors',
          filename: 'js/[name].[contenthash:8].js',
        },
        // 公共模块(被多处引用的)
        common: {
          minChunks: 2,   // 至少被 2 个 chunk 引用
          priority: -20,
          reuseExistingChunk: true,
          name: 'common',
        },
        // React 单独打包(长期缓存)
        react: {
          test: /[\/]node_modules[\/](react|react-dom|scheduler)[\/]/,
          name: 'react-vendor',
          priority: 10,
          chunks: 'initial',
        },
      },
    },
  },
};

HMR 热更新原理(WebSocket + module.hot.accept 全流程)

HMR 是 Webpack 开发体验的核心,整个流程如下:

css 复制代码
文件修改(保存)
      ↓
  1. Webpack 监听到文件变化(watch 模式)
      ↓
  2. 重新编译变化的模块(增量编译,非全量)
      ↓
  3. 生成两个文件:
     - [hash].hot-update.json(描述哪些模块更新了)
     - [chunkId].[hash].hot-update.js(更新的模块代码)
      ↓
  4. webpack-dev-server 通过 WebSocket 通知浏览器
     消息格式:{ type: 'hash', data: 'abc123' }
              { type: 'ok' }
      ↓
  5. 浏览器端 HMR Runtime 收到通知
      ↓
  6. 通过 JSONP 请求拉取 hot-update.js
      ↓
  7. 执行 module.hot.accept 回调(模块自我更新)
      ↓
  8. 如果模块没有注册 accept → 向上冒泡
     如果冒泡到顶层还没有 accept → 全页面刷新(fallback)

module.hot.accept 使用:

javascript 复制代码
// React Fast Refresh 的简化原理
if (module.hot) {
  // 接受自身更新
  module.hot.accept();

  // 接受某个依赖更新,并提供回调
  module.hot.accept('./store', () => {
    // 当 store 模块更新时,重新渲染根组件
    const newStore = require('./store').default;
    ReactDOM.render(<App store={newStore} />, document.getElementById('root'));
  });

  // 模块销毁时清理(避免内存泄漏)
  module.hot.dispose((data) => {
    clearInterval(timer);
    data.lastValue = someState; // 传递给下一个版本的模块
  });
}

WebSocket 通信协议:

go 复制代码
// webpack-dev-server 发送的消息类型
{ type: 'hash', data: 'newHash' }    // 新的编译 hash
{ type: 'ok' }                        // 编译成功
{ type: 'errors', data: [...] }       // 编译错误
{ type: 'warnings', data: [...] }     // 编译警告
{ type: 'close' }                     // 服务器关闭

Source Map 生成原理(VLQ 编码 + 7种 devtool 选项对比)

Source Map 本质: 一个 JSON 文件,记录了"编译后代码的某个位置"→"源码的某个位置"的映射关系。

VLQ 编码(Variable-Length Quantity):

ini 复制代码
原理:用 Base64 字符表示可变长度整数,压缩映射数据
每个段由 4-5 个 VLQ 数字组成:
  [生成文件列偏移, 源文件索引, 源文件行偏移, 源文件列偏移, 名称索引]

示例:mappings 字段 "AAAA;AACA"
  AAAA → 第一行第一个映射(0,0,0,0 四个都是0)
  ; → 换行
  AACA → 第二行第一个映射

7种 devtool 选项对比:

devtool 值 构建速度 重构建速度 质量 适用场景
false / 无 最快 ⚡⚡⚡ 最快 ⚡⚡⚡ 无映射 生产环境(配合独立 map 文件上传监控)
eval 快 ⚡⚡ 最快 ⚡⚡⚡ 低(转换后代码) 开发初期,追求速度
eval-source-map 快 ⚡⚡ 高(原始代码) 开发推荐 ✅
eval-cheap-source-map 较快 ⚡⚡ 快 ⚡⚡ 中(无列信息) 开发,稍差质量换速度
eval-cheap-module-source-map 中 ⚡ 高(Loader 转换前) 开发推荐(含 Babel 前源码)✅
source-map 最慢 最高 生产环境(需要调试)
hidden-source-map 最慢 最高(不暴露) 生产环境(Sentry 错误监控)✅
nosources-source-map 最慢 只有位置 生产环境(保护源码安全)

最佳实践:

arduino 复制代码
// 开发环境
devtool: 'eval-cheap-module-source-map',

// 生产环境(上传到 Sentry,不暴露给用户)
devtool: 'hidden-source-map',
// 同时配置 webpack.SourceMapDevToolPlugin 上传到错误监控平台

1.3 Loader 机制

Loader 本质

Loader 是一个纯函数:

javascript 复制代码
// 最简单的 Loader
module.exports = function(source, sourceMap, meta) {
  // source: 前一个 Loader 传来的字符串或 Buffer
  // sourceMap: 上一个 Loader 传来的 source map
  // meta: 元数据

  // 处理 source...
  const result = transform(source);

  // 同步返回
  return result;

  // 或者异步返回
  // const callback = this.async();
  // callback(null, result, sourceMap, meta);
};

// Loader 上下文(this)提供了大量工具方法
// this.getOptions()        获取 Loader 配置
// this.async()             异步模式
// this.emitFile()          输出文件
// this.addDependency()     添加文件依赖(watch 时监听)
// this.cacheable(false)    关闭缓存
// this.resourcePath        当前处理文件的绝对路径
// this.rootContext          项目根目录

pitch 阶段 vs normal 阶段

假设配置了 use: ['a-loader', 'b-loader', 'c-loader']

css 复制代码
执行顺序:

pitch 阶段(从左到右):
  a-loader.pitch → b-loader.pitch → c-loader.pitch

normal 阶段(从右到左):
  c-loader → b-loader → a-loader

完整流程:
  a.pitch → b.pitch → c.pitch → 读文件 → c → b → a

pitch 中断机制:

java 复制代码
// b-loader.js(pitch 阶段)
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  // 如果 pitch 返回了值,就中断后续 loader 的 pitch 和 normal 阶段
  // 直接将返回值交给前一个 loader 的 normal 阶段处理
  if (someCondition) {
    return `module.exports = 'cached result'`;
    // 返回后:只有 a.normal 还会执行,b/c 的 normal 和 c 的 pitch 都跳过
  }
};

pitch 的实际用途(style-loader 经典案例):

javascript 复制代码
// style-loader 通过 pitch 中断,将 css-loader 的结果注入 <style>
module.exports.pitch = function(remainingRequest) {
  // 返回一段 JS 代码,运行时动态加载 CSS
  return `
    var content = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});
    require(${loaderUtils.stringifyRequest(this, require.resolve('./addStyles'))});
    // ...
  `;
  // pitch 返回后,css-loader 的 normal 不再执行
  // 而是在运行时通过 require 动态调用
};

常用 Loader 原理解析

babel-loader:

kotlin 复制代码
// 本质:调用 @babel/core.transform()
module.exports = function(source) {
  const options = this.getOptions(); // 读取 .babelrc 或 babel.config.js
  const { code, map } = babel.transformSync(source, {
    ...options,
    filename: this.resourcePath,
    inputSourceMap: this.sourceMap,
  });
  this.callback(null, code, map);
};

css-loader:

  • 解析 CSS 中的 @importurl() 依赖
  • 将 CSS 转换为 JS 模块(导出 CSS 字符串 + 依赖列表)
  • 支持 CSS Modules(将类名替换为哈希值)
java 复制代码
// css-loader 处理后的输出(简化)
// 原始:.foo { color: red; }
// 输出 JS 模块:
module.exports = [
  [module.id, '.foo { color: red; }', '']
];
module.exports.locals = {}; // CSS Modules 类名映射

style-loader:

  • 将 css-loader 的输出注入到 <style> 标签
  • 支持 HMR(通过 module.hot.accept 动态更新样式)
  • 只适合开发环境,生产环境用 MiniCssExtractPlugin
javascript 复制代码
// style-loader 运行时注入
function insertStyleElement(options) {
  const style = document.createElement('style');
  const target = document.querySelector(options.target) || document.head;
  target.appendChild(style);
  return style;
}

file-loader vs url-loader:

ini 复制代码
// file-loader:将文件输出到 output 目录,返回文件路径
module.exports = function(source) {
  const url = loaderUtils.interpolateName(this, '[contenthash].[ext]', { content: source });
  this.emitFile(url, source);
  return `module.exports = ${JSON.stringify(url)}`;
};

// url-loader:小于 limit 时转成 base64 Data URL,大于 limit 时降级到 file-loader
module.exports = function(source) {
  const limit = this.getOptions().limit || 8192; // 默认 8KB
  if (source.length < limit) {
    const base64 = source.toString('base64');
    const mimeType = mime.getType(this.resourcePath);
    return `module.exports = "data:${mimeType};base64,${base64}"`;
  }
  // fallback 到 file-loader
  return fileLoader.call(this, source);
};

手写一个注释剥离 Loader

ini 复制代码
// strip-comment-loader.js
/**
 * 功能:移除 JS 文件中的所有注释(单行注释、多行注释)
 * 注意:正则方案简单但不完美(字符串中的 // 也会被误删)
 *       生产级别应该用 AST(如 babel 的 removeComments 选项)
 */
const { validate } = require('schema-utils');

// 定义选项 schema
const schema = {
  type: 'object',
  properties: {
    preserveLicense: {
      type: 'boolean',
      description: '是否保留 License 注释(/*!...*/)',
    },
    stripStrings: {
      type: 'boolean',
      description: '是否连字符串中的注释也一起删除(默认 false)',
    },
  },
  additionalProperties: false,
};

module.exports = function stripCommentLoader(source) {
  // 获取并校验配置
  const options = this.getOptions() || {};
  validate(schema, options, { name: 'strip-comment-loader' });

  const { preserveLicense = true, stripStrings = false } = options;

  let result = source;

  if (stripStrings) {
    // 简单正则方案(会误删字符串中的注释,谨慎使用)
    result = result
      .replace(//*[\s\S]*?*//g, (match) => {
        // 如果保留 License 注释(以 /*! 开头)
        if (preserveLicense && match.startsWith('/*!')) return match;
        return '';
      })
      .replace(///.*/g, '');
  } else {
    // 更安全的方案:用状态机跳过字符串和模板字符串
    result = stripCommentsSafe(source, { preserveLicense });
  }

  // 清理多余空白行
  result = result.replace(/\n{3,}/g, '\n\n');

  return result;
};

/**
 * 状态机版本:安全地移除注释(不影响字符串内容)
 */
function stripCommentsSafe(source, { preserveLicense }) {
  let output = '';
  let i = 0;
  const len = source.length;

  while (i < len) {
    const ch = source[i];
    const next = source[i + 1];

    // 跳过单引号字符串
    if (ch === "'" || ch === '"') {
      const quote = ch;
      output += ch;
      i++;
      while (i < len && source[i] !== quote) {
        if (source[i] === '\') { output += source[i++]; } // 转义
        output += source[i++];
      }
      output += source[i++] || '';
      continue;
    }

    // 跳过模板字符串
    if (ch === '`') {
      output += ch;
      i++;
      while (i < len && source[i] !== '`') {
        if (source[i] === '\') { output += source[i++]; }
        output += source[i++];
      }
      output += source[i++] || '';
      continue;
    }

    // 单行注释 //
    if (ch === '/' && next === '/') {
      while (i < len && source[i] !== '\n') i++;
      continue; // 吃掉整行注释
    }

    // 多行注释 /* */
    if (ch === '/' && next === '*') {
      const commentStart = i;
      i += 2;
      while (i < len - 1 && !(source[i] === '*' && source[i + 1] ==='/' )) { i++; }
      i += 2; // 跳过 */
      const comment = source.slice(commentStart, i);
      // License 注释(/*!)保留
      if (preserveLicense && comment.startsWith('/*!')) {
        output += comment;
      }
      continue;
    }

    output += ch;
    i++;
  }

  return output;
}

// 使用示例(webpack.config.js)
// {
//   test: /.js$/,
//   use: [
//     'babel-loader',
//     {
//       loader: path.resolve('./loaders/strip-comment-loader'),
//       options: { preserveLicense: true }
//     }
//   ]
// }

1.4 Plugin 机制

Tapable 事件系统

Webpack 的整个插件系统建立在 tapable 库之上,本质是一个发布-订阅模式。

核心 Hook 类型:

javascript 复制代码
const {
  SyncHook,           // 同步,按注册顺序执行
  SyncBailHook,       // 同步,返回非 undefined 则停止
  SyncWaterfallHook,  // 同步,上一个返回值传给下一个
  SyncLoopHook,       // 同步,返回非 undefined 则重新执行

  AsyncSeriesHook,      // 异步串行,依次执行
  AsyncSeriesBailHook,  // 异步串行,某个返回值则停止
  AsyncSeriesWaterfallHook, // 异步串行瀑布
  AsyncParallelHook,    // 异步并行,同时执行所有
  AsyncParallelBailHook, // 异步并行,某个有值则停止
} = require('tapable');

// 使用示例
const hook = new AsyncSeriesHook(['compiler', 'options']);

// 注册(订阅)
hook.tapAsync('MyPlugin', (compiler, options, callback) => {
  doSomethingAsync(() => callback()); // 完成后调用 callback
});

hook.tapPromise('AnotherPlugin', async (compiler, options) => {
  await doAsync();
  // 返回 Promise 即可
});

// 触发(发布)
hook.callAsync(compiler, options, () => {
  console.log('所有监听者执行完毕');
});

tap / tapAsync / tapPromise 区别:

javascript 复制代码
// 同步注册(只能用于 SyncHook)
hook.tap('Plugin', (arg1, arg2) => { /* 同步 */ });

// 异步注册(回调方式)
hook.tapAsync('Plugin', (arg1, arg2, callback) => {
  setTimeout(() => callback(), 100);
});

// 异步注册(Promise 方式)
hook.tapPromise('Plugin', (arg1, arg2) => {
  return new Promise(resolve => setTimeout(resolve, 100));
});

Compiler vs Compilation 对象

维度 Compiler Compilation
生命周期 整个 webpack 进程 每次编译(watch 模式每次文件变更)
实例数量 唯一(单例) 每次构建创建一个新的
职责 全局配置、文件系统、Plugin 注册 模块构建、依赖图、Chunk 分割、资源生成
访问方式 plugin.apply(compiler) compiler.hooks.make → compilation

关键 hooks 详解

javascript 复制代码
class MyPlugin {
  apply(compiler) {
    // beforeRun: webpack 首次启动前(watch 模式不触发)
    compiler.hooks.beforeRun.tapAsync('MyPlugin', (compiler, callback) => {
      console.log('即将开始构建');
      callback();
    });

    // run: 开始读取 records(序列化构建状态)
    compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
      callback();
    });

    // emit: 生成文件到 output 目录前(可以在这里修改 assets)
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // 修改输出文件
      Object.keys(compilation.assets).forEach(filename => {
        if (filename.endsWith('.js')) {
          const content = compilation.assets[filename].source();
          // 可以修改 content...
          compilation.assets[filename] = {
            source: () => content,
            size: () => content.length,
          };
        }
      });
      callback();
    });

    // done: 构建完成(包含成功和失败)
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      if (stats.hasErrors()) {
        console.error('构建失败');
      } else {
        console.log('构建成功!耗时:', stats.endTime - stats.startTime, 'ms');
      }
    });

    // watchRun: watch 模式,每次文件变更触发
    compiler.hooks.watchRun.tapAsync('MyPlugin', (compiler, callback) => {
      const changedFiles = compiler.modifiedFiles; // Set<string>
      console.log('变化的文件:', [...changedFiles]);
      callback();
    });
  }
}

手写一个生成 filelist.md 文件的 Plugin

javascript 复制代码
// FileListPlugin.js
class FileListPlugin {
  constructor(options = {}) {
    this.options = {
      filename: 'filelist.md',  // 默认输出文件名
      ...options,
    };
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
      const assets = compilation.assets;

      // 生成文件列表 Markdown
      let content = '# 构建产物清单\n\n';
      content += `> 构建时间:${new Date().toLocaleString()}\n\n`;
      content += '| 文件名 | 大小 |\n';
      content += '|--------|------|\n';

      // 按文件大小排序
      const sortedAssets = Object.entries(assets)
        .sort(([, a], [, b]) => b.size() - a.size());

      let totalSize = 0;
      sortedAssets.forEach(([filename, asset]) => {
        const size = asset.size();
        totalSize += size;
        content += `| ${filename} | ${formatSize(size)} |\n`;
      });

      content += `\n**总计:${sortedAssets.length} 个文件,${formatSize(totalSize)}**\n`;

      // 将文件添加到 assets(会被写入 output 目录)
      compilation.assets[this.options.filename] = {
        source: () => content,
        size: () => Buffer.byteLength(content),
      };

      callback();
    });
  }
}

function formatSize(bytes) {
  if (bytes < 1024) return bytes + ' B';
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}

module.exports = FileListPlugin;

// 使用
// new FileListPlugin({ filename: 'assets-report.md' })

常用插件原理解析

HtmlWebpackPlugin:

  • compiler.hooks.emit 阶段,读取 HTML 模板
  • 分析 compilation.assets 中的 JS/CSS 文件
  • 自动注入 <script><link> 标签(带 contenthash)
  • 支持 EJS 模板语法,可传入自定义变量

MiniCssExtractPlugin:

  • 在 Loader 阶段:将 CSS 内容从 JS 模块中"抽离",记录到 compilation 的 CSS 模块图
  • 在 Plugin 阶段(compiler.hooks.emit):将收集的 CSS 合并,生成独立 .css 文件
  • 与 style-loader 互斥(一个运行时注入,一个编译时提取)

DefinePlugin:

javascript 复制代码
// 原理:在编译时做字符串替换(不是真正的全局变量注入)
new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify('production'),
  '__DEV__': JSON.stringify(false),
});

// 编译后:if (false) { /* dead code,会被 Tree Shaking 删除 */ }
// if (__DEV__) { ... }  →  if (false) { ... }

BannerPlugin:

php 复制代码
// 在每个 Chunk 文件头部添加注释
new webpack.BannerPlugin({
  banner: '/*! My App v1.0.0 | MIT License */\n',
  raw: true,    // true: 直接插入(不包裹 /* */)
  entryOnly: false, // false: 所有 chunk 都添加
});

1.5 代码注释/无用代码去除(重点!)

Dead Code Elimination(DCE)原理

DCE 是编译器优化技术,分为两类:

1. 语义级 DCE(Tree Shaking):

  • 依赖 ESM 静态结构
  • 找出"永远不会被调用的 export"
  • 由打包工具(Webpack/Rollup)在 bundle 阶段完成

2. 代码级 DCE(Minifier DCE):

  • 处理 if(false)/三元运算/永远为真的条件
  • 删除不可达代码(unreachable code)
  • 由压缩工具(Terser/UglifyJS/esbuild)完成
arduino 复制代码
// 典型 DCE 场景
if (process.env.NODE_ENV === 'production') {
  console.log('生产环境代码');
} else {
  console.log('开发环境代码');  // 生产构建中会被删除
}

// DefinePlugin 替换后:
if ('production' === 'production') {  // 常量折叠
  console.log('生产环境代码');
} else {
  console.log('开发环境代码');  // 不可达代码,DCE 删除
}

// Terser 最终输出:
console.log('生产环境代码');

Terser 压缩:如何识别并删除注释

Terser 是 Webpack 5 内置的 JS 压缩工具(替代 UglifyJS)。

注释处理相关配置:

java 复制代码
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        // 是否将 License 注释提取到独立文件
        extractComments: false, // true: 提取到 xxx.LICENSE.txt
        // extractComments: /^**!|@preserve|@license|@cc_on/i,

        terserOptions: {
          format: {
            // 注释处理策略(核心配置)
            comments: false,
            // comments: 'all'              // 保留所有注释
            // comments: 'some'             // 保留特殊注释(默认)
            // comments: false              // 删除所有注释 ✅ 推荐生产
            // comments: /特定正则/         // 匹配正则的注释保留
            // comments: (node, comment) => {
            //   return comment.value.includes('@preserve');
            // }
          },
          compress: {
            // 删除不可达代码(if(false){})
            dead_code: true,
            // 删除 console.xxx 调用
            drop_console: true,  // ['log', 'warn'] 可以精细控制
            // 删除 debugger 语句
            drop_debugger: true,
            // 常量折叠
            evaluate: true,
            // 删除无用变量赋值
            unused: true,
          },
          mangle: {
            // 变量名混淆
            toplevel: true,       // 顶层变量名也混淆
            keep_classnames: false,
            keep_fnames: false,
          },
        },
      }),
    ],
  },
};

保留 License 注释 vs 删除所有注释

场景1:开源库 + 法律合规(必须保留 License)

javascript 复制代码
new TerserPlugin({
  extractComments: {
    // 匹配需要提取的注释(License 类型)
    condition: /^**!|@preserve|@license|@cc_on/i,
    filename: (fileData) => {
      return `${fileData.filename}.LICENSE.txt`;
    },
    banner: (licenseFile) => {
      return `License information can be found in ${licenseFile}`;
    },
  },
  terserOptions: {
    format: {
      comments: false, // 内联注释全删,License 已提取到单独文件
    },
  },
});

场景2:内部项目(全部删除)

yaml 复制代码
new TerserPlugin({
  extractComments: false,  // 不生成 LICENSE 文件
  terserOptions: {
    format: { comments: false },  // 删除所有内联注释
  },
});

UglifyJS vs Terser vs esbuild 压缩能力对比

维度 UglifyJS Terser esbuild
语言支持 ES5(不支持 ES6+) ES2020+ ✅ ES2022+ ✅
压缩率 中(稍逊于 Terser)
速度 慢(JS 实现) 极快(Go 实现)⚡⚡⚡
注释处理 支持 支持(更细粒度)✅ 支持(较简单)
Mangle 支持 支持(更多选项)✅ 支持
Source Map 支持 支持 支持
维护状态 已停更 ❌ 活跃维护 ✅ 活跃维护 ✅
Webpack 集成 uglifyjs-webpack-plugin(废弃) 内置 TerserPlugin ESBuildMinifyPlugin

速度比较(典型项目,单位 ms):

makefile 复制代码
Terser:  800ms ~ 2000ms  (纯 JS 实现,单线程)
esbuild: 50ms ~ 200ms    (Go 实现,多线程,快 10~40x)
SWC:     100ms ~ 400ms   (Rust 实现,居中)

代码混淆:变量名替换、属性名缩短

javascript 复制代码
// 原始代码
function calculateUserDiscount(userLevel, purchaseAmount) {
  const discountRate = userLevel === 'premium' ? 0.2 : 0.1;
  return purchaseAmount * discountRate;
}

// Terser mangle 后(变量名单字母化)
function a(b, c) {
  const d = b === 'premium' ? 0.2 : 0.1;
  return c * d;
}

// Terser mangle.properties 属性名混淆(激进,慎用!)
// 注意:会混淆所有属性名,可能破坏与外部 API 的交互

属性名混淆注意事项:

css 复制代码
terserOptions: {
  mangle: {
    properties: {
      // 只混淆以 _ 开头的属性(私有约定)
      regex: /^_/,
      // 保留特定属性名
      reserved: ['__esModule', '__webpack_require__'],
    }
  }
}

Pure annotation(/*#__PURE__*/)与 Tree Shaking 配合

/*#__PURE__*/ 是一个特殊注释,告诉打包工具:"这个函数调用没有副作用,如果结果不被使用,可以安全删除。"

typescript 复制代码
// 问题:Webpack 无法判断 React.createElement 是否有副作用
// 所以默认不删除,即使 Button 未被使用
const Button = React.createElement(BaseButton, { type: 'button' });

// 解决:加上 /*#__PURE__*/ 注解
const Button = /*#__PURE__*/ React.createElement(BaseButton, { type: 'button' });
// 现在 Webpack 知道:如果 Button 没被使用,可以整个删掉

// Babel 会自动为 JSX 添加 #__PURE__ 注解
const element = <Button />
// 编译后:
const element = /*#__PURE__*/ React.createElement(Button, null);

// 类方法装饰器也需要
class MyClass {
  @memoize
  getValue() { return 42; }
}
// 编译后,Babel 会加注解确保 Tree Shaking 有效

实际效果验证:

javascript 复制代码
// utils.js(有副作用的库)
console.log('模块加载时执行'); // 副作用!即使不使用也会执行
export const expensiveOp = /*#__PURE__*/ createHeavyObject();
//                         ^^^^^^^^^^^ 告诉 Webpack:createHeavyObject() 可安全省略

// main.js
import { expensiveOp } from './utils'; // 如果 expensiveOp 未使用
// 无注解:两者都保留在 bundle 中
// 有注解:expensiveOp 被删除,但 console.log 因有副作用仍保留

二、Vite 核心原理

2.1 开发模式(No-bundle 革命)

No-bundle 理念:为什么不预打包

传统 Webpack Dev Server 的问题:

markdown 复制代码
项目启动时:
  1. 从 entry 开始,解析所有模块(可能有 1000+ 个)
  2. 每个模块都要经过 Loader 转换
  3. 生成完整的 bundle.js
  时间:中型项目 30s ~ 2min

文件修改时:
  1. 找出受影响的模块(依赖链条很长)
  2. 重新打包受影响的 chunk
  时间:3s ~ 20s(热更新)

Vite 的方案(利用原生 ESM):

markdown 复制代码
项目启动时:
  1. 只做依赖预构建(esbuild 处理 node_modules,快!)
  2. 启动 dev server(Koa HTTP 服务 + WebSocket HMR)
  时间:< 500ms(超快!)

文件修改时:
  1. 只有被请求的模块才编译
  2. 模块粒度的 HMR(精确更新)
  时间:< 100ms

基于原生 ESM 的模块加载

xml 复制代码
<!-- Vite dev 模式下,index.html 中的 script -->
<script type="module" src="/src/main.ts"></script>
javascript 复制代码
// 浏览器发起请求:GET /src/main.ts
// Vite Dev Server 接收,实时编译,返回 JS

// 原始 main.ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

// Vite 编译后(简化)返回给浏览器:
import { createApp } from '/@fs/.../node_modules/vue/dist/vue.esm-bundler.js'
import App from '/src/App.vue'
createApp(App).mount('#app')
// 浏览器再根据这些 import 发起后续请求

关键:浏览器原生 ESM 是按需加载的,只有被 import 的模块才会发起请求。

依赖预构建(esbuild 预打包 node_modules)

为什么需要预构建:

  1. CJS/UMD → ESM 转换: 大量 npm 包只有 CommonJS 版本,浏览器不支持 require()
  2. 模块合并: lodash-es 有 600+ 个小模块,每个 import 都是一个 HTTP 请求,太慢
  3. 深层依赖树: 某些包有数百个内部 require(),需要合并成单文件
javascript 复制代码
// 预构建过程(简化)
import esbuild from 'esbuild';

// 分析 package.json 和入口文件,找出所有依赖
const deps = scanDependencies('./src/main.ts');

// 用 esbuild 批量预构建
await esbuild.build({
  entryPoints: Object.keys(deps),
  bundle: true,
  format: 'esm',  // 输出 ESM 格式
  outdir: './node_modules/.vite/deps',  // 缓存目录
  splitting: true,  // 代码分割(共享依赖提取)
});

// 预构建结果被缓存,重启不需要重新构建(除非 node_modules 变化)

预构建缓存策略:

  • 缓存目录:node_modules/.vite/deps/
  • 缓存有效期:基于 lock file、node_modules 时间戳、vite.config 内容的 hash
  • 手动清除:vite --force 或删除 .vite 目录

按需编译:只有请求到的模块才编译

arduino 复制代码
用户访问 http://localhost:5173
  ↓
Vite Dev Server(Koa 中间件)
  ↓
请求 /src/main.ts
  ↓ (首次请求才编译)
ts → js(esbuild transform)
  ↓
返回编译后的 JS
  ↓
浏览器解析 import,发起新请求
  ↓
请求 /src/App.vue
  ↓ (首次请求才编译)
.vue → js(vite:vue 插件处理)
  ↓
只有用户实际访问到的路由/组件才会被编译

HMR 原理(Vite vs Webpack,更快的原因)

Webpack HMR 局限:

复制代码
文件修改 → 找出受影响的所有模块 → 重新生成受影响的 chunk → 推送给浏览器
问题:依赖链很深时,可能一个小修改导致重新处理很多模块

Vite HMR 优势:

scss 复制代码
文件修改(精确到模块)
  ↓
查找 HMR 边界(向上找最近的 accept())
  ↓
只有边界内的模块需要重新请求
  ↓
通过 WebSocket 推送精确的 update 消息:
  { type: 'update', updates: [{ path: '/src/Counter.vue' }] }
  ↓
浏览器直接 import() 新版本模块
  ↓
Vue/React Fast Refresh 框架层面执行更新

Vite 更快的核心原因:

  1. 原生 ESM 粒度更细:每个文件是独立模块,HMR 边界更小
  2. 无需重新打包 chunk:不需要重新生成任何 bundle
  3. 浏览器缓存:未修改的模块有 HTTP 304 缓存
  4. esbuild 编译:即使需要重新编译,esbuild 比 babel 快 20~100x

2.2 生产模式(Rollup 打包)

为何生产仍用 Rollup 打包

直接用 ESM + 浏览器加载的问题:

  1. HTTP 请求过多(数百个模块 = 数百个请求,即使 HTTP/2 也有性能损耗)
  2. 无法合并小模块,代码重复
  3. Tree Shaking 效果不理想(浏览器不执行 Tree Shaking)
  4. 无法进行 code splitting 最优化
  5. CSS 处理复杂(需要合并和 code splitting 对应)

Rollup 的优势(适合生产打包):

  • ESM 输出最纯净(无运行时 runtime 代码)
  • Tree Shaking 最彻底(静态分析能力强)
  • Scope Hoisting(内联模块,减少闭包开销)
  • 成熟的 code splitting(dynamic import)

Rolldown 替换 Rollup(Vite 8+)

markdown 复制代码
Rolldown = Rust 重写的 Rollup 兼容实现

性能提升:
  Rollup(JS)   → 构建 1000 个模块:~800ms
  Rolldown(Rust)→ 构建 1000 个模块:~100ms
  提速:5~10x(甚至更高)

兼容性:
  - 完全兼容 Rollup 插件 API
  - 输出格式相同
  - 正在逐步替换(Vite 8 中 Rolldown GA)

构建流程:analyze → bundle → optimize → emit

php 复制代码
// Vite 生产构建(vite build)内部流程

// Phase 1: Analyze(分析入口)
const bundle = await rollup.rollup({
  input: resolveEntry(viteConfig),
  plugins: [
    ...vitePlugins,
    ...rollupPlugins,
  ],
  // Rollup 分析依赖图
});

// Phase 2: Bundle(代码分割 + 合并)
const { output } = await bundle.generate({
  format: 'es',
  chunkFileNames: 'assets/[name]-[hash].js',
  entryFileNames: 'assets/[name]-[hash].js',
  assetFileNames: 'assets/[name]-[hash][extname]',
  manualChunks: viteConfig.build.rollupOptions?.output?.manualChunks,
});

// Phase 3: Optimize(压缩 + 内联)
for (const chunk of output) {
  if (chunk.type === 'chunk') {
    chunk.code = await minify(chunk.code);  // esbuild/terser 压缩
  }
}

// Phase 4: Emit(写入磁盘)
await writeOutputFiles(output, viteConfig.build.outDir);

2.3 Vite 插件系统

Vite 插件 = Rollup 插件超集

javascript 复制代码
// Rollup 插件(可直接在 Vite 中使用)
const myRollupPlugin = {
  name: 'my-rollup-plugin',

  // Rollup 标准 hooks(dev + build 都执行)
  resolveId(id, importer) { /* 解析模块路径 */ },
  load(id) { /* 加载模块内容 */ },
  transform(code, id) { /* 转换模块代码 */ },
  buildStart(options) { /* 构建开始 */ },
  buildEnd(error) { /* 构建结束 */ },
  generateBundle(options, bundle) { /* 生成 bundle */ },
};

// Vite 特有 hooks(仅 dev 模式)
const myVitePlugin = {
  name: 'my-vite-plugin',

  // 访问解析后的 Vite 配置
  configResolved(config) {
    console.log('当前模式:', config.command); // 'serve' | 'build'
  },

  // 修改 index.html(注入脚本/样式/meta)
  transformIndexHtml(html) {
    return html.replace(
      '<head>',
      `<head><meta name="build-time" content="${new Date().toISOString()}">`
    );
    // 或者返回数组格式(更精细控制注入位置)
    return {
      html,
      tags: [
        {
          tag: 'script',
          attrs: { src: '/analytics.js' },
          injectTo: 'body',
        },
      ],
    };
  },

  // 处理 HMR 更新(精细控制哪些文件的 HMR 行为)
  handleHotUpdate({ file, server, modules }) {
    if (file.endsWith('.json')) {
      // JSON 文件变化,触发全量重载
      server.ws.send({ type: 'full-reload' });
      return []; // 返回空数组表示自己处理了,不走默认逻辑
    }
    // 返回 undefined 走默认 HMR 逻辑
  },

  // Dev Server 配置(添加自定义路由/中间件)
  configureServer(server) {
    server.middlewares.use('/api/mock', (req, res) => {
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({ mock: true }));
    });
  },
};

插件执行顺序(enforce: pre/normal/post)

php 复制代码
// Vite 插件执行顺序(关键!)

// 1. enforce: 'pre' 插件(最先执行)
// 用途:需要在其他插件处理之前修改代码(如 @vitejs/plugin-vue)

// 2. 普通 Vite 插件(无 enforce)

// 3. enforce: 'post' 插件(最后执行)
// 用途:需要在其他转换完成后处理(如分析器、压缩器)

// 实际示例
export default defineConfig({
  plugins: [
    // pre: 最先处理 .vue 文件
    { ...vuePlugin, enforce: 'pre' },

    // normal: 正常顺序
    reactPlugin(),

    // post: 最后执行(可看到所有转换后的最终结果)
    { ...analyzerPlugin, enforce: 'post' },
  ],
});

// 完整执行顺序:
// pre.config → normal.config → post.config
// pre.configResolved → normal.configResolved → post.configResolved
// pre.resolveId → normal.resolveId → post.resolveId
// pre.load → normal.load → post.load
// pre.transform → normal.transform → post.transform

三、Rollup 核心原理

3.1 为何适合库打包(ESM 输出纯净,无 runtime)

Webpack 打包库的问题:

csharp 复制代码
// Webpack 打包后,即使是一个简单的库,也会包含大量 runtime 代码:
/******/ (() => { // webpackBootstrap
/******/   var __webpack_modules__ = ({
/******/     "./src/index.js": ((__unused_webpack_module, __webpack_exports__) => {
// ... 大量 runtime 代码
/******/ })();

Rollup 打包库的输出(极其纯净):

css 复制代码
// rollup 输出(ESM 格式)
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;

export { add, multiply };
// 就这么干净!没有任何 runtime 开销

Rollup 为什么适合库:

  1. 无 runtime 代码(Webpack 有 __webpack_require__ 等运行时)
  2. 输出格式灵活(ESM/CJS/UMD/IIFE,一次构建多个格式)
  3. 天然 Tree Shaking(所有文件默认 ESM 处理)
  4. 用户可以基于 Rollup 输出再做优化

3.2 Scope Hoisting(模块内联,减少闭包)

javascript 复制代码
// 有两个模块
// math.js
export const PI = 3.14159;
export const circumference = radius => 2 * PI * radius;

// main.js
import { circumference } from './math';
console.log(circumference(5));

Webpack 打包(有 module 闭包):

ini 复制代码
// 每个模块都包裹在函数中(为了模拟 CommonJS 作用域)
__webpack_modules__["./math.js"] = (module, exports) => {
  const PI = 3.14159;
  const circumference = radius => 2 * PI * radius;
  exports.circumference = circumference;
};
// 主模块
const math = __webpack_require__("./math.js");
console.log(math.circumference(5));

Rollup 打包(Scope Hoisting):

ini 复制代码
// 模块被"内联"到同一作用域
const PI = 3.14159;
const circumference = radius => 2 * PI * radius;
console.log(circumference(5));
// 完全扁平化,无额外函数调用开销

性能好处:

  • 减少函数调用(JS 引擎优化更容易)
  • 减少闭包(内存占用更少)
  • 代码体积更小
  • V8 内联优化(inline)效果更好

3.3 Tree Shaking 比 Webpack 更彻底的原因

原因一:Scope Hoisting 让死代码更容易识别

javascript 复制代码
// math.js
export const add = (a, b) => a + b;         // 被使用
export const subtract = (a, b) => a - b;    // 未被使用

// main.js
import { add } from './math';
console.log(add(1, 2));

Rollup 的处理:

csharp 复制代码
// 内联后,subtract 从来没有被引用 → 直接删除
const add = (a, b) => a + b;
console.log(add(1, 2));
// subtract 彻底消失!

Webpack 的处理:

arduino 复制代码
// 标记 subtract 为 unused,由 Terser 最终删除
// 但仍然有 module 闭包的间接引用,Terser 要分析才能删除

原因二:Rollup 的 Statement 级别分析

Rollup 可以精确到每一条语句(statement)的副作用分析,粒度比 Webpack 更细。

原因三:整个项目默认 ESM

Rollup 在一开始就假设所有模块都是 ESM,不需要处理 CommonJS 的动态性。


四、esbuild 核心原理

4.1 Go 语言实现,为何这么快

速度对比(构建一个中等规模项目):

makefile 复制代码
webpack(5):  12,000 ms
rollup:         9,000 ms
parcel 2:       8,000 ms
esbuild:          200 ms  ← 快 40~60 倍!

为什么这么快(四大原因):

1. Go 语言编译为原生机器码

复制代码
JS(Node.js)→ V8 JIT 编译 → 机器码(动态,有 JIT 开销)
Go           → 静态编译   → 原生机器码(无 JIT 开销)
性能差距:原生代码通常比 JIT 快 3~10 倍

2. 并行处理(利用多核 CPU)

go 复制代码
// esbuild 内部伪代码
func buildAll(files []string) {
  // 用 goroutine 并行处理所有文件
  var wg sync.WaitGroup
  results := make(chan Result, len(files))

  for _, file := range files {
    wg.Add(1)
    go func(f string) {
      defer wg.Done()
      results <- parseAndTransform(f)  // 并行!
    }(file)
  }

  wg.Wait()
  // JS/Python 受 GIL 限制,真并行很难做到
  // Go 原生支持轻量级 goroutine(M:N 线程模型)
}

3. 没有过度的抽象层

css 复制代码
Webpack 处理一个文件:
  Plugin A → Plugin B → Loader C → Loader D → 多次 AST 转换 → ...
  每次转换:代码 → AST → 代码 → AST(来回多次)

esbuild 处理一个文件:
  一次 Parse(AST)→ 一次 Transform → 一次 Print
  整个过程只做一次 AST 解析,减少了大量序列化/反序列化

4. 高效的内存使用

复制代码
Go 的内存分配效率远高于 JS
GC 压力更小(Go 的 GC 针对低延迟优化)
缓存利用率高(数据结构紧凑,CPU cache 友好)

4.2 功能边界

esbuild 有意保持简单,不支持:

功能 支持情况 原因
HMR 热更新 ❌ 不支持 需要 dev server,超出 bundler 范畴
复杂代码分割 ⚠️ 基础支持 动态 import 支持,但 chunk 分组策略简单
CSS Modules ❌ 不支持 复杂性高,交给 PostCSS 等工具
插件生态 ✅ 支持(API 简单) 但比 Rollup/Webpack 少
TypeScript 类型检查 ❌ 仅转换,不检查 转换很快,但 tsc 检查交给 IDE/CI
Vue SFC ❌ 无官方支持 需要第三方插件,功能有限

esbuild 擅长的:

  • 极速 JS/TS/JSX 转换(Transform,不是 Build)
  • 简单项目的全量打包(无复杂分割需求)
  • 作为其他工具的编译器内核(Vite 用它做预构建)
  • 压缩(Minify,速度远超 Terser)

4.3 在 Vite 中的角色

sql 复制代码
Vite Dev Server
  ├── 依赖预构建:esbuild(将 node_modules CJS → ESM,合并小包)
  ├── 单文件转换:esbuild(TypeScript/JSX → JS,速度快)
  └── HMR/路由/插件系统:Vite 自己实现

Vite Build(生产)
  ├── 打包:Rollup(或 Rolldown)← 不用 esbuild,原因见下
  └── 压缩:esbuild(可选,比 Terser 快 10-20x)

为什么 Vite 生产不用 esbuild 打包:
  - esbuild 的 code splitting 不够完善
  - CSS 处理能力有限
  - Tree Shaking 效果不如 Rollup
  - 缺少 Rollup 丰富的插件生态

五、横向对比

维度 Webpack 5 Vite 8 Rollup 4 esbuild
冷启动速度 慢(全量打包,10s~2min) 极快(< 1s,ESM按需)⚡⚡⚡ 中(5s30s) 极快(< 1s)⚡⚡⚡
热更新速度 中(需重新打包chunk,3~20s) 极快(模块级HMR,< 100ms)⚡⚡⚡ 无(库打包,无dev server) 无(需自行实现)
生产构建速度 慢(大项目 2~5min) 快(Rolldown GA后 5~10x提速)⚡⚡ 中等 ⚡ 最快(10~40x)⚡⚡⚡
开发体验 配置复杂,上手成本高 开箱即用,配置简单 ⭐⭐⭐ 配置手动,适合库开发 无完整 Dev 体验
生产优化 功能最全(splitChunks/scope hoisting/多种优化)⭐⭐⭐ Rollup/Rolldown 生产,优化能力强 ⭐⭐⭐ 输出最纯净,无 runtime ⭐⭐⭐ 基础优化,压缩超快 ⭐⭐
代码分割 最强(splitChunks 精细配置)⭐⭐⭐ 依赖 Rollup(manualChunks)⭐⭐ 基于 dynamic import,简洁 ⭐⭐ 基础 dynamic import ⭐
Tree Shaking 支持(需 ESM + production 模式)⭐⭐ 依赖 Rollup(更彻底)⭐⭐⭐ 最彻底(Scope Hoisting)⭐⭐⭐ 支持(效果中等)⭐⭐
插件生态 最成熟(数千个 loader/plugin)⭐⭐⭐ 兼容 Rollup 插件 + 专有插件 ⭐⭐⭐ 生态丰富,库开发覆盖完整 ⭐⭐ 生态较少,API 简单 ⭐
TypeScript 需要 ts-loader 或 babel-loader 内置支持(esbuild 转换)⭐⭐⭐ 需插件(@rollup/plugin-typescript)⭐⭐ 原生支持(极快)⭐⭐⭐
CSS 处理 功能最完整(CSS Modules/PostCSS/预处理器)⭐⭐⭐ 内置(PostCSS/CSS Modules/预处理器)⭐⭐⭐ 需插件,基础支持 ⭐ 基础CSS,无 Modules ⭐
SSR 支持 支持(较复杂)⭐⭐ 内置 SSR 模式 ⭐⭐⭐ 不直接支持 ⭐ 不支持
学习曲线 陡峭(配置项多)❗❗❗ 平缓(约定大于配置)✅ 中等(主要配置 input/output/plugins)✅ 简单(API 极少)✅
适用场景 大型复杂应用(遗留项目/微前端/特殊需求) 新项目首选(应用开发/SSR/微前端)⭐ 库/组件包发布首选 ⭐ 工具链内核(如 Vite 预构建)⭐
典型用户 大厂存量项目/CRA 新项目/Vue3/React新项目 React/Vue 生态库作者 Vite/Bun/Deno 内部

六、实战配置

6.1 Webpack 5 完整配置(含注释剥离、压缩、splitChunks)

javascript 复制代码
// webpack.config.js(完整生产配置)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  mode: isDev ? 'development' : 'production',

  // 入口(支持多入口)
  entry: {
    main: './src/index.tsx',
    // polyfill: './src/polyfill.ts', // 可拆分 polyfill
  },

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: isDev ? 'js/[name].js' : 'js/[name].[contenthash:8].js',
    chunkFilename: isDev ? 'js/[name].chunk.js' : 'js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
    clean: true, // 构建前清空 dist
    publicPath: '/',
  },

  // Source Map
  devtool: isDev ? 'eval-cheap-module-source-map' : 'hidden-source-map',

  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.jsx', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
    // 优先使用 ESM 版本(Tree Shaking 更好)
    mainFields: ['module', 'browser', 'main'],
  },

  module: {
    rules: [
      // TypeScript / JavaScript
      {
        test: /.[jt]sx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', {
                  useBuiltIns: 'usage',
                  corejs: 3,
                  targets: '> 0.5%, not dead',
                }],
                '@babel/preset-typescript',
                ['@babel/preset-react', { runtime: 'automatic' }],
              ],
              // 禁用注释(生产环境,babel 不保留)
              comments: isDev,
              plugins: [
                isDev && require.resolve('react-refresh/babel'),
              ].filter(Boolean),
            },
          },
        ],
      },

      // CSS
      {
        test: /.css$/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              modules: {
                // CSS Modules:文件名带 .module.css
                auto: /.module.css$/,
                localIdentName: isDev
                  ? '[path][name]__[local]'
                  : '[contenthash:8]',
              },
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: ['autoprefixer', 'postcss-preset-env'],
              },
            },
          },
        ],
      },

      // SCSS
      {
        test: /.s[ac]ss$/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      },

      // 图片(Webpack 5 内置 asset modules,不需要 file-loader)
      {
        test: /.(png|jpe?g|gif|webp|svg)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024, // < 8KB 转 base64
          },
        },
      },

      // 字体
      {
        test: /.(woff2?|eot|ttf|otf)$/,
        type: 'asset/resource',
      },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      favicon: './public/favicon.ico',
      // 注入时,自动添加 contenthash 的 script/link 标签
      minify: isDev ? false : {
        removeComments: true,        // 删除 HTML 注释
        collapseWhitespace: true,
        removeAttributeQuotes: false,
      },
    }),

    !isDev && new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css',
    }),

    // 分析产物大小(按需开启)
    process.env.ANALYZE && new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
    }),
  ].filter(Boolean),

  optimization: {
    minimize: !isDev,
    minimizer: [
      // JS 压缩(含注释剥离)
      new TerserPlugin({
        parallel: true,           // 多进程压缩
        extractComments: false,   // 不生成 .LICENSE.txt(内部项目)

        terserOptions: {
          ecma: 2020,
          format: {
            // 🔑 核心:删除所有注释
            comments: false,
            // 保留 License:comments: /^**!|@preserve|@license/i
          },
          compress: {
            drop_console: true,   // 删除 console.log
            drop_debugger: true,
            dead_code: true,      // 删除不可达代码
            evaluate: true,       // 常量折叠
            passes: 2,            // 多次压缩(更彻底,稍慢)
            pure_funcs: ['console.info', 'console.debug', 'console.warn'],
          },
          mangle: {
            safari10: true,       // 修复 Safari 10 bug
          },
        },
      }),

      // CSS 压缩
      new CssMinimizerPlugin({
        minimizerOptions: {
          preset: ['default', {
            discardComments: { removeAll: true }, // 删除所有 CSS 注释
          }],
        },
      }),
    ],

    // 代码分割(精细配置)
    splitChunks: {
      chunks: 'all',
      minSize: 20000,       // 最小 20KB 才分割
      maxSize: 244000,      // 最大 244KB(超过继续分割)
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        // React 核心库单独打包(长期缓存)
        reactVendors: {
          test: /[\/]node_modules[\/](react|react-dom|react-router|react-router-dom|scheduler)[\/]/,
          name: 'react-vendors',
          chunks: 'initial',
          priority: 30,
          enforce: true,  // 忽略 minSize/maxSize
        },

        // 其他 node_modules
        vendors: {
          test: /[\/]node_modules[\/]/,
          name(module) {
            // 按包名分组(更细粒度的缓存)
            const packageName = module.context.match(
              /[\/]node_modules[\/](.*?)([\/]|$)/
            )[1];
            return `npm.${packageName.replace('@', '')}`;
          },
          priority: 20,
          reuseExistingChunk: true,
        },

        // 公共业务代码(被 2+ chunk 引用)
        common: {
          minChunks: 2,
          name: 'common',
          priority: 10,
          reuseExistingChunk: true,
        },
      },
    },

    // 将 webpack runtime 单独提取(避免每次内容变化影响 vendors hash)
    runtimeChunk: {
      name: 'runtime',
    },

    // Tree Shaking 相关
    usedExports: true,      // 标记 used exports
    concatenateModules: true, // Scope Hoisting
    innerGraph: true,       // 追踪模块内部依赖(更精确的 Tree Shaking)
    sideEffects: true,      // 读取 package.json 中的 sideEffects
  },

  // 开发服务器
  devServer: {
    port: 3000,
    hot: true,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },

  // 性能预算
  performance: {
    hints: isDev ? false : 'warning',
    maxEntrypointSize: 512 * 1024,  // 500KB
    maxAssetSize: 512 * 1024,
  },
};

6.2 Vite 完整配置(含自定义插件、构建优化)

javascript 复制代码
// vite.config.ts
import { defineConfig, loadEnv, Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';

// 自定义插件:自动注入构建信息到 window.__BUILD_INFO__
function buildInfoPlugin(): Plugin {
  return {
    name: 'build-info',
    enforce: 'post',

    // 构建开始时生成构建信息
    buildStart() {
      this.buildInfo = {
        time: new Date().toISOString(),
        version: process.env.npm_package_version,
        commit: process.env.COMMIT_SHA || 'dev',
      };
    },

    // 在 index.html 中注入构建信息
    transformIndexHtml(html) {
      return {
        html,
        tags: [{
          tag: 'script',
          attrs: { type: 'text/javascript' },
          children: `window.__BUILD_INFO__ = ${JSON.stringify(this.buildInfo)};`,
          injectTo: 'head-prepend',
        }],
      };
    },
  };
}

// 自定义插件:移除生产环境中的 console.log
function removeConsolePlugin(): Plugin {
  return {
    name: 'remove-console',
    transform(code, id) {
      // 只处理生产环境的 JS 文件
      if (process.env.NODE_ENV !== 'production') return;
      if (!id.match(/.[jt]sx?$/)) return;
      if (id.includes('node_modules')) return;

      // 简单方案:正则替换(生产中应用 esbuild 选项,更可靠)
      return code.replace(/console.(log|debug|info)(.*?);?/g, '');
    },
  };
}

export default defineConfig(({ mode }) => {
  // 加载环境变量(.env, .env.production 等)
  const env = loadEnv(mode, process.cwd(), '');
  const isProd = mode === 'production';

  return {
    // 全局别名
    resolve: {
      alias: {
        '@': resolve(__dirname, 'src'),
        '@components': resolve(__dirname, 'src/components'),
        '@hooks': resolve(__dirname, 'src/hooks'),
        '@utils': resolve(__dirname, 'src/utils'),
        '@assets': resolve(__dirname, 'src/assets'),
      },
    },

    // 全局 CSS 变量注入
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@import "@/styles/variables.scss";`,
        },
      },
      modules: {
        // CSS Modules 类名格式
        generateScopedName: isProd
          ? '[hash:base64:8]'
          : '[name]__[local]__[hash:base64:5]',
      },
    },

    // 插件
    plugins: [
      react({
        // React Fast Refresh(dev)/ 自动 JSX runtime
        babel: {
          plugins: [
            // 只在开发环境开启 Fast Refresh
            ...(!isProd ? [['babel-plugin-react-refresh', {}]] : []),
          ],
        },
      }),

      buildInfoPlugin(),
      isProd && removeConsolePlugin(),

      // Bundle 大小分析(ANALYZE=true vite build)
      process.env.ANALYZE && visualizer({
        open: true,
        gzipSize: true,
        brotliSize: true,
        filename: 'dist/stats.html',
      }),
    ].filter(Boolean),

    // 环境变量(暴露给前端)
    define: {
      '__APP_VERSION__': JSON.stringify(env.npm_package_version),
      '__API_BASE__': JSON.stringify(env.VITE_API_BASE_URL),
    },

    // Dev Server
    server: {
      port: 5173,
      host: true,  // 允许局域网访问
      open: true,
      proxy: {
        '/api': {
          target: env.VITE_API_PROXY_TARGET,
          changeOrigin: true,
          rewrite: (path) => path.replace(/^/api/, ''),
        },
      },
    },

    // 预构建(依赖预构建优化)
    optimizeDeps: {
      // 手动添加需要预构建的依赖(通常 Vite 自动检测)
      include: ['lodash-es', 'axios', 'dayjs'],
      // 排除不需要预构建的(纯 ESM 的库)
      exclude: ['@vueuse/core'],
    },

    // 生产构建
    build: {
      target: 'es2020',
      outDir: 'dist',
      assetsDir: 'assets',
      sourcemap: isProd ? 'hidden' : true,  // 生产用 hidden(Sentry 上传用)

      // Rollup 配置
      rollupOptions: {
        output: {
          // 手动分割 chunks(精细控制 vendor 缓存)
          manualChunks: (id) => {
            // React 生态单独 chunk
            if (id.includes('/node_modules/react') ||
                id.includes('/node_modules/react-dom') ||
                id.includes('/node_modules/scheduler')) {
              return 'react-vendor';
            }
            // 路由
            if (id.includes('/node_modules/react-router')) {
              return 'router';
            }
            // 工具库
            if (id.includes('/node_modules/lodash') ||
                id.includes('/node_modules/dayjs') ||
                id.includes('/node_modules/axios')) {
              return 'utils-vendor';
            }
            // 其他 node_modules(统一打包)
            if (id.includes('/node_modules/')) {
              return 'vendor';
            }
          },

          // 文件命名
          chunkFileNames: 'js/[name]-[hash].js',
          entryFileNames: 'js/[name]-[hash].js',
          assetFileNames: ({ name }) => {
            if (/.(png|jpe?g|gif|svg|webp)$/.test(name || '')) {
              return 'images/[name]-[hash][extname]';
            }
            if (/.(css)$/.test(name || '')) {
              return 'css/[name]-[hash][extname]';
            }
            if (/.(woff2?|eot|ttf|otf)$/.test(name || '')) {
              return 'fonts/[name]-[hash][extname]';
            }
            return 'assets/[name]-[hash][extname]';
          },
        },
      },

      // 压缩配置
      minify: 'esbuild',   // 'terser' | 'esbuild' | false
      // esbuild 比 terser 快 10~20x,但压缩率略低

      // esbuild 压缩选项(minify: 'esbuild' 时有效)
      // esbuildOptions 通过 vite 的 esbuild 选项配置
      // 注意:移除注释通过 esbuild 的 legalComments 控制

      // CSS 代码分割(每个 async chunk 提取独立 CSS)
      cssCodeSplit: true,

      // 静态资源 inline 阈值
      assetsInlineLimit: 8192,  // 8KB 以下 inline

      // chunk 大小警告阈值
      chunkSizeWarningLimit: 1000,  // 1000KB

      // 是否生成 manifest.json(用于后端路由集成)
      manifest: isProd,
    },

    // esbuild 转换配置(dev + build 都生效)
    esbuild: {
      // 删除 console 和 debugger
      drop: isProd ? ['console', 'debugger'] : [],
      // 删除注释
      legalComments: isProd ? 'none' : 'inline',
      // JSX 注入
      jsxImportSource: 'react',
    },
  };
});

6.3 如何验证 Tree Shaking 有效

方法一:Bundle Analyzer 可视化

ini 复制代码
# Webpack
ANALYZE=true webpack --config webpack.config.js
# 打开 http://localhost:8888 查看交互式图表

# Vite
ANALYZE=true vite build
# 打开 dist/stats.html

使用 webpack-bundle-analyzer:

javascript 复制代码
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    process.env.ANALYZE && new BundleAnalyzerPlugin({
      analyzerMode: 'server',  // 启动本地服务器
      // analyzerMode: 'static', // 生成静态 HTML
      openAnalyzer: true,
      generateStatsFile: true,
      statsFilename: 'stats.json',
    }),
  ].filter(Boolean),
};

Vite / Rollup:

php 复制代码
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

plugins: [
  visualizer({
    open: true,
    gzipSize: true,
    brotliSize: true,
    template: 'treemap',  // 'treemap' | 'sunburst' | 'network'
  }),
]

方法二:source-map-explorer 精确分析

perl 复制代码
npm install -g source-map-explorer

# Webpack(需要开启 source-map)
npx source-map-explorer dist/js/main.*.js
# 会打开浏览器显示每个依赖的实际大小

方法三:手动验证(小实验)

javascript 复制代码
// math.js(测试库)
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;   // 故意不使用
export const multiply = (a, b) => a * b;   // 故意不使用

// main.js(只用 add)
import { add } from './math';
console.log(add(1, 2));
perl 复制代码
# 构建后,搜索 bundle 中是否包含 subtract/multiply
grep -r "subtract" dist/
grep -r "multiply" dist/

# 如果 Tree Shaking 有效:找不到这两个函数
# 如果无效:能找到(说明 Tree Shaking 失效)

方法四:sideEffects 验证

json 复制代码
// package.json(告知所有文件无副作用)
{
  "sideEffects": false
}
arduino 复制代码
// 验证方法
// 引入一个文件,但不使用任何导出
import './utils/logger'; // 只是 import,没有用任何东西

// 如果 sideEffects: false,这个 import 应该被 Tree Shaking 删除
// 检查 bundle 中是否包含 logger 的代码

方法五:Webpack Stats JSON 分析

java 复制代码
// webpack.config.js
module.exports = {
  stats: {
    // 详细统计信息
    optimizationBailout: true,  // 显示为什么 Tree Shaking 失败的模块
  },
};
lua 复制代码
webpack --json > stats.json
# 用 https://webpack.github.io/analyse/ 上传 stats.json 分析

常见 Tree Shaking 失效原因排查:

问题 原因 解决方案
import * as xxx from 命名空间导入,无法确定用哪些 改为具名导入 import { add } from
CommonJS 依赖 require() 动态,无法静态分析 找 ESM 版本(lodash-es 替代 lodash
sideEffects 未配置 Webpack 保守策略,不删除 package.json 配置 sideEffects: false
Babel 编译 ESM → CJS 某些旧配置会把 ESM 转成 CJS 确保 @babel/preset-envmodules: false
副作用代码 模块顶层有副作用(如修改全局对象) /*#__PURE__*/ 或移除副作用

七、常见面试题精选

Q1:Webpack 的 Loader 和 Plugin 有什么区别?

Loader: 文件转换器,专注于单文件的转换(非 JS/JSON → JS)

  • 在模块加载阶段运行
  • 是一个函数,输入 source,输出转换后的 source
  • 有序执行(pitch 从左到右,normal 从右到左)

Plugin: 功能扩展器,可以介入构建的任意阶段

  • 基于 Tapable 事件系统
  • 可以访问 Compiler 和 Compilation 对象
  • 能做任何 Loader 不能做的事(生成文件、修改 bundle、添加资源等)

Q2:为什么 Vite 开发环境这么快,生产用 Rollup?

开发快: 利用浏览器原生 ESM,无需打包 = 零构建时间,只有 esbuild 预构建 node_modules
生产用 Rollup:

  • 浏览器加载 1000+ 个 ESM 文件网络开销巨大(即使 HTTP/2)
  • Rollup Tree Shaking 更彻底,输出更纯净
  • 成熟的 code splitting 和插件生态

Q3:如何彻底删除所有代码注释?

yaml 复制代码
// Webpack + TerserPlugin
new TerserPlugin({
  extractComments: false,
  terserOptions: {
    format: { comments: false },
  },
})

// Vite + esbuild
esbuild: {
  legalComments: 'none',  // 删除所有注释包括 License
}
// 或 build.minify: 'terser' + terserOptions

Q4:Tree Shaking 的必要条件是什么?

  1. 使用 ESM(import/export 语法,不是 require
  2. Webpack 的 mode: 'production'optimization.usedExports: true
  3. package.json 配置 sideEffects: false(或列出有副作用的文件)
  4. Babel 不把 ESM 转成 CJS(modules: false
  5. 依赖库提供 ESM 版本(lodash-es 而不是 lodash

Q5:HMR 和 live reload 的区别?

live reload(全量刷新): 文件改变 → 整页刷新 → 应用状态丢失
HMR(模块热替换): 文件改变 → 只更新修改的模块 → 保留组件状态

HMR 需要:

  • 开发服务器支持(WebSocket 推送)
  • 框架集成(React Fast Refresh / Vue HMR)
  • module.hot.accept 注册更新回调

📝 学习总结: 打包工具的核心设计哲学是"在开发体验和生产优化之间找到最佳平衡"。Webpack 选择了灵活性(插件生态)、Vite 选择了开发体验(No-bundle)、Rollup 选择了纯净输出(库打包)、esbuild 选择了极速(工具链内核)。理解各自的设计权衡,才能在实际项目中做出正确选择。

相关推荐
libokaifa2 小时前
OpenSpec + TDD:让 AI 写代码,用测试兜底
前端·ai编程
用户15815963743702 小时前
搭 AI Agent 团队踩了 18 个坑,总结出这 5 个关键步骤
前端
Kellen2 小时前
Fumadocs 基础概念:从内容源到页面渲染
前端
Lee川2 小时前
前端进阶之路:从性能优化到响应式布局的实战指南(Tailwindcss)
前端·面试
努力干饭中2 小时前
Git Rebase 最佳实践
前端·git
Ferries2 小时前
《从前端到 Agent》系列|02:应用层-提示词工程 (Prompt Engineering)
前端·人工智能·深度学习
Awu12272 小时前
⚡Pretext: 无 DOM 布局回流的快速文本测量库
前端
前端Hardy2 小时前
别再手写代码了!2026 前端 5 个 AI 杀招,直接解放 80% 重复劳动(附工具+步骤)
前端·javascript·面试
SuperEugene2 小时前
Element Plus/VXE-Table UI 组件库规范:统一用法实战,避开样式冲突与维护混乱|工程化与协作规范篇
前端·javascript·vue.js·ui·前端框架·element plus·vxetable