前端构建工具

webpack原理的简单入门

约 1859 字大约 6 分钟

nodewebpack

2021-03-21

webpack

前言

我们知道, webpack 作为前端工程化中,主流的模块打包工具之一,应用于各种各样的前端工程化项目中。

虽然大多数项目都或多或少会使用到 webpack, 但是可能对于大多数的 前端开发人员来说, 可能只是改改 webpack 的配置, 或者甚至从未动过 webpack 的相关文件, 或多或少对 webpack 的配置以及功能感到陌生。

还有类似于 vue-clicreate-react-appumi.js 等各种基于 webpack 封装的 脚手架, 提供了各种开箱即用的功能,这使得 webpack 离我们好像越来越远。

但是当我们的某个项目面临了不得不去 深入 webpack 才能解决的问题,或者 面试时,被问起 webpack 相关的问题, 就难以解决或者回答。

所以我们需要对 webpack 至少有基本的了解,了解它的原理、如何编写 loaderplugin 等。

webpack是什么

引用 webpack官网

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph from one or more entry points and then combines every module your project needs into one or more bundles, which are static assets to serve your content from.

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

从作用上讲,webpack 的功能就是将不同模块的文件,打包整合到一起,并且保证它们之间引用的正确,且有序执行。 这使得我们在做项目架构时,能够从模块的角度去做文件拆分,然后交给 webpack 打包整合。

而一个项目中的文件,不仅有 html文件、CSS文件、JavaScript文件、图片资源、Vue特有的.vue文件,typescript的.ts 文件等,以及项目的中的代码还需要进行压缩混淆、浏览器兼容、等等必要的处理,启动一个本地的开发服务器、模块的热更新替换等, 可以通过webpack 提供的各种机制,来一一实现。

对于 webpack 来说, 它自身只能识别 JavaScript 文件, 而对于其他的资源,可以通过 webpack提供的 Loader 特性来实现 识别。 通过 Loader,可以把其它类型的资源文件,转换为 webpack能够处理的有效模块。

而对于 代码混淆、本地开发服务器、模块热更新,则可以通过 webpack 提供的 Plugin 特性来实现功能上的扩展。

模块打包原理

在 webpack 中,有四个基础且核心的概念:

  • 入口(entry)
  • 输出(output)
  • 加载器(Loader)
  • 插件(Plugin)

入口(entry)

指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

输出(output)

告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件

加载器(Loader)

webpack 自身只能理解 JavaScript 文件 和 json 文件, loader 可以将其他类型的资源文件转换为 webpack能够处理的有效模块。

本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

插件(Plugin)

用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。 插件接口功能极其强大,可以用来处理各种各样的任务。

模块(modules)

在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块。

对于 webpack ,任何文件都可以是一个模块。

模块打包运行原理

在说 webpack 的 模块打包运行原理 之前, 先看下 我们是如何使用 webpack的, 一般情况下, 我们通过编写一个 配置文件webpack.config.js, 对 webpack 进行本地化的配置, 大致的配置如下:

module.exports = {
  // 声明模块的入口文件
  entry: './src/entry.js',
  output: {
    path: path.resolve(__dirname, 'dist'), // 输出目录
    filename: 'bundle.js', // 文件名称
  },
  module: {
    rules: [
      // 配置 使用 babel-loader 对 .js 资源进行转换
      {
        test: /\.js$/,
        loader: 'babel-loader',
      },
      // ...more loader
    ],
  },
  // 插件配置
  plugins: [
    new EslintWebpackPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    // ...more plugin
  ],
  // ...more config
}

webpack 读取了 配置文件后,运行的流程大致如下:

  1. 读取 webpack 的配置参数;
  2. 启动 webpack , 创建 compiler 对象,开始解析项目;
  3. 从入口文件 entry 开始解析,并找到其导入的依赖模块,递归遍历分析,形成依赖关系树
  4. 对不同的文件类型资源的依赖模块文件,使用对应的 Loader 进行转换,最终转为 webpack的有效模块;
  5. 在编译过程中, webpack 通过 发布订阅模式,向外抛出一些 hookswebpackPlugin 通过监听各个 hooks , 执行插件任务,扩展 webpack 的功能,干预输出结果。
  6. 根据 输出配置 output ,将打包构建好的资源文件 输出。

compiler 对象是一个全局单例,负责控制整个 webpack 构建流程。

在构建过程中,还会产生一个当前构建的上下文对象 compilation, 它包含了当前构建的所有信息,在每个热更新或重新构建时, compiler 都会产生一个新的compilation 对象,负责当前构建过程。

每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后, 会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块, 进而继续循环执行下一个模块的编译解析。

最终, webpack 打包构建出来的 bundle 文件,是一个 IIFE 执行函数。

// webpack5下进行的最小化打包输出文件
(() => { 
  // webpack 模块文件内容
  var __webpack_modules__ = ({
    "entry.js": ((modules) => { /* ... */ }),
    "other.js": ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { /* ... */ })
  });

  // 模块缓存
  var __webpack_module_cache__ = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = __webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {}
    };

    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    // Return the exports of the module
    return module.exports;
  }

  // startup
  // Load entry module and return exports
  // This entry module can't be inlined because the eval devtool is used.
  var __webpack_exports__ = __webpack_require__("entry.js");
    
})();

在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__ 是Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。

__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。