Webpack知识体系(下)| 青训营笔记

113 阅读6分钟

Webpack知识体系(下)

这是我参与第五届青训营 伴学笔记创作活动的第15天。

课程重点

  1. 理解loader
  2. 理解插件

详细知识点

理解loader

因为webpack只能处理标准的js资源,为了处理非标准js资源,设计出资源翻译模块————Loader,用于将资源翻译为标准js。 下面介绍一个使用Loader处理less的案例:

  1. 安装loader:npm i less-loader css-loader style-loader -D
  2. 添加module处理less
 module: {
    rules: [
      {
        // 用来匹配 .css 结尾的文件
        test: /.css$/,
        // use 数组里面 Loader 执行顺序是从右到左
        use: ["style-loader", "css-loader","less-loader"],
      },
    ],
  },

链式调用 image.png

  1. less-loader: 实现less => css的转换
  2. css-loader: 将css包装成类似module.exports = "${css}"的内容,包装后的内容符合javascript语法。
  3. style-loader:将css模块包进require语句,并在运行时调用injectStyle等函数将内容注入到页面的style标签。

其它特性 image.png

  • 链式执行
  • 支持异步执行
  • 分normal、pitch两种模式

手动实现一个简单的loader

loader接受的参数:

  • content: 源文件内容
  • map: SourceMap数据
  • meta: 数据,可以是任何内容
// loaders/loader1.js
module.exports = function loader1(content) {
  console.log("hello loader");
  return content;
};

在module中配置

 {
        test: /\.less$/,
        use: [
              path.resolve(__dirname,"src/loaders/loader1.js"),//自定义的loader
              "style-loader", //使用多个loader
              path.resolve(__dirname,"src/loaders/loader1.js"),//自定义的loader
              "css-loader", 
              path.resolve(__dirname,"src/loaders/loader1.js"),//自定义的loader
              "less-loader",
              path.resolve(__dirname,"src/loaders/loader1.js")//自定义的loader
            ],
            //loader:'xxx',//只能使用一个loader
  },

理解插件

通过插件我们可以扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。

plugin的工作原理 webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 ——「深入浅出 Webpack」

站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。

钩子的本质 就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。

image.png 钩子的核心信息:

  1. 时机:compier.hooks.compilation 编译过程的特定节点,Webpack会以钩子形式通知插件此刻正在发生什么事情;
  2. 上下文:通过tapable提供的回调机制,以参数方式传递上下文信息;
  3. 交互:在上下文参数对象中附带了很多存在side effect的交互接口,插件可以通过这些接口改变

Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。 Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

  • tap:可以注册同步钩子和异步钩子。
  • tapAsync:回调方式注册异步钩子。
  • tapPromise:Promise 方式注册异步钩子。

Plugin构建对象

compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。

这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。 它有以下主要属性:

  • compiler.options 可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
  • compiler.inputFileSystem 和 compiler.outputFileSystem 可以进行文件操作,相当于 Nodejs 中 fs。
  • compiler.hooks 可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。

compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。

一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。 它有以下主要属性:

  • compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。
  • compilation.chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
  • compilation.assets 可以访问本次打包生成所有文件的结果。
  • compilation.hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。

下面让我们试着开发一个插件

class TestPlugin {
  constructor() {
    console.log("TestPlugin constructor()");
  }
  // 1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
  // 2. webpack创建 compiler 对象
  // 3. 遍历所有插件,调用插件的 apply 方法
  apply(compiler) {
    console.log("TestPlugin apply()");
  }
}

module.exports = TestPlugin;

注册hook

class TestPlugin {
  constructor() {
    console.log("TestPlugin constructor()");
  }
  // 1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
  // 2. webpack创建 compiler 对象
  // 3. 遍历所有插件,调用插件的 apply 方法
  apply(compiler) {
    console.log("TestPlugin apply()");

    // 从文档可知, compile hook 是 SyncHook, 也就是同步钩子, 只能用tap注册
    compiler.hooks.compile.tap("TestPlugin", (compilationParams) => {
      console.log("compiler.compile()");
    });

    // 从文档可知, make 是 AsyncParallelHook, 也就是异步并行钩子, 特点就是异步任务同时执行
    // 可以使用 tap、tapAsync、tapPromise 注册。
    // 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。
    compiler.hooks.make.tap("TestPlugin", (compilation) => {
      setTimeout(() => {
        console.log("compiler.make() 111");
      }, 2000);
    });

    // 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行
    compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("compiler.make() 222");
        // 必须调用
        callback();
      }, 1000);
    });

    compiler.hooks.make.tapPromise("TestPlugin", (compilation) => {
      console.log("compiler.make() 333");
      // 必须返回promise
      return new Promise((resolve) => {
        resolve();
      });
    });

    // 从文档可知, emit 是 AsyncSeriesHook, 也就是异步串行钩子,特点就是异步任务顺序执行
    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("compiler.emit() 111");
        callback();
      }, 3000);
    });

    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("compiler.emit() 222");
        callback();
      }, 2000);
    });

    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("compiler.emit() 333");
        callback();
      }, 1000);
    });
  }
}

module.exports = TestPlugin;

通过调试查看 compiler 和 compilation 对象数据情况: node --inspect-brk ./node_modules/webpack-cli/bin/cli.js

总结

以上内容简单介绍了webpack中loader和plugin原理以及使用,下图总结了webpack的一些常用的配置项和常用的loader,plugin,最好能掌握它们并熟练使用。 image.png