likes
comments
collection
share

打造你的 Webpack Loader 和 Plugin插件

作者站长头像
站长
· 阅读数 10

前言

webpack 只认识 js 文件,像htmlcss图片等都不认识的

打造你的 Webpack Loader 和 Plugin插件

Loader

帮助 webpack 将不同类型的文件 转化为 webpack 能识别的模块

loader的 分类 及 执行顺序

loader 有以下 4 种类型

类别解释类别解释
pre前置 loaderinline内联 loader
nomal普通 loaderpost后置 loader

执行顺序分别是:

  • pre > normal > inline > post
  • 相同优先级的 loader 执行顺序( 如2个 normal 或 2个 inline)遵守 从右向左,从下到上 的原则

🌰:以下 3 个 loader( 默认没有配置 )都属于normal-loader ,执行顺序就是 loader3 -> loader2 -> loader1

module: {
    rules: [
        {
            test: /\.js$/,
            loader: 'loader1'
        },
        {
            test: /\.js$/,
            loader: 'loader2'
        },
        {
            test: /\.js$/,
            loader: 'loader3'
        },
    ]
}

但有时候我们希望有些loader先执行,有些loader后执行,这时需要用到 enforce 属性把其定义成前置loader 或者 后置loader。如果没有指定类型,那默认还是 normal-loader

🌰:此时的执行顺序就是 loader1 -> loader2 -> loader3

module: {
    rules: [
        {
            test: /\.js$/,
            loader: 'loader1',
            enforce: 'pre'
        },
        {
            test: /\.js$/,
            loader: 'loader2'
        },
        {
            test: /\.js$/,
            loader: 'loader3',
            enforce: 'post'
        },
    ]
}

inline-loader

此时你发现没有使用 inline-loader,其他 loader ( pre、nomal、post ) 都可以 配置使用,inline-loader 顾名思义,需要内联使用, 在每个 import 语句中显式指定 loader

🌰: 下面是 inline-loader 的使用方法,!是loader之间的分隔符, params是传递给inline-loader2的参数,我要用内联的方式显式指定两个loader 来处理./styles.css文件

import Styles from 'inline-loader1!inline-loader2?params!./styles.css

inline-loader 通过添加不同的前缀跳过其他类型的loader

这里其他类型指的是,你在配置文件webpack.config.jsmodule.rules中针对某类文件(例如.css)已经配置过的loader

  1. !:跳过 nomal-loader
import Styles from '!inline-loader1!inline-loader2?params!./styles.css

  1. -!:跳过 pre-loadernomal-loader
import Styles from '-!inline-loader1!inline-loader2?params!./styles.css

  1. !!:跳过 pre-loadernomal-loaderpost-loader
import Styles from '!!inline-loader1!inline-loader2?params!./styles.css

总结: 推荐配置方式,不推荐内联方式,因为不好复用,了解一下即可

创建一个最简单的loader

  1. 创建一个空项目,记得安装必须的依赖 yarn add webpack webpack-cli html-webpack-plugin -D

打造你的 Webpack Loader 和 Plugin插件

入口文件 main.js 写一行简单的代码

const message = 'Hello webpack-loader-plugin';
  1. 根目录下创建一个 loaders/simple-loader.js 的文件
module.exports = function(content) {
  console.log(content);
  return content;
}

loader 一共有 3 个参数,contentmapmeta

  • content:文件内容
  • map:与source-map相关
  • meta:来自其他 loader(上一个)传递过来的参数
  1. webpack.config.js 配置文件中引入 simple-loader.js
module: {
  rules: [
    {
      test: /\.js$/,
      loader: './loaders/simple-loader.js'
    }
  ]
},
  1. 执行 npx webpack 打包命令,可以看到输出了 main.js 中的内容

打造你的 Webpack Loader 和 Plugin插件

阶段总结

可见 loader 就是一个函数,当执行打包命令时,会执行 webpack.config.js 中所有 loader,每个 loader 把自己监听的文件,当作参数传递进 loader 函数内,因为项目中只有一个 main.js 的 js 文件,故被当作 content 参数传递进了 simple-loader函数内部,被打印了出来

4 种定义 loader 的方式

同步 loader

/loaders 目录下创建一个 sync-loader.js同步 loader 文件,同步 loader 有 种书写方式,第一种默认,第二种通过 this.callback() 可以把打包时的错误信息、和 其他参数 一并传递出去

// 方式 1
module.exports = function(content) {
  return content
}

// 方式 2
module.exports = function(content, map, meta) {
  /**
   * 参数1: err 代表错误信息
   * 参数2: 文件内容
   * 参数3: source-map相关
   * 参数4: 其他 loader 传递过来的参数
   */
  console.log('同步loader内部');
  return this.callback(null, content, map, meta)
}

异步 loader

/loaders 目录下创建一个 async-loader.js异步 loader 文件,注意到 this.async() 这个方法,同时使用 setTimeout 模拟一个异步操作

module.exports = function(content, map, meta) {
  const callback = this.async();

  // 模拟异步操作
  setTimeout(() => {
    console.log('异步loader内部');
    callback(null, content, map, meta)
  }, 1000);
}

修改配置文件后,我们打包一下,看会输出什么

rules: [
  // {
  //   test: /\.js$/,
  //   loader: './loaders/simple-loader.js'
  // },
  {
    test: /\.js$/,
    use: ['./loaders/sync-loader', './loaders/async-loader']
  },
]

打造你的 Webpack Loader 和 Plugin插件 1 秒中之后控制台先输出了 async-loader 异步 loader 里的打印,又输出了sync-loader 同步 loader 里的打印

raw loader

/loaders 目录下创建一个 raw-loader.jsraw loader 文件,区别在于,content 将被转换为 Buffer数据流, 同时在导出模块是添加 module.exports.raw = true

module.exports = function(content) {
  // content 为 Buffer 数据流
  console.log(content);
  return content;
}

module.exports.raw = true;

修改配置文件,让我们打包看一下打印结果,输出了Buffer类型的数据

module: {
  rules: [
    // {
    //   test: /\.js$/,
    //   loader: './loaders/simple-loader.js'
    // },
    {
      test: /\.js$/,
      // use: ['./loaders/sync-loader', './loaders/async-loader']
      use: ['./loaders/raw-loader']
    },
  ]
}

打造你的 Webpack Loader 和 Plugin插件 当我们处理 图片字体图标 等文件时,可以使用 raw loader

pitch loader

/loaders 目录下创建一个 pitch-loader.jspitch loader 文件

module.exports = function(content) {
  console.log('pitch-loader1');
  return content;
}

module.exports.pitch = function() {
  console.log('pitch-fn-1');
}

我们看到 pitch loader 多了一个 pitch 方法,它是如何运行的那,我们知道当配置多个 loader 时,遵循 从右向左,从下到上 的规则 ,例如:['style-loader', 'css-loader']

打造你的 Webpack Loader 和 Plugin插件 当我们的 loader 配置了 pitch 方法后,将按照下图的方法顺序运行

打造你的 Webpack Loader 和 Plugin插件

测试一下

/loaders 目录下创建一个 pitch-loader2.js

module.exports = function(content) {
  console.log('pitch-loader2');
  return content;
}

module.exports.pitch = function() {
  console.log('pitch-fn-2');
}

修改配置文件,让我们打包看一下输出结果

rules: [
  {
    test: /\.js$/,
    use: ['./loaders/pitch-loader', './loaders/pitch-loader2']
  },
]

打造你的 Webpack Loader 和 Plugin插件 结果如我们预期的一样,总结一下:先从左向右执行 pitch方法,再从右往左执行正常方法,❗️一旦任何一个 pitch 方法中执行 return 语句,整个链条就会截止到此方法,然后跳到 上一个 pitch方法的正常方法

打造你的 Webpack Loader 和 Plugin插件

实战 clean-console-loader

下面我们创建一个真正具备功能意义上的loader,它有以下能力:

  1. 我们希望将 js 中的console.log语句全部干掉
  2. 动态添加作者信息

/loaders 目录下创建一个 clean-console-loader.js,使用正则把 console.log 替空,通过 this.getOptions 和自定义的 schema 验证规则,获取 wepack.config / loader /options 中传入的属性

const schema = require('./schema.json');

module.exports = function(content) {
  // 获取 配置文件中 options 中的选项
  // schema 是对 options 的验证规则
  // schema 要复合 json-schema 的规则

  const opts = this.getOptions(schema);

  const prefix = `
  /**
   * Auth: ${opts.author}
  */
  `;

  return prefix + content.replace(/console\.log\(.*\);?/g, '')
}

创建 /loaders/schema.json,对 options 的验证规则

{
  "type": "object",
  "properties": {
    "author": {
      "type": "string"
    }
  },
  "additionalProperties": false
}

修改 main.js 文件、配置文件

const message = 'Hello webpack-loader-plugin';
// 添加打印语句
console.log(1)
console.log(2)
console.log(3)
rules: [
  {
    test: /\.js$/,
    loader: './loaders/clean-console-loader',
    options: {
      author: '田川_'
    }
  },
]

执行 npx webpackdist/js/main.js文件内没有任何 console.log,同时动态添加了作者信息

打造你的 Webpack Loader 和 Plugin插件

Plugin

webpack 就像一条生产线,要经过一系列的处理,才能把源文件转化为输出结果

webpack 在执行时会创建 compiler compilation对象,此二对象身上有很多 hooks 钩子函数,这些生命周期(钩子)组成了webpack的运行流程,

打造你的 Webpack Loader 和 Plugin插件

插件要做的就是,找到相应的钩子🪝,往上面挂上自己的任务,也就是注册事件 ,当 webpack 执行时就会自动触发我们注册的事件

总之,插件就是,通过扩展wepack的能力,使webpack变得更强

打造你的 Webpack Loader 和 Plugin插件

创建第一个插件

创建 plugins/first-plugin.js

class FirstPlugin {
  constructor() {
    console.log('插件的构造函数打印')
  }

  apply(compiler) {
    console.log('apply')
  }
}

module.exports = FirstPlugin;

webpack.config.js 中使用插件

const FirstPlugin = require('./plugins/first-plugin.js');
...
{
  plugins: [new FirstPlugin()]
}

执行 npx webpack 我们看到打印顺序

打造你的 Webpack Loader 和 Plugin插件

注册 compile 事件

以下生命周期钩子函数,是由 compiler 暴露, 可以通过如下方式访问

compiler.hooks.someHook.tap('MyPlugin', (params) => {
  /* ... */
});

官方文档compiler 有如此多 hooks,第一个钩子是 enviroment 打造你的 Webpack Loader 和 Plugin插件

由文档可知,environment 是同步钩子,所以要 tab 调用

调用方法介绍
tap同步 / 异步都可以
tapAsync异步
tapPromise异步

以下是一个 异步串行 的例子🌰

class FirstPlugin {
  constructor() {
    console.log('插件的构造函数打印')
  }

  apply(compiler) {
    console.log('apply 方法执行');
    // 由文档可知,environment 是同步钩子,所以要 tab 调用
    compiler.hooks.environment.tap('FirstPlugin', () => {
      console.log('environment钩子没有参数,每个🪝具体参数,见官方文档')
    });
    // 由文档可知,emit 是异步串行钩子
    compiler.hooks.emit.tap('FirstPlugin', (compilation) => {
      console.log('first-plugin emit 111')
    });

    compiler.hooks.emit.tapAsync('FirstPlugin', (compilation, callback) => {
      setTimeout(() => {
        console.log('first-plugin emit 222');
        callback();
      }, 2000);
    });

    compiler.hooks.emit.tapPromise('FirstPlugin', (compilation) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log('first-plugin emit 333');
          resolve();
        }, 1000)
      })
    });
  }
}

module.exports = FirstPlugin;

打造你的 Webpack Loader 和 Plugin插件

以下是一个 异步并行 的例子🌰

// 异步并行
compiler.hooks.make.tapAsync('FirstPlugin', (compilation, callback) => {
  setTimeout(() => {
    console.log('first-plugin make 111');
    callback();
  }, 3000);
});

compiler.hooks.make.tapAsync('FirstPlugin', (compilation, callback) => {
  setTimeout(() => {
    console.log('first-plugin make 222');
    callback();
  }, 1000);
});

compiler.hooks.make.tapAsync('FirstPlugin', (compilation, callback) => {
  setTimeout(() => {
    console.log('first-plugin make 333');
    callback();
  }, 2000);
});

注册 compilation 事件

由上文流程图可知, compilation 的执行阶段在 compile.afterCompile()之前,也就是 compiler.make()阶段才会生效,所以我们可以在 make事件里 注册 compilation的事件,以下是一个 seal的例子

compiler.hooks.make.tapAsync('FirstPlugin', (compilation, callback) => {

  compilation.hooks.seal.tap('FirstPlugin', () => {
    console.log(' ---  compilation.seal   --- ')
  })
  
  setTimeout(() => {
    console.log('first-plugin make 111');
    callback();
  }, 3000);
});

实战 sign-plugin

开发思路

  • 需要打包输出前添加注释签名,需要使用 compiler.hooks.emit 钩子🪝,它是打包 📦 输出前触发,emit是我们最后的机会

打造你的 Webpack Loader 和 Plugin插件

  • 如何获取打包输出的资源,compilation.assets,可以获取所有即将输出的资源文件

创建 plugins/sign-plugin.js

class SignPlugin {
  constructor(options = []) {
    this.options = options
  }
  apply(compiler) {
    compiler.hooks.emit.tap("SignPlugin", (compilation) => {

      const extensions = ["css", "js"];

      // 1. 筛选目标资源的扩展类型: compilation.assets
      // 2. 过滤只保留 js和css资源
      const assets = Object.keys(compilation.assets).filter((assetPath) => {
        // 过滤代码略
        const splitted = assetPath.split(".");
        // 获取最后一个元素作为扩展名
        const extension = splitted[splitted.length - 1];
        // 判断是否为资源
        return extensions.includes(extension);
      });
      const prefix = `/**
        Author: ${this.options.author}
      */`
      // 3. 遍历所有资源添加上注释
      // console.log(assets);
      assets.forEach((asset) => {
        const source = compilation.assets[asset].source();
    
        const content = prefix + source;
    
        // ... some missing code ...
    
        compilation.assets[asset] = {
          // ... some missing code ...
          source() {
            return content;
          },
          // ... some missing code ...
          size() {
            return content.length;
          },
        };
      });
    });
  }
}

module.exports = SignPlugin;

webpack.config.js 配置中使用插件

const SignPlugin = require('./plugins/sign-plugin.js');
plugins: [
  new SignPlugin({
    author: '田大大'
  })
],
mode: 'production'

执行 npx webpack

打造你的 Webpack Loader 和 Plugin插件