webpack 基础

一、webpack 介绍

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

依赖图(dependency graph)

每当一个文件依赖另一个文件时,webpack 都会将文件视为直接存在 依赖关系。

这使得 webpack 可以获取非代码资源,如 images 或 web 字体等。并会把它们作为 依赖 提供给应用程序。

当 webpack 处理应用程序时,它会根据命令行参数中或配置文件中定义的模块列表开始处理。

从 入口 开始,webpack 会递归的构建一个 依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为少量的 bundle —— 通常只有一个 —— 可由浏览器加载。

对于 HTTP/1.1 的应用程序来说,由 webpack 构建的 bundle 非常强大。当浏览器发起请求时,它能最大程度的减少应用的等待时间。而对于 HTTP/2 来说,你还可以使用代码分割进行进一步优化

打包基本过程

Webpack 打包过程中,将 JavaScript 代码打包成一个或多个文件,涉及以下关键步骤:

  1. 依赖图分析

    • Webpack 会从指定的入口文件开始,递归地分析代码中的依赖关系。这包括了导入(import)和导出(export)语句,以及模块的引用关系。
  2. 模块解析

    • Webpack 会解析代码中的模块引入语句,确定每个模块的路径,以便能够正确地加载它们。
  3. Loader 转换

    • 当 Webpack 确定了需要加载的模块后,它会根据配置中的 Loader 对模块进行相应的转换。例如,可以使用 Babel Loader 将 ES6+ 代码转换成 ES5,使用 CSS Loader 处理样式表等。
  4. 代码拆分

    • Webpack 可以根据配置,将代码拆分成多个块。这可以通过手动的代码分割(如使用 import() 动态导入)或者通过配置 splitChunks 进行自动拆分。
  5. 优化和压缩

    • Webpack 提供了许多内置的优化和压缩工具,例如 UglifyJS Plugin 可以用于压缩 JavaScript 代码,OptimizeCSSAssetsPlugin 可以用于优化和压缩 CSS。
  6. 生成输出文件

    • 经过以上步骤处理后,Webpack 会根据配置生成最终的输出文件。这些输出文件可以是一个或多个,具体取决于配置和拆分的策略。
  7. 资源管理

    • 除了处理 JavaScript 代码外,Webpack 还可以通过 Loader 处理其他类型的资源,如样式表、图片、字体等,将它们一并打包到输出文件中。

总的来说,Webpack 的核心工作是将所有依赖关系解析、转换和打包成一个或多个输出文件,这些文件可以在浏览器中加载和运行。这使得前端开发者能够更有效地管理项目中的代码,并提高应用程序的性能和加载速度。

二、webpack 原理解析

2.1 生成一个模块的依赖图(dependency graph)

生成一个模块的依赖图(dependency graph)是 Webpack 在构建过程中的核心功能之一。它会帮助你理解模块之间的依赖关系,从而正确地打包项目。

以下是生成依赖图的基本步骤:

  1. 配置入口文件

    • 在 Webpack 的配置文件中,你需要指定一个或多个入口文件。这些入口文件是构建过程的起点,Webpack 会从这里开始分析依赖关系。
    module.exports = {
      entry: "./src/index.js", // 示例入口文件路径
      // ...
    };
    
  2. 运行 Webpack

    • 使用命令行工具执行 Webpack,它将会开始构建项目。Webpack 会从指定的入口文件开始分析模块的依赖关系。
    npx webpack --config webpack.config.js
    

    或者,如果你在项目中配置了 npm script:

    npm run build
    
  3. 观察输出

    • Webpack 在构建过程中会输出一些信息,包括正在处理的模块和它们的依赖关系。

    例如:

    Hash: 3d83a67d49cd557de07f
    Version: webpack 5.49.0
    Time: 36ms
    Built at: 2021-09-19 11:02:37
                             Asset      Size
    3d83a67d49cd557de07f.js  27 bytes  [emitted] [immutable]  [from: ./src/index.js]
    Entrypoint 3d83a67d49cd557de07f = 3d83a67d49cd557de07f.js
    
  4. 查看输出文件

    • 如果构建成功,你将会在输出目录(由你的配置决定)中找到生成的文件。可以通过查看这些文件来了解模块之间的依赖关系。

    例如:

    dist/
    |- 3d83a67d49cd557de07f.js
    

    你可以在输出的 JavaScript 文件中看到模块的引用关系。

这样,你就能够生成并查看项目的依赖图,以便更好地理解模块之间的关系。

2.2 模块依赖分析原理

依赖图分析是 Webpack 在构建过程中的核心功能之一,它可以通过解析模块间的依赖关系,构建出一个完整的依赖图。

以下是一个简单的示例来说明如何通过代码实现一个简单的依赖图分析:

假设我们有以下的两个 JavaScript 文件:

  1. entry.js
import { add } from "./math.js";

const result = add(2, 3);
console.log(result);
  1. math.js
export function add(a, b) {
  return a + b;
}

我们可以通过一个简单的 JavaScript 脚本来手动实现依赖图分析:

const fs = require("fs");
const path = require("path");

// 定义一个简单的模块加载器
function requireModule(filename) {
  const content = fs.readFileSync(filename, "utf-8");
  const module = { exports: {} };
  const wrapper = new Function(
    "module",
    "exports",
    "__dirname",
    "__filename",
    content
  );
  wrapper(module, module.exports, __dirname, __filename);
  return module.exports;
}

// 入口文件路径
const entryFile = "./entry.js";

// 用于保存所有模块的依赖关系
const dependencyGraph = {};

// 定义一个递归函数来分析模块依赖
function createDependencyGraph(filename) {
  const content = fs.readFileSync(filename, "utf-8");
  const dirname = path.dirname(filename);

  // 将当前模块添加到依赖图中
  if (!dependencyGraph[filename]) {
    dependencyGraph[filename] = { dependencies: [], code: content };
  }

  // 分析当前模块的依赖
  const matches = content.match(
    /import\s*{\s*(\w+)\s*}\s*from\s*['"](.*)['"]/g
  );
  if (matches) {
    matches.forEach((match) => {
      const [, importedModule, dependencyPath] = match.match(
        /import\s*{\s*(\w+)\s*}\s*from\s*['"](.*)['"]/
      );
      const absolutePath = path.resolve(dirname, dependencyPath);
      dependencyGraph[filename].dependencies.push(absolutePath);

      // 递归分析依赖模块的依赖
      createDependencyGraph(absolutePath);
    });
  }
}

// 生成依赖图
createDependencyGraph(entryFile);

console.log(dependencyGraph);

生成依赖图

{
  './entry.js': {
    dependencies: [ 'D:\\webpack\\math.js' ],
    code: "import { add } from './math.js';\r\n" +
      '\r\n' +
      'const result = add(2, 3);\r\n' +
      'console.log(result);\r\n'
  },
  'D:\\webpack\\math.js': {
    dependencies: [],
    code: 'export function add(a, b) {\r\n  return a + b;\r\n}\r\n'
  }
}

这个示例中,我们使用了 path 模块来处理文件路径,确保了在不同操作系统上都能正确工作。同时,我们也修复了正则表达式匹配的问题,确保正确解析模块的依赖关系。

这只是一个非常简单的示例,实际的依赖图分析要复杂得多,需要考虑更多的情况和模块系统的支持。Webpack 使用了更为复杂的技术来进行依赖图分析,以确保正确地处理各种模块系统、Loader 等情况。

这个简单的脚本通过手动解析模块的依赖关系,构建了一个简单的依赖图。这个依赖图中包含了模块的路径、依赖的模块路径以及模块的代码。

实际上,Webpack 使用更复杂的技术来进行依赖图分析,包括对各种模块系统(CommonJS、ES6 模块等)的支持、对 Loader 的处理等。

这只是一个简单的示例来说明依赖图分析的基本原理。

2.3 模块解析(根据模块引入语句来确定模块的路径)

Webpack 的一个关键功能就是模块解析。它会根据模块引入语句来确定模块的路径,以便能够正确地加载它们。

具体来说,Webpack 会根据配置中的规则来解析模块路径。默认情况下,Webpack 会按照以下规则来解析模块路径:

  1. 相对路径:如果模块路径以 ./../ 开头,Webpack 会将其视为相对路径,并以引入语句所在文件的位置为基准来解析路径。

  2. 绝对路径:如果模块路径以 / 开头,Webpack 会将其视为绝对路径,并从项目的根目录开始解析。

  3. 模块路径:如果模块路径不以 ./..// 开头,Webpack 会将其视为模块路径,并会根据配置中的 resolve 字段来解析模块的实际路径。这可能包括搜索 node_modules 目录,以及配置别名等。

例如,在以下引入语句中:

import SomeModule from "some-module";

Webpack 会根据配置中的 resolve 规则来确定 some-module 模块的实际路径。

resolve: {
  modules: ['node_modules'], // 指定搜索模块的目录
  extensions: ['.js', '.json'], // 自动解析的扩展名
  alias: {
    // 别名配置
    'some-module': path.resolve(__dirname, 'src/some-module.js')
  }
}

在这个示例中,Webpack 会首先搜索 node_modules 目录下是否存在 some-module 模块,如果不存在,它会检查是否有别名配置,如果有则使用别名指定的路径。

总的来说,Webpack 的模块解析功能确保了模块的正确加载,同时也提供了一些灵活的配置选项,使开发者可以根据项目的需要来配置模块的解析规则。

2.4 loader 转换

Loader 在 Webpack 中扮演着非常重要的角色,它们用于将各种类型的资源(如 JavaScript、CSS、图片等)转换成 Webpack 可以处理的模块。这些转换过程包括了对代码的处理、压缩、转译等。

以下是 Loader 转换的基本原理:

  1. 匹配文件

    • 在 Webpack 配置文件中,你可以通过配置不同的 Loader 来指定哪些文件需要被转换。
    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/, // 使用正则表达式匹配所有以 .js 结尾的文件
            use: "babel-loader", // 使用 babel-loader 来转换这些文件
          },
          {
            test: /\.css$/, // 匹配所有以 .css 结尾的文件
            use: ["style-loader", "css-loader"], // 使用 style-loader 和 css-loader 来转换这些文件
          },
          // ...
        ],
      },
      // ...
    };
    
  2. 使用 Loader

    • 当 Webpack 确定了哪些文件需要被转换后,它会按照配置中的 Loader 列表来处理这些文件。每个 Loader 都会对匹配到的文件进行特定的转换。

    例如,当匹配到以 .js 结尾的文件时,Webpack 会使用 babel-loader 来将 ES6+ 代码转换成 ES5。

  3. Loader 链

    • 在配置中,可以将多个 Loader 串联起来形成一个 Loader 链。每个 Loader 都会按顺序依次处理文件。
    module: {
      rules: [
        {
          test: /\.scss$/, // 匹配 .scss 文件
          use: ['style-loader', 'css-loader', 'sass-loader'], // 依次使用 style-loader、css-loader、sass-loader
        },
        // ...
      ],
    }
    

    在上面的例子中,首先将 .scss 文件交给 sass-loader 处理,然后是 css-loader 处理,最后是 style-loader 处理。

  4. 转换文件

    • Loader 会根据配置对文件进行相应的转换操作。例如,对 JavaScript 文件进行转译、对 CSS 文件进行处理、对图片进行压缩等。
  5. 输出模块

    • Loader 处理完文件后,会将转换后的内容输出为一个模块,以便 Webpack 进一步处理。

总的来说,Loader 是 Webpack 构建过程中的一个重要环节,它负责对不同类型的文件进行转换处理,使得它们能够成为 Webpack 可以理解和处理的模块。Loader 链的配置允许你在构建过程中对文件进行多个处理步骤,从而满足各种不同的需求。

2.5 常用的 loader

以下是一些常用的 Webpack Loader,可详细查看 loader

  1. babel-loader:用于将新版本的 JavaScript 代码转译成可以在旧版本 JavaScript 引擎中执行的代码,通常用于处理 ES6+代码。

  2. css-loader:用于解析处理 CSS 文件,使其可以在 JavaScript 文件中被引用。

  3. style-loader:将 CSS 样式以 <style> 标签的形式插入到 HTML 文件中。

  4. sass-loader:用于将 Sass 或者 SCSS 文件转译成 CSS。

  5. file-loader:用于处理文件,将文件复制到输出目录,并返回文件路径。

  6. url-loader:类似于 file-loader,但可以将小于指定大小的文件转换成 base64 编码的 DataURL。

  7. json-loader:用于加载 JSON 文件。

  8. image-webpack-loader:用于处理图片,可以进行压缩和优化。

  9. html-loader:用于解析 HTML 文件中的图片路径等资源。

  10. csv-loader:用于加载 CSV 文件。

  11. xml-loader:用于加载 XML 文件。

  12. raw-loader:将文件以字符串的形式导入。

  13. eslint-loader:用于在 Webpack 构建过程中检查和 lint JavaScript 代码。

  14. ts-loader:用于将 TypeScript 文件转译成 JavaScript。

  15. vue-loader:用于加载和转译 Vue.js 单文件组件。

  16. stylelint-webpack-plugin:用于在 Webpack 构建过程中对 CSS 进行 lint 检查。

  17. postcss-loader:用于对 CSS 进行处理,例如自动添加浏览器前缀、压缩等。

实际上 Webpack 社区中有许多不同用途的 Loader 可以满足各种需求。

Contributors: masecho