深入浅出loader与plugin

146 阅读3分钟

loader和plugin它们两者在webpack内部是如何进行工作的呢?

让我们手写一个loader和plugin来看看他的内部原理,以便加深对webpack的理解

手写loader

看过一些webpack文档的人应该都知道,loader是链式传递的,将文件资源从上一个传递到下一个,并且loader的处理也遵循从下往上的顺序,现在我们简单了解一下loader的开发原则

  1. 单一原则:每个loader只做一件事情,简单应用,便于维护

  2. 链式原则:webpack会按顺序链式调用每个loader

  3. 统一原则:遵循webpack制定的设计规则和结构,输入输出均为字符串,各个loader完全独立,即插即用

    现在我们尝试写一个less-loaderstyle-loader,将less文件处理后通过style标签的方式渲染到页面上去

同步loader

loader其实就是一个函数,接受匹配到的资源字符串soueceMap,我们可以修改文件内容字符串后再返回给下一个loader处理,因此一个最简单的loader如下

module.exports = function(source, soueceMap){
    return source
}

导出的loader函数不能使用箭头函数,因为很多loader内部的属性和方法都需要通过this进行调用,比如this.cacheable()来进行缓存、this.sourceMap判断是否需要生成sourceMap等。

我们在项目中创建一个loader文件夹,用来存放我们自己写的loader,然后新建我们自己的style-loader。

//loader/style-loader.js
function loader(source, sourceMap) {
  let style = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.head.appendChild(style)
  `;
  return style;
}
module.exports = loader;

这里的source可以看做是处理后的css文件字符串,我们通过style标签的形式将他插入到head中,这时可以发现最后返回的是一个JS代码的字符串,webpack会将返回的字符串打包到模块中。

异步loader

上面的style-loader都是同步操作,我们在处理source时,有时候会进行异步操作,一种方法是通过async/await,阻塞操作执行;另一种方法可以通过loader本身提供的回调函数callback

//loader/less-loader
const less = require("less");
function loader(source) {
  const callback = this.async();
  less.render(source, function (err, res) {
    let { css } = res;
    callback(null, css);
  });
}
module.exports = loader;

callback的详细传参方法如下:

callback({
    //当无法转换原内容时,给 Webpack 返回一个 Error
    error: Error | Null,
    //转换后的内容
    content: String | Buffer,
    //转换后的内容得出原内容的Source Map(可选)
    sourceMap?: SourceMap,
    //原内容生成 AST语法树(可选)
    abstractSyntaxTree?: AST 
})

加载本地loader

loader文件准备好了之后,我们需要将它们加载到webpack配置中去;我们加载第三方的loader只需要安装后在loader属性中写loader名称即可,现在加载本地loader需要把loader的路径配置上。

module.exports = {
  module: {
    rules: [{
      test: /\.less/,
      use: [
        {
          loader: './loader/style-loader.js',
        },
        {
          loader: path.resolve(__dirname, "loader", "less-loader"),
        },
      ],
    }]
  }
}

我们可以在loader中配置本地loader的相对路径或者绝对路径,但是这样写起来比较繁琐,我们可以利用webpack提供的resolveLoader属性,来告诉webpack应该去哪里解析本地loader。

module.exports = {
  module: {
    rules: [{
      test: /\.less/,
      use: [
        {
          loader: 'style-loader',
        },
        {
          loader: 'less-loader',
        },
      ],
    }]
  },
  resolveLoader:{
    modules: [path.resolve(__dirname, 'loader'), 'node_modules']
  }
}

处理参数

我们在配置loader时,经常会给loader传递参数进行配置,一般是通过options属性来传递的,也有像url-loader通过字符串来传参。

{
  test: /\.(jpg|png|gif|bmp|jpeg)$/,
  use: 'url-loader?limt=1024&name=[hash:8].[ext]'
}

手写plugin

在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。和手写loader一样,我们先来写一个简单的plugin:

class MyPlugin {
  //编写一个构造器
  constructor(options) {
    console.log(options);
    this.options = options
  }

  apply(compiler) {
    console.log(this.options)
    compiler.hooks.compile.tap("CopyrightWebpackPlugin", () => {
      console.log("compiler");
    });
}
}
module.exports = MyPlugin;

plugin的本质是类;我们在定义plugin时,其实是在定义一个类;定义好plugin后就可以在webpack配置中使用这个插件:

//webpack.config.js
const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
  plugins: [
    new MyPlugin()
  ],
}

我们在构建插件时就能通过options获取配置信息,对插件做一些初始化的工作。在构造函数中我们发现多了一个apply函数,它会在webpack运行时被调用,并且注入compiler对象;其工作流程如下:

  1. webpack启动,执行new myPlugin(options),初始化插件并获取实例
  2. 初始化complier对象,调用myPlugin.apply(complier)给插件传入complier对象
  3. 插件实例获取complier,通过complier监听webpack广播的事件,通过complier对象操作webpack

  我们可以通过apply函数中注入的compiler对象进行注册事件。

class MyPlugin {
  apply(compiler) {
    //注册完成的钩子
    compiler.hooks.done.tap("MyPlugin", (compilation) => {
      console.log("compilation done");
    });
  }
}

compiler不仅有同步的钩子,通过tap函数来注册,还有异步的钩子,通过tapAsynctapPromise来注册:

class MyPlugin {
  apply(compiler) {
    compiler.hooks.run.tapAsync("MyPlugin", (compilation, callback) => {
      setTimeout(()=>{
        console.log("compilation run");
        callback()
      }, 1000)
    });
    compiler.hooks.emit.tapPromise("MyPlugin", (compilation) => {
      return new Promise((resolve, reject) => {
        setTimeout(()=>{
          console.log("compilation emit");
          resolve();
        }, 1000)
      });
    });
  }
}

compiler和compilation的区别在于:

  • compiler代表了整个webpack从启动到关闭的生命周期,而compilation只是代表了一次新的编译过程
  • compiler和compilation暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理

手写FileListPlugin

  了解了compiler和compilation的区别,我们就来尝试一个简单的示例插件,在打包目录生成一个filelist.md文件,文件的内容是将所有构建生成文件展示在一个列表中:

class FileListPlugin {
    apply(compiler){
        compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback)=>{
            var filelist = 'In this build:\n\n';
            // 遍历所有编译过的资源文件,
            // 对于每个文件名称,都添加一行内容。
            for (var filename in compilation.assets) {
                filelist += '- ' + filename + '\n';
            }
            // 将这个列表作为一个新的文件资源,插入到 webpack 构建中:
            compilation.assets['filelist.md'] = {
                source: function() {
                    return filelist;
                },
                size: function() {
                    return filelist.length;
                }
            };
            callback();
        })
    }
}
module.exports = FileListPlugin

参考文献Webpack手写loader和plugin