Dark Dwarf Blog background

Webpack

Webpack

1. 引入

a.a. 打包工具

随着项目越来越复杂,我们会使用 ES6 模块将代码拆分到多个文件中,并引入许多第三方库。这会导致浏览器需要发起大量的 HTTP 请求来加载这些零散的文件,严重影响页面性能。

打包工具 (Bundler) 正是为了解决这个问题而生。它是一个构建工具,能够将我们项目中所有散落的 JavaScript 模块、CSS 文件、图片等资源,根据依赖关系进行分析、处理和压缩,最终“打包”成少数几个优化过的、适合浏览器加载的静态文件。

b.b. Webpack 的核心工作

Webpack 是目前最流行的 JavaScript 打包工具之一。它的核心工作流程如下:

  1. 从一个或多个入口文件 (Entry) 开始。
  2. 根据 importrequire 语句,构建一个依赖图 (Dependency Graph),这个图包含了项目所需的所有模块。
  3. 将图中的所有模块(包括 JavaScript、CSS、图片等)通过 加载器 (Loaders)插件 (Plugins) 进行处理。
  4. 最终将处理后的内容打包成一个或多个静态文件,输出到指定的出口 (Output) 目录。

可以把 Webpack 想象成一位智能的“项目打包员”。我们告诉他主文件在哪里(Entry),他会顺藤摸瓜,找到所有相关的文件和资源(构建依赖图),然后用各种工具(Loaders 和 Plugins)对这些材料进行加工和分类,最后把它们整齐地打包成几个可以直接发货的包裹(Output)。

2. Webpack 相关概念

a.a. 核心概念

Webpack 中有如下核心概念:

  1. 入口 (Entry):Webpack 开始构建依赖图的起点。它告诉 Webpack 应该从哪个文件开始打包。
  2. 出口 (Output):告诉 Webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。
  3. 加载器 (Loaders):让 Webpack 能够去处理那些非 JavaScript 文件(Webpack 自身只能理解 JavaScript 和 JSON)。Loader 可以将所有类型的文件转换为 Webpack 能处理的有效模块。
  4. 插件 (Plugins):用于执行范围更广的任务,从打包优化和压缩,一直到重新定义环境中的变量。插件是 Webpack 的支柱功能。

b.b. 目录结构约定

Webpack 约定了如下的目录结构:

  • src (source):存放我们编写的源代码(.js, .css, .html 模板等)。我们所有的开发工作都在这里进行。
  • dist (distribution):存放打包后生成的、用于部署的文件。这个目录是自动生成的,我们不应该手动修改它。

3. 基本配置与使用

a.a. 初始化项目

  1. 创建项目目录并初始化 npm
mkdir webpack-demo && cd webpack-demo
npm init -y
  1. 安装 Webpack 相关的包作为开发依赖 (-D--save-dev):
npm install webpack webpack-cli -D

b.b. 创建 webpack.config.js

在项目根目录下创建 webpack.config.js 文件。这是 Webpack 的主配置文件,它使用 Node.js 的 CommonJS 模块语法。

// webpack.config.js
const path = require('path'); // 引入 Node.js 的 path 模块

module.exports = {
  // 1. 模式 (Mode)
  mode: 'development', // 'development' 或 'production'

  // 2. 入口 (Entry)
  entry: './src/index.js',

  // 3. 出口 (Output)
  output: {
    filename: 'main.js', // 打包后输出的文件名
    path: path.resolve(__dirname, 'dist'), // 输出目录的绝对路径
    clean: true, // 在生成文件之前清空 output 目录
  },
};

c.c. 执行打包

在终端中运行以下命令,Webpack 会读取配置文件并执行打包:

npx webpack

npx 是一个 npm 工具,它能运行 node_modules/.bin 目录下的可执行文件。命令执行后即可在 dist 目录下看到生成的 main.js 文件。

4. 处理非 JavaScript 资源

a.a. 处理 HTML

我们需要 html-webpack-plugin 来根据模板自动生成 HTML 文件,并注入打包后的 JS。

  1. 安装插件:
npm install html-webpack-plugin -D
  1. 配置 webpack.config.js
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 引入插件

module.exports = {
  // ...其他配置
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/template.html', // 指定 HTML 模板文件
    }),
  ],
};

b.b. 加载 CSS

Webpack 需要 css-loaderstyle-loader 来处理 CSS 文件。

  • css-loader:解析 CSS 文件中的 @importurl()
  • style-loader:将 css-loader 解析后的内容,通过 <style> 标签注入到 DOM 中。
  1. 安装 Loaders:
npm install style-loader css-loader -D
  1. 配置 webpack.config.js
module.exports = {
  // ...其他配置
  module: {
    rules: [
      {
        test: /\.css$/i, // 匹配所有 .css 文件
        use: ['style-loader', 'css-loader'], // 使用这两个 loader
      },
    ],
  },
};

注意use 数组中 Loader 的执行顺序是从右到左。所以必须先用 css-loader 解析,再用 style-loader 注入。

c.c. 加载图片等资源

Webpack 5 内置了资源模块 (Asset Modules),可以轻松处理图片、字体等资源。

配置 webpack.config.js

module.exports = {
  // ...其他配置
  module: {
    rules: [
      // ...其他 rules
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i, // 匹配图片文件
        type: 'asset/resource', // 使用资源模块类型
      },
    ],
  },
};

配置完成后,你就可以在 JavaScript 中直接 import 图片了:

import OdinImage from './odin.png';

const imgElement = document.createElement('img');
imgElement.src = OdinImage; // Webpack 会返回图片打包后的最终 URL
document.body.appendChild(imgElement);

5. 其他的 Webpack 组件

a.a. webpack-dev-server

webpack-dev-server 是一个开发服务器,提供实时重新加载 (Live Reloading) 功能,这样修改代码后无需手动运行 npx webpack

  1. 安装:
npm install webpack-dev-server -D
  1. 配置 webpack.config.js
module.exports = {
  // ...其他配置
  devServer: {
    static: './dist', // 告诉服务器从哪个目录提供内容
    watchFiles: ['./src/template.html'], // 监听 HTML 模板文件的变化
  },
};
  1. 启动服务器:
npx webpack serve --open

b.b. Source Maps

打包后的代码经过压缩和合并,变得难以阅读。如果出错,浏览器提示的错误位置将是打包后文件的位置。Source Map 是一个映射文件,能将打包后的代码映射回原始源代码,极大地简化了调试过程。

webpack.config.js 中添加一行配置即可:

module.exports = {
  // ...其他配置
  devtool: 'eval-source-map', // 一个适合开发模式的 source map 类型
};

6. 优化工作流与生产环境配置

a.a. 使用 npm 脚本 (npm scripts)

每次都输入 npx webpacknpx webpack serve 这样的命令有些繁琐。我们可以使用 package.json 中的 scripts 字段来创建自定义命令,简化工作流。

package.json 中添加 scripts 对象:

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "dev": "webpack serve --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.70.0",
    "webpack-cli": "^4.9.2"
  }
}

现在我们就可以使用更简洁、更具语义的命令了:

  • npm run build:等同于 npx webpack,用于构建生产版本。
  • npm run dev:等同于 npx webpack serve --open,用于启动开发服务器。

注意:在 scripts 中,我们可以直接写 webpack 而不是 npx webpack。这是因为 npm 脚本在执行时,会自动将 node_modules/.bin 目录添加到系统的 PATH 中。

b.b. Webpack 模式

Webpack 有两种主要的模式:

  1. development (开发模式)

    • 目标:关注强大的源码调试功能和快速的增量构建。
    • 特点:不压缩代码,包含详细的错误信息和提示,构建速度快。
    • 我们在 devtool: 'eval-source-map' 中生成的 Source Map 就是在此模式下工作的。
  2. production (生产模式)

    • 目标:关注打包后文件的体积最小化和运行性能最优化。
    • 特点:自动启用代码压缩(Minification)、作用域提升(Scope Hoisting)、移除无用代码(Tree Shaking)等一系列优化。打包后的代码可读性差,但体积小、运行快。

我们不应该在部署上线时使用开发模式,因为它会暴露源码,且文件体积过大。

c.c. 拆分配置文件

每次在开发和部署之间切换时,手动修改 webpack.config.js 中的 mode 是非常低效且容易出错的。专业的做法是为不同的环境创建不同的配置文件。

我们需要安装一个名为 webpack-merge 的工具来优雅地合并这些配置。

  1. 安装 webpack-merge
npm install webpack-merge -D
  1. 创建三个配置文件:
    • webpack.common.js:存放开发和生产环境共享的配置(如 entry, output, loaders)。
    • webpack.dev.js:存放仅开发环境使用的配置(如 devServer, devtool)。
    • webpack.prod.js:存放仅生产环境使用的配置(如性能优化、代码压缩等)。
  • webpack.common.js (通用配置)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/template.html',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
    ],
  },
};
  • webpack.dev.js (开发配置)
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-source-map',
  devServer: {
    static: './dist',
    watchFiles: ['./src/template.html'],
  },
});
  • webpack.prod.js (生产配置)
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'production',
    devtool: 'source-map', // 生产环境推荐使用 source-map
  });
  1. 更新 package.json 中的脚本:使用 --config 标志来指定要使用的配置文件。
"scripts": {
  "build": "webpack --config webpack.prod.js",
  "dev": "webpack serve --open --config webpack.dev.js"
}

现在,运行 npm run dev 会使用开发配置启动服务器,而 npm run build 会使用生产配置来打包最终文件。这套设置一劳永逸,极大地提升了开发效率和规范性。

d.d. 使用模板仓库

每次新建项目都要重复一遍 Webpack 的配置过程是很繁琐的。为了解决这个问题,我们可以利用 GitHub 的模板仓库功能。我们可以创建一个包含所有常用 Webpack 配置、目录结构、linter 设置等内容的样板项目。然后,在该仓库的设置页面,勾选 “Template repository” 选项。这样,当我们新建一个仓库时,就可以从 “Repository template” 下拉菜单中选择模板。这样,新创建的仓库就会直接包含所有预设的配置,从而我们可以跳过繁琐的搭建步骤,直接开始项目开发。