Webpack总结

笔记

Posted by Wason on June 23, 2022

Webpack总结

Webpack 是一个用于JavaScript应用程序的静态模块打包工具,可以很方便的管理模块间的依赖。

静态模块

此处的静态模块指的是开发阶段,可以被 Webpack 直接引用的资源(可以直接被获取打包进bundle.js的资源)。当 Webpack 处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块(不再局限js文件),并生成一个或多个 bundle,如下图

Webpack作用

1.编译代码能力:提高效率,解决浏览器兼容问题
2.模块整合能力:提高性能、可维护性,解决浏览器频繁请求文件问题
3.万物皆可模块化能力:增强了项目可维护性,支持不同种类的前端模块化类型,统一的模块化方案,所有资源文件的加载都可以用代码控制

Webpack的构建过程

webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webpack机制中,去改变Webpack的运作。

从启动到结束会依次经历三大流程:

  • 初始化阶段:从配置文件和 Shell 语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数
  • 编译构建阶段:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出阶段:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统

1.初始化阶段

初始化阶段主要是从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。配置文件默认下为webpack.config.js,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件。下面是webpack.config.js文件配置,内容分析如下注释:

var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');

module.exports = {
  // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
  entry: './path/to/my/entry/file.js',
  // 文件路径指向(可加快打包过程)。
  resolve: {
    alias: {
      'react': pathToReact
    }
  },
  // 生成文件,是模块构建的终点,包括输出文件与输出路径。
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].js'
  },
  // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel',
        query: {
          presets: ['es2015', 'react']
        }
      }
    ],
    noParse: [pathToReact]
  },
  // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置的 plugins。完成上述步骤之后,则开始初始化Compiler编译对象,该对象掌控着webpack声明周期,不执行具体的任务,只是进行一些调度工作

class Compiler extends Tapable {
    constructor(context) {
        super();
        this.hooks = {
            beforeCompile: new AsyncSeriesHook(["params"]),
            compile: new SyncHook(["params"]),
            afterCompile: new AsyncSeriesHook(["compilation"]),
            make: new AsyncParallelHook(["compilation"]),
            entryOption: new SyncBailHook(["context", "entry"])
            // 定义了很多不同类型的钩子
        };
        // ...
    }
}

function webpack(options) {
  var compiler = new Compiler();
  ...// 检查options,若watch字段为true,则开启watch线程
  return compiler;
}
...

在上面的代码中,Compiler 对象继承自 Tapable,初始化时定义了很多钩子函数

2.编译构建

用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。然后,根据配置中的 entry 找出所有的入口文件,如下

module.exports = {
  entry: './src/file.js'
}

初始化完成后会调用Compiler的run来真正启动webpack编译构建流程,主要流程如下:

compile:开始编译
make:从入口点分析模块及其依赖的模块,创建这些模块对象
build-module:构建模块
seal:封装构建结果
emit:把各个chunk输出到结果文件

1) compile 编译

执行了run方法后,首先会触发compile,主要是构建一个Compilation对象。该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象

2) make 编译模块

当完成了上述的compilation对象后,就开始从Entry入口文件开始读取,主要执行_addModuleChain()函数,源码如下:

_addModuleChain(context, dependency, onModule, callback) {
   ...
   // 根据依赖查找对应的工厂函数
   const Dep = /** @type {DepConstructor} */ (dependency.constructor);
   const moduleFactory = this.dependencyFactories.get(Dep);

   // 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
   moduleFactory.create({
       dependencies: [dependency]
       ...
   }, (err, module) => {
       ...
       const afterBuild = () => {
        this.processModuleDependencies(module, err => {
         if (err) return callback(err);
         callback(null, module);
           });
    };

       this.buildModule(module, false, null, null, err => {
           ...
           afterBuild();
       })
   })
}

_addModuleChain中接收参数dependency传入的入口依赖,使用对应的工厂函数NormalModuleFactory.create方法生成一个空的module对象。回调中会把此module存入compilation.modules对象和dependencies.module对象中,由于是入口文件,也会存入compilation.entries中。随后,执行buildModule进入真正的构建模块module内容的过程

3) build module 完成模块编译

这个过程的主要调用配置的loaders,将我们的模块转成标准的JS模块。在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。
从配置的入口模块开始,分析其 AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。

3.输出阶段

seal方法主要是要生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码。Webpack 中的 chunk ,可以理解为配置在 entry 中的模块,或者是动态引入的模块;
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。在确定好输出内容后,根据配置确定输出的路径和文件名即可。

output: {
  path: path.resolve(__dirname, 'build'),
  filename: '[name].js'
}

在 Compiler 开始生成文件前,钩子 emit 会被执行,这是我们修改最终文件的最后一个机会。整个过程如下图所示:

Webpack中常见的Loader Loader本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
默认情况下,在遇到import或者load加载模块的时候,webpack只支持对js文件打包。像css、sass、png等这些类型的文件的时候,webpack则无能为力,这时候就需要配置对应的loader进行文件内容的解析。在加载模块的时候,执行顺序如下图所示

关于配置Loader的方式,有常见的三种方式:

  • 配置方式(推荐):在 webpack.config.js文件中指定 loader
  • 内联方式:在每个 import 语句中显式指定 loader
  • Cli 方式:在 shell 命令中指定它们

1.配置方式

关于Loader的配置,我们通常是写在module.rules属性中,属性介绍如下:

  • rules是一个数组的形式,因此我们可以配置很多个loader
  • 每一个loader对应一个对象的形式,对象属性test 为匹配的规则,一般情况为正则表达式
  • 属性use针对匹配到文件类型,调用对应的 loader 进行处理

下面是一个module.rules的示例代码:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: {
              modules: true
            }
          },
          { loader: 'sass-loader' }
        ]
      }
    ]
  }
};

2.Loader特性

从上述代码可以看到,在处理css模块的时候,use属性中配置了三个loader分别处理css文件。因为loader 支持链式调用,链中的每个loader会处理之前已处理过的资源,最终变为js代码。顺序为相反的顺序执行,即上述执行方式为sass-loader、css-loader、style-loader;

除此之外,loader的特性还有如下:

  • Loader 可以是同步的,也可以是异步的
  • Loader 运行在 Node.js 中,并且能够执行任何操作
  • 除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块
  • 插件(plugin)可以为 loader 带来更多特性
  • Loader 能够产生额外的任意文件

可以通过 loader 的预处理函数,为 JavaScript 生态系统提供更多能力。用户现在可以更加灵活地引入细粒度逻辑,例如:压缩、打包、语言翻译和更多其他特性。

3.常用Loader

在页面开发过程中,除了需要导入一些场景js文件外,还需要配置响应的loader进行加载。

WebPack常见的Loader如下:

  • style-loader:将css添加到DOM的内联样式标签style里,然后通过 dom 操作去加载 css
  • css-loader :允许将css文件通过require的方式引入,并返回css代码
  • less-loader: 处理less,将less代码转换成css
  • sass-loader: 处理sass,将scss/sass代码转换成css
  • postcss-loader:用postcss来处理css
  • autoprefixer-loader: 处理css3属性前缀,已被弃用,建议直接使用postcss
  • file-loader: 分发文件到output目录并返回相对路径
  • url-loader: 和file-loader类似,但是当文件小于设定的limit时可以返回一个Data Url
  • html-minify-loader: 压缩HTML
  • babel-loader: 用babel来转换ES6文件到ES
  • awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
  • eslint-loader:通过 ESLint 检查 JavaScript 代码
  • tslint-loader:通过 TSLint检查 TypeScript 代码
  • cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

下面以css-loader为例子,来说明Loader的使用过程。首先,我们在项目中安装css-loader插件

npm install --save-dev css-loader

然后将规则配置到module.rules中,比如:

rules: [
  ...,
 {
  test: /\.css$/,
    use: {
      loader: "css-loader",
      options: {
     // 启用/禁用 url() 处理
     url: true,
     // 启用/禁用 @import 处理
     import: true,
        // 启用/禁用 Sourcemap
        sourceMap: false
      }
    }
 }
]

Webpack中常见的Plugin

Plugin就是插件,基于事件流框架Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果;

Webpack中的Plugin也是如此,Plugin赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在 Webpack 的不同阶段(钩子 / 生命周期),贯穿了Webpack整个编译周期.

使用的时候,通过配置文件导出对象中plugins属性传入new实例对象即可

const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 访问内置的插件
module.exports = {
  ...
  plugins: [
    new webpack.ProgressPlugin(),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
};

特性

Plugin从本质上来说,就是一个具有apply方法Javascript对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log('webpack 构建过程开始!');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

我们可以使用上面的方式,来获取Plugin在编译生命周期钩子。如下:

  • entry-option :初始化 option
  • compile: 真正开始的编译,在创建 compilation 对象之前
  • compilation :生成好了 compilation 对象
  • make:从 entry 开始递归分析依赖,准备对每个模块进行 build
  • after-compile: 编译 build 过程结束
  • emit :在将内存中 assets 内容写到磁盘文件夹之前
  • after-emit :在将内存中 assets 内容写到磁盘文件夹之后
  • done: 完成所有的编译过程
  • failed: 编译失败的时候

常见的Plugin

Weebpack中,常见的plugin有如下一些:

  • define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
  • ignore-plugin:忽略部分文件
  • html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
  • web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
  • uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)
  • terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
  • webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
  • serviceworker-webpack-plugin:为网页应用增加离线缓存功能
  • clean-webpack-plugin: 目录清理
  • ModuleConcatenationPlugin: 开启 Scope Hoisting
  • speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)
  • webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)

下面通过clean-webpack-plugin来看一下插件的使用方法, 首先,需要安装clean-webpack-plugin插件

npm install --save-dev clean-webpack-plugin

然后,引入插件即可使用

const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
 ...
  plugins: [
    ...,
    new CleanWebpackPlugin(),
    ...
  ]
}

Loader和Plugin的区别,以及如何自定义Loader和Plugin

区别
① Loader本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作
② Plugin就是插件,基于事件流框架Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果

Loader 运行在打包文件之前,Loader在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugins 在整个编译周期都起作用,Plugin在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

自定义Loader

Loader本质上来说就是一个函数,函数中的 this 作为上下文会被 webpack 填充,因此我们不能将 loader设为一个箭头函数。该函数接受一个参数,为 webpack 传递给 loader 的文件源内容。
同时,函数中 this 是由 webpack 提供的对象,能够获取当前 loader 所需要的各种信息。函数中有异步操作或同步操作,异步操作通过 this.callback 返回,返回值要求为 string 或者 Buffer,如下

// 导出一个函数,source为webpack传递给loader的文件源内容
module.exports = function(source) {
    const content = doSomeThing2JsString(source);

    // 如果 loader 配置了 options 对象,那么this.query将指向 options
    const options = this.query;

    // 可以用作解析其他模块路径的上下文
    console.log('this.context');

    /*
     * this.callback 参数:
     * error:Error | null,当 loader 出错时向外抛出一个 error
     * content:String | Buffer,经过 loader 编译后需要导出的内容
     * sourceMap:为方便调试生成的编译后内容的 source map
     * ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
     */
    this.callback(null, content); // 异步
    return content; // 同步
}

自定义Plugin

Webpack的Plugin是基于事件流框架Tapable,由于webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务。
同时,Webpack编译会创建两个核心对象:compiler和compilation。

① compiler:包含了 Webpack 环境的所有的配置信息,包括 options,loader 和 plugin,和 webpack 整个生命周期相关的钩子.
② compilation:作为 Plugin 内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的 Compilation 将被创建.

如果需要自定义Plugin,也需要遵循一定的规范:

  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例
  • 传给每个插件的 compiler 和 compilation 对象都是同一个引用,因此不建议修改
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住

下面是一个自定Plugin的模板:

class MyPlugin {
    // Webpack 会调用 MyPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply (compiler) {
    // 找到合适的事件钩子,实现自己的插件功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
        // compilation: 当前打包构建流程的上下文
        console.log(compilation);

        // do something...
    })
  }
}

在 emit 事件被触发后,代表源文件的转换和组装已经完成,可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。

参考文章

1. webpack