Webpack 详细教程:从入门到精通

目录

  1. 什么是 Webpack?
  2. 为什么需要 Webpack?
  3. 核心概念
  4. 第一个 Webpack 项目:零配置
  5. 手动配置 Webpack (核心步骤)
    • 1. 项目初始化
    • 2. 安装 Webpack
    • 3. 创建 src 目录和入口文件
    • 4. 配置 webpack.config.js
    • 5. 配置打包脚本
    • 6. 运行打包
  6. 处理样式文件
    • 1. 加载 CSS
    • 2. 提取 CSS 到单独文件
  7. 处理图片和其他文件
  8. 使用 Babel 转换 JavaScript
  9. 使用 Source Maps
  10. 开发服务器
  11. 高级优化
    • 1. 生产环境与开发环境分离
    • 2. 代码分割
    • 3. 懒加载
    • 4. Tree Shaking (摇树优化)
    • 5. 缓存
  12. 总结与最佳实践

什么是 Webpack?

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器

webpack详细教程
(图片来源网络,侵删)

当你告诉 Webpack 如何转换你的文件时,它会构建一个依赖图,该图映射了项目所需的每个模块,并生成一个或多个bundle

Webpack 就像一个超级智能的打包工具,它能:

  • 理解你的项目结构。
  • 识别不同类型的文件(JS, CSS, 图片, 字体等)。
  • 转换这些文件,使其能在浏览器中运行(将 ES6+ 转换成 ES5,将 Sass 转换成 CSS)。
  • 合并所有转换后的文件,打包成少量(甚至一个)优化的文件,以提高网页加载速度。

为什么需要 Webpack?

在没有构建工具的时代,我们需要手动管理:

  • 依赖关系<script> 标签的顺序很重要,必须先加载依赖库,再加载使用它的代码。
  • 兼容性:浏览器不能直接运行所有现代 JavaScript 特性(如 ES6, ES7)。
  • 资源管理:CSS、图片等资源需要通过特定的方式引入到 HTML 中。

Webpack 解决了这些问题:

webpack详细教程
(图片来源网络,侵删)
  • 模块化:让你可以在任何类型的文件中使用 import/require 语法来引入模块,而不仅仅是 JavaScript。
  • 自动化:通过 Loader 和 Plugin,自动处理代码转换、压缩、优化等繁琐工作。
  • 性能优化:提供代码分割、懒加载、缓存等策略,优化应用加载性能。

核心概念

在配置 Webpack 之前,必须理解以下几个核心概念:

  1. Entry (入口)

    • 指示 Webpack 应该使用哪个模块作为其构建内部依赖图的开始。
    • Webpack 会从这个入口文件开始,递归地找出所有依赖的模块。
  2. Output (出口)

    • 告诉 Webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。
    • 默认情况下,主输出文件会放在 dist/main.js 路径下。
  3. Loader (加载器)

    webpack详细教程
    (图片来源网络,侵删)
    • Webpack 只能理解 JavaScript 和 JSON 文件,Loader 让 Webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用。
    • 规则:在 webpack.config.js 中,module.rules 数组定义了一组规则,每个规则都包含一个 test 属性(用于匹配文件)和一个 use 属性(指定要使用的 loader)。
  4. Plugin (插件)

    • Loader 用于转换特定类型的文件,而 Plugin 则可以用于执行更广泛的任务,如打包优化、资源管理、环境变量注入等。
    • 作用:通过在 webpack.config.jsplugins 数组中实例化并添加插件来使用它们。
  5. Mode (模式)

    • 通过选择 development, productionnone 之中的一个,来设置 mode 参数,你可以启用 Webpack 内置在相应环境下的优化。
    • production 模式会自动压缩代码、优化等。
    • development 模式会提供更快的构建速度和更详细的调试信息。

第一个 Webpack 项目:零配置

从 Webpack 5 开始,它提供了一个零配置的体验,我们先用这个快速感受一下。

  1. 初始化项目

    mkdir webpack-demo
    cd webpack-demo
    npm init -y
  2. 安装 Webpack

    npm install webpack webpack-cli --save-dev
  3. 创建项目结构

    webpack-demo/
    ├── dist/
    ├── src/
    │   └── index.js
    └── package.json
  4. 编写入口文件 src/index.js

    function component() {
      const element = document.createElement('div');
      const btn = document.createElement('button');
      btn.innerHTML = 'Click me and check the console!';
      element.appendChild(btn);
      btn.onclick = () => import(/* webpackChunkName: "print" */ './print.js').then(module => {
        const print = module.default;
        print();
      });
      return element;
    }
    document.body.appendChild(component());
  5. 创建被异步加载的文件 src/print.js

    export default function printMe() {
      console.log('I get called from print.js!');
    }
  6. 运行打包package.json 中添加脚本:

    "scripts": {
      "build": "webpack"
    }

    然后运行:

    npm run build

    你会看到 dist 目录下生成了 main.js 文件,Webpack 自动配置了入口为 src/index.js,并处理了动态导入。

手动配置 Webpack (核心步骤)

零配置虽然方便,但实际项目中我们通常需要手动配置来满足特定需求。

1. 项目初始化

mkdir my-webpack-project
cd my-webpack-project
npm init -y

2. 安装 Webpack

npm install webpack webpack-cli --save-dev

3. 创建 src 目录和入口文件

my-webpack-project/
├── dist/        # 存放打包后的文件
├── src/
│   ├── index.js # 入口文件
│   └── style.css # 样式文件
├── index.html   # HTML 模板
└── package.json

src/index.js:

import _ from 'lodash';
import './style.css';
function createDomElement() {
  const element = document.createElement('div');
  element.innerHTML = _.join(['Hello', 'Webpack', 'with', 'Style!'], ' ');
  return element;
}
document.body.appendChild(createDomElement());

src/style.css:

body {
  background-color: #f0f0f0;
  font-family: Arial, sans-serif;
}
div {
  color: #333;
  font-size: 24px;
  text-align: center;
  padding: 20px;
}

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">Webpack Demo</title>
</head>
<body>
  <!-- 引用打包后的 JS 文件 -->
  <script src="dist/main.js"></script>
</body>
</html>

4. 配置 webpack.config.js

在项目根目录下创建 webpack.config.js 文件。

const path = require('path');
module.exports = {
  // 1. 入口
  entry: './src/index.js',
  // 2. 出口
  output: {
    // [name] 是一个占位符,会根据 entry 的 key 生成
    filename: '[name].bundle.js',
    // path 必须是绝对路径
    path: path.resolve(__dirname, 'dist'),
    // 每次构建前清空 dist 目录
    clean: true,
  },
  // 3. 模块规则
  module: {
    rules: [
      {
        test: /\.css$/i, // 匹配 .css 文件
        use: ['style-loader', 'css-loader'], // 从右到左使用 loader
      },
    ],
  },
  // 4. 插件
  plugins: [],
  // 5. 模式
  mode: 'development', // 可以是 'development' 或 'production'
};

解释

  • path: Node.js 内置模块,用于处理文件路径。
  • rules: 我们添加了一条规则,当遇到 .css 文件时,先使用 css-loader 解析 CSS 文件,然后使用 style-loader 将解析后的 CSS 内容通过 <style> 标签注入到 HTML 中。
  • 安装 Loaders: 我们需要安装这两个 loader。
    npm install --save-dev style-loader css-loader

5. 配置打包脚本

修改 package.jsonscripts 部分:

"scripts": {
  "build": "webpack --config webpack.config.js"
}

--config 参数指定了配置文件的路径,如果你的配置文件名就是 webpack.config.js,可以省略此参数。

6. 运行打包

npm run build

dist 目录下会生成 main.bundle.js,在浏览器中打开 index.html,你应该能看到应用运行了,并且样式也被正确应用。

处理样式文件

1. 加载 CSS

我们已经在上一步中使用了 style-loadercss-loader,它们的作用是:

  • css-loader: 使你能够使用 importrequire 导入 CSS 文件。
  • style-loader: 将 CSS 插入到 DOM 的 <style> 标签中。

2. 提取 CSS 到单独文件

在生产环境中,我们通常希望将 CSS 提取到一个单独的文件中,而不是内联到 HTML 中,这样可以利用浏览器缓存,这时我们需要使用 mini-css-extract-plugin

  1. 安装插件和 loader

    npm install --save-dev mini-css-extract-plugin
  2. 修改 webpack.config.js

    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    module.exports = {
      // ... entry, output ...
      module: {
        rules: [
          {
            test: /\.css$/i,
            // 生产环境使用 MiniCssExtractPlugin.loader,开发环境使用 style-loader
            use: [process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader'],
          },
        ],
      },
      plugins: [
        new MiniCssExtractPlugin({
          // 提取出的 CSS 文件名
          filename: '[name].css',
        }),
      ],
      // ... mode ...
    };
  3. 修改 index.html 需要手动引入提取出的 CSS 文件。

    <link rel="stylesheet" href="dist/main.css">
    <script src="dist/main.bundle.js"></script>
  4. 运行打包 运行 npm run build,你会看到 dist 目录下同时生成了 main.bundle.jsmain.css

处理图片和其他文件

为了处理图片、字体等文件,我们需要 file-loaderurl-loader(Webpack 5 推荐使用内置的 Asset Modules)。

使用 Asset Modules (Webpack 5 推荐)

  1. 修改 webpack.config.js

    module.exports = {
      // ...
      module: {
        rules: [
          // ... css rule ...
          {
            test: /\.(png|svg|jpg|jpeg|gif)$/i,
            type: 'asset/resource', // 生成单独的文件,并导出其 URL
          },
          {
            test: /\.(woff|woff2|eot|ttf|otf)$/i,
            type: 'asset/resource',
          },
        ],
      },
      // ...
    };
  2. 在 CSS 中使用图片

    body {
      background-image: url('./assets/background.png');
      background-size: cover;
    }
  3. 创建 src/assets 目录并放入一张 background.png 图片。

  4. 运行打包 Webpack 会自动将图片复制到 dist 目录,并在 CSS 中更新其引用路径。

使用 Babel 转换 JavaScript

为了让你的现代 JavaScript 代码能在旧浏览器上运行,你需要使用 Babel。

  1. 安装 Babel 相关依赖

    npm install --save-dev babel-loader @babel/core @babel/preset-env
    • babel-loader: Webpack 和 Babel 的桥梁。
    • @babel/core: Babel 核心库。
    • @babel/preset-env: 一个智能预设,根据你目标环境,决定转换哪些 JavaScript 特性。
  2. 创建 Babel 配置文件 在项目根目录创建 .babelrc 文件:

    {
      "presets": ["@babel/preset-env"]
    }
  3. 修改 webpack.config.js

    module.exports = {
      // ...
      module: {
        rules: [
          // ... css and image rules ...
          {
            test: /\.m?js$/, // 匹配 .js 和 .mjs 文件
            exclude: /node_modules/, // 排除 node_modules 目录
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
          }
        ]
      }
      // ...
    };
  4. 编写现代 JS 代码src/index.js 中使用一些 ES6+ 语法,如箭头函数、let/const 等,然后运行打包,Babel 会将其转换为 ES5。

使用 Source Maps

当打包后的代码出错时,Source Maps 能帮助你在源代码中定位错误,而不是在压缩后的代码中。

webpack.config.js 中配置 devtool

module.exports = {
  // ...
  devtool: 'inline-source-map', // 开发环境推荐
  // 生产环境推荐 'source-map'
  // ...
};
  • inline-source-map: Source Map 作为 Data URI 嵌入在 bundle 中。
  • source-map: 生成一个单独的 .map 文件,性能更好,适合生产环境。

开发服务器

每次修改代码后都手动运行 npm run build 非常繁琐。webpack-dev-server 可以启动一个简单的 Web 服务器,并提供实时重载功能。

  1. 安装

    npm install --save-dev webpack-dev-server
  2. 修改 webpack.config.js 指定静态文件的根目录。

    module.exports = {
      // ...
      devServer: {
        static: './dist', // 静态文件服务的目录
      },
      // ...
    };
  3. 修改 package.json 脚本

    "scripts": {
      "build": "webpack",
      "start": "webpack serve --open"
    }

    --open 选项会自动在浏览器中打开页面。

  4. 启动开发服务器

    npm start

    现在当你修改代码并保存时,浏览器会自动刷新。

高级优化

1. 生产环境与开发环境分离

最佳实践是为开发和生产环境创建不同的配置文件。

  1. 创建配置文件

    • webpack.common.js (公共配置)
    • webpack.dev.js (开发配置)
    • webpack.prod.js (生产配置)
  2. 安装 webpack-merge

    npm install --save-dev webpack-merge
  3. 编写配置文件

    • webpack.common.js:

      const path = require('path');
      const MiniCssExtractPlugin = require('mini-css-extract-plugin');
      module.exports = {
        entry: './src/index.js',
        output: {
          filename: '[name].bundle.js',
          path: path.resolve(__dirname, 'dist'),
          clean: true,
        },
        module: {
          rules: [
            {
              test: /\.css$/i,
              use: [MiniCssExtractPlugin.loader, 'css-loader'],
            },
            // ... other rules ...
          ],
        },
        plugins: [
          new MiniCssExtractPlugin({
            filename: '[name].css',
          }),
        ],
      };
    • webpack.dev.js:

      const { merge } = require('webpack-merge');
      const common = require('./webpack.common.js');
      module.exports = merge(common, {
        mode: 'development',
        devtool: 'inline-source-map',
        devServer: {
          static: './dist',
        },
      });
    • webpack.prod.js:

      const { merge } = require('webpack-merge');
      const common = require('./webpack.common.js');
      module.exports = merge(common, {
        mode: 'production',
        devtool: 'source-map',
        // 生产环境可以添加更多插件,如 TerserPlugin (Webpack 已内置)
      });
  4. 修改 package.json 脚本

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

2. 代码分割

代码分割允许你将代码拆分成多个 bundle,然后可以按需加载或并行加载,这可以通过以下几种方式实现:

  • 入口起点: 使用 entry 配置手动地分离代码。
  • 防止重复: 使用 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入: 通过内联函数调用的方式分离代码。

动态导入 是最推荐的方式,因为它能实现按需加载。

我们已经在前面的 src/index.js 中使用了动态导入:

btn.onclick = () => import(/* webpackChunkName: "print" */ './print.js').then(module => {
  const print = module.default;
  print();
});

/* webpackChunkName: "print" */ 是一个魔法注释,告诉 Webpack 将这个 chunk 命名为 print.js,运行 npm run build,你会看到 dist 目录下除了 main 相关文件,还有一个 print.[hash].js 文件。

3. 懒加载

代码分割是实现懒加载的基础,当用户需要时(如点击按钮、滚动到某个位置)才加载对应的代码块,从而减小初始包的大小。

上面的动态导入示例就是懒加载的典型应用。

4. Tree Shaking (摇树优化)

Tree Shaking 指的是移除 JavaScript 上下文中的未引用代码(Dead Code),它依赖于 ES2025 模块语法的静态结构特性(importexport)。

如何启用:

  1. 使用 ES6 模块语法 (import/export)。
  2. package.json 中设置 "sideEffects": false,或者明确指定哪些文件有副作用。
  3. 确保 modeproduction

Webpack 在生产模式下会自动开启 Tree Shaking。

5. 缓存

为了利用浏览器缓存,我们需要确保文件名在内容不变时保持不变,内容变化时文件名变化。

Webpack 的 [contenthash] 占位符可以完美解决这个问题。

修改 webpack.common.js 中的 output.filename:

output: {
  filename: '[name].[contenthash].bundle.js', // 使用 contenthash
  // ...
},

只有当文件内容发生变化时,[contenthash] 才会改变。

总结与最佳实践

  • 从零开始: 先理解 Entry, Output, Loader, Plugin 这四大核心概念。
  • 分步配置: 不要试图一次性配置所有功能,先配置 JS 打包,然后是 CSS,接着是图片和字体,最后是优化。
  • 环境分离: 始终为开发和生产环境创建不同的配置文件,使用 webpack-merge 合并公共配置。
  • 利用工具: Webpack 5 的零配置、Asset Modules 等新特性极大地简化了配置。
  • 持续学习: Webpack 生态非常庞大,还有许多高级主题如 PWA、环境变量注入等,可以在需要时再深入学习。

这份教程涵盖了 Webpack 的绝大部分核心功能和最佳实践,掌握了这些,你就可以应对绝大多数前端项目的构建需求了,祝你学习愉快!