删库在逃程序员的Blog

Webpack 开发模式完全指南 —— 提升 10 倍开发效率

author
·
6
0

Webpack 开发模式完全指南 —— 提升 10 倍开发效率

系列文章第 2 篇 | 预计阅读时间:20 分钟


一、开发模式核心目标

开发模式的核心目标只有两个:

  1. 快速编译 — 修改代码后立即看到效果
  2. 精准调试 — 错误提示准确指向源代码位置

二、Source Map —— 精准定位错误

2.1 为什么需要 Source Map?

Webpack 打包后的代码是完全不同的形式:

// 打包后的代码(根本看不懂)
/******/ (() => {
/******/   var __webpack_modules__ = ({
/******/     "./node_modules/css-loader/dist/cjs.js!./src/less/index.less":
/******/     ((module, __webpack_exports__, __webpack_require__) => {
/******/       eval("__webpack_require__.r(__webpack_exports__);\n...");
/******/     })
/******/   });
/******/ })();

问题: 一旦代码出错,错误提示的行号和列号对应的是打包后的代码,根本无法定位到源代码。

2.2 Source Map 工作原理

Source Map 是一个 .map 文件,包含源代码与构建后代码的行列映射关系

源代码 (src/main.js)     打包后代码 (dist/main.js)
第 1 行 第 1 列  ────────→  第 10 行 第 5 列
第 2 行 第 5 列  ────────→  第 15 行 第 20 列
...

当浏览器运行出错时,通过 .map 文件反向映射,提示源代码的错误位置

2.3 配置方案

开发模式推荐配置:

module.exports = {
  mode: "development",
  devtool: "cheap-module-source-map",
};

为什么选这个?

配置值 优点 缺点 适用场景
cheap-module-source-map 打包快,只包含行映射 没有列映射 开发模式(推荐)
source-map 包含完整的行/列映射 打包速度慢 生产模式
eval 最快 没有映射信息 不推荐
false 不生成 map 文件 无法调试 生产模式(可选)

生产模式配置(可选):

module.exports = {
  mode: "production",
  devtool: "source-map",  // 或者 false 不生成
};

三、HMR 热模块替换 —— 无需刷新页面

3.1 为什么需要 HMR?

传统开发流程:

  1. 修改代码
  2. 保存文件
  3. Webpack 重新打包所有模块
  4. 手动刷新浏览器
  5. 等待页面加载

HMR 开发流程:

  1. 修改代码
  2. 保存文件
  3. Webpack 只重新打包修改的模块
  4. 自动推送到浏览器(无需刷新)
  5. 立即看到效果

效率提升: 从 5-10 秒缩短到 0.5 秒!

3.2 启用 HMR

webpack.config.js 配置:

module.exports = {
  mode: "development",
  devServer: {
    host: "localhost",
    port: 3000,
    open: true,      // 自动打开浏览器
    hot: true,       // 开启 HMR
  },
  devtool: "cheap-module-source-map",
};

启动开发服务器:

npx webpack serve --config webpack.config.js

3.3 CSS 的 HMR

经过 style-loader 处理的 CSS 天然支持 HMR,无需额外配置。

修改样式后,页面不会刷新,样式自动更新。

3.4 JS 的 HMR

JS 的 HMR 需要手动处理模块热替换:

// main.js
import count from "./js/count";

const result = count(2, 1);
console.log(result);

// 启用 HMR
if (module.hot) {
  module.hot.accept("./js/count.js", () => {
    // count.js 更新后重新执行
    const newResult = count(2, 1);
    console.log("count 模块更新了:", newResult);
  });
}

实际开发中: 使用框架(Vue/React)时,框架已内置 HMR 支持,无需手动配置。


四、OneOf —— 提升打包速度

4.1 为什么需要 OneOf?

默认行为: 每个文件都会经过所有 loadertest 匹配,即使最终不处理。

main.js → test(/.css/) ❌ → test(/.less/) ❌ → test(/.js/) ✅
index.css → test(/.css/) ✅ → test(/.less/) ❌ → test(/.js/) ❌

问题: 每个文件都要过一遍所有规则,效率低。

4.2 OneOf 解决方案

OneOf 规则: 一旦匹配上一个 loader,就不再匹配其他规则。

module.exports = {
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.css$/,
            use: ["style-loader", "css-loader"],
          },
          {
            test: /\.less$/,
            use: ["style-loader", "css-loader", "less-loader"],
          },
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader",
          },
          // ... 其他规则
        ],
      },
    ],
  },
};

效果: 每个文件最多只经过一个 loader 处理,打包速度提升 30%-50%。


五、Include / Exclude —— 排除不需要处理的文件

5.1 为什么要排除?

node_modules 中的第三方库已经编译过,不需要再次处理。

不排除的后果:

  • Babel 编译几万个第三方文件,速度极慢
  • ESLint 检查第三方代码,报一堆无关错误

5.2 配置方式

方式一:exclude(排除)

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,  // 排除 node_modules
        loader: "babel-loader",
      },
    ],
  },
};

方式二:include(只包含)

const path = require("path");

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, "src"),  // 只处理 src 目录
        loader: "babel-loader",
      },
    ],
  },
};

推荐: 使用 include 更精确,性能更好。


六、Cache —— 缓存编译结果

6.1 为什么需要缓存?

场景: 项目有 100 个 JS 文件,只修改了 1 个。

无缓存: 100 个文件全部重新编译(ESLint 检查 + Babel 编译)

有缓存: 99 个未修改的文件直接使用缓存,只编译 1 个文件

速度提升: 第二次打包快 5-10 倍!

6.2 Babel 缓存配置

// babel.config.js
module.exports = {
  presets: ["@babel/preset-env"],
};

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, "src"),
        use: [
          {
            loader: "babel-loader",
            options: {
              cacheDirectory: true,      // 开启缓存
              cacheCompression: false,   // 不压缩缓存文件(更快)
            },
          },
        ],
      },
    ],
  },
};

缓存位置: node_modules/.cache/babel-loader/

6.3 ESLint 缓存配置

const ESLintWebpackPlugin = require("eslint-webpack-plugin");

module.exports = {
  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "src"),
      cache: true,  // 开启缓存
      cacheLocation: path.resolve(
        __dirname,
        "node_modules/.cache/.eslintcache"
      ),
    }),
  ],
};

缓存位置: node_modules/.cache/.eslintcache


七、多进程打包 —— 榨干 CPU 性能

7.1 为什么需要多进程?

单进程打包: 一个 CPU 核心处理所有文件,其他核心空闲

多进程打包: 多个 CPU 核心同时处理,充分利用性能

适用场景: 大型项目(100+ 文件),小型项目不建议(进程启动有开销)

7.2 获取 CPU 核心数

const os = require("os");
const threads = os.cpus().length;  // 获取 CPU 核心数
console.log(`CPU 核心数:${threads}`);

7.3 安装依赖

npm i thread-loader -D

7.4 配置多进程

const os = require("os");
const threads = os.cpus().length;

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, "src"),
        use: [
          {
            loader: "thread-loader",  // 放在最前面
            options: {
              workers: threads,  // 进程数量
            },
          },
          {
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "src"),
      cache: true,
      threads,  // ESLint 也开启多进程
    }),
  ],
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: threads,  // 压缩也开启多进程
      }),
    ],
  },
};

⚠️ 注意事项:

  1. 进程启动开销: 每个进程启动约 600ms,小项目反而更慢
  2. 适用场景: 100+ 文件的大型项目
  3. 进程数量: 一般等于 CPU 核心数,不要设置过多

八、开发服务器完整配置

8.1 完整示例

const os = require("os");
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const threads = os.cpus().length;

module.exports = {
  entry: "./src/main.js",
  output: {
    path: undefined,  // 开发模式不需要输出
    filename: "static/js/main.js",
  },
  module: {
    rules: [
      {
        oneOf: [  // OneOf 优化
          {
            test: /\.css$/,
            use: ["style-loader", "css-loader"],
          },
          {
            test: /\.less$/,
            use: ["style-loader", "css-loader", "less-loader"],
          },
          {
            test: /\.s[ac]ss$/,
            use: ["style-loader", "css-loader", "sass-loader"],
          },
          {
            test: /\.(png|jpe?g|gif|webp)$/,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024,  // 小于 10kb 转 base64
              },
            },
          },
          {
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
            generator: {
              filename: "static/media/[hash:8][ext][query]",
            },
          },
          {
            test: /\.js$/,
            include: path.resolve(__dirname, "src"),
            use: [
              {
                loader: "thread-loader",
                options: {
                  workers: threads,
                },
              },
              {
                loader: "babel-loader",
                options: {
                  cacheDirectory: true,
                  cacheCompression: false,
                },
              },
            ],
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "src"),
      exclude: /node_modules/,
      cache: true,
      cacheLocation: path.resolve(
        __dirname,
        "node_modules/.cache/.eslintcache"
      ),
      threads,
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
  ],
  devServer: {
    host: "localhost",
    port: 3000,
    open: true,
    hot: true,
    client: {
      logging: "verbose",  // 显示详细日志
      overlay: {
        errors: true,      // 显示错误遮罩
        warnings: false,   // 不显示警告遮罩
      },
    },
  },
  mode: "development",
  devtool: "cheap-module-source-map",
};

8.2 启动脚本

package.json 配置:

{
  "scripts": {
    "start": "npm run dev",
    "dev": "npx webpack serve --config webpack.config.js",
    "build": "npx webpack --config webpack.prod.js"
  }
}

启动开发服务器:

npm run dev

九、小结

本篇我们学习了开发模式的完整优化方案:

优化项 作用 速度提升
Source Map 精准定位错误 -
HMR 无需刷新页面 5-10 秒 → 0.5 秒
OneOf 减少 loader 匹配 30%-50%
Include/Exclude 排除第三方代码 50%+
Cache 缓存编译结果 5-10 倍(第二次)
多进程 充分利用 CPU 大型项目 2-3 倍

最佳实践总结:

  1. ✅ 开发模式使用 cheap-module-source-map
  2. ✅ 开启 HMR 热更新
  3. ✅ 使用 oneOf 优化 loader 匹配
  4. ✅ 用 include 限定处理范围
  5. ✅ 开启 Babel 和 ESLint 缓存
  6. ✅ 大型项目开启多进程

下一篇预告: 《生产模式与性能优化》—— 深入讲解代码分割、Tree Shaking、网络缓存、PWA 等生产环境优化技术,让你的应用加载速度提升 80%!


系列导航:

  • 📌 第 1 篇:Webpack 入门与基础
  • 📌 第 2 篇:开发模式完全指南(本文)
  • ⏭️ 第 3 篇:生产模式与性能优化
  • ⏭️ 第 4 篇:资源处理大全
  • ⏭️ 第 5 篇:高级优化实战

本文基于 Webpack 5 编写,如有问题欢迎留言讨论。

评论 (0)