打造你的 Webpack Loader 和 Plugin插件

前言

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

Loader

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

loader的 分类 及 执行顺序

loader 有以下 4 种类型

类别 解释 类别 解释
pre 前置 loader inline 内联 loader
nomal 普通 loader post 后置 loader

执行顺序分别是:

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

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

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

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

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

js 复制代码
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文件

js 复制代码
import Styles from 'inline-loader1!inline-loader2?params!./styles.css

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

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

  1. !:跳过 nomal-loader
js 复制代码
import Styles from '!inline-loader1!inline-loader2?params!./styles.css
  1. -!:跳过 pre-loadernomal-loader
js 复制代码
import Styles from '-!inline-loader1!inline-loader2?params!./styles.css
  1. !!:跳过 pre-loadernomal-loaderpost-loader
js 复制代码
import Styles from '!!inline-loader1!inline-loader2?params!./styles.css

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

创建一个最简单的loader

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

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

js 复制代码
const message = 'Hello webpack-loader-plugin';
  1. 根目录下创建一个 loaders/simple-loader.js 的文件
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
js 复制代码
module: {
  rules: [
    {
      test: /\.js$/,
      loader: './loaders/simple-loader.js'
    }
  ]
},
  1. 执行 npx webpack 打包命令,可以看到输出了 main.js 中的内容

阶段总结

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

4 种定义 loader 的方式

同步 loader

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

js 复制代码
// 方式 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 模拟一个异步操作

js 复制代码
module.exports = function(content, map, meta) {
  const callback = this.async();

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

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

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

1 秒中之后控制台先输出了 async-loader 异步 loader 里的打印,又输出了sync-loader 同步 loader 里的打印

raw loader

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

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

module.exports.raw = true;

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

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

当我们处理 图片字体图标 等文件时,可以使用 raw loader

pitch loader

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

js 复制代码
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']

当我们的 loader 配置了 pitch 方法后,将按照下图的方法顺序运行

测试一下

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

js 复制代码
module.exports = function(content) {
  console.log('pitch-loader2');
  return content;
}

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

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

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

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

实战 clean-console-loader

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

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

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

js 复制代码
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 的验证规则

js 复制代码
{
  "type": "object",
  "properties": {
    "author": {
      "type": "string"
    }
  },
  "additionalProperties": false
}

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

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

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

Plugin

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

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

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

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

创建第一个插件

创建 plugins/first-plugin.js

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

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

module.exports = FirstPlugin;

webpack.config.js 中使用插件

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

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

注册 compile 事件

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

js 复制代码
compiler.hooks.someHook.tap('MyPlugin', (params) => {
  /* ... */
});

官方文档compiler 有如此多 hooks,第一个钩子是 enviroment

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

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

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

js 复制代码
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;

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

js 复制代码
// 异步并行
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的例子

js 复制代码
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是我们最后的机会
  • 如何获取打包输出的资源,compilation.assets,可以获取所有即将输出的资源文件

创建 plugins/sign-plugin.js

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 配置中使用插件

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

执行 npx webpack

相关推荐
ywf12152 分钟前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭9 分钟前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf6 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特6 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷7 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian7 小时前
前端node常用配置
前端
华洛7 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq8 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A9 小时前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常9 小时前
被EdgeToEdge适配折磨疯了,谁懂!
前端