webpack 手写 loader 与 plugin

手写 loader

什么是 loader?

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 “load(加载)” 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS 文件!

通俗等说:对于 webpack 来说,一切资源皆是模块,但由于 webpack 默认只支持 es5 的 js 以及 json,像是 es6+, react,css 等都要由 loader 来转化处理。

loader 代码结构

loader 只是一个导出为函数的 js 模块。

module.exports = function(source, map) {
	return source;
}
复制代码

其中 source 表示匹配上的文件资源字符串,map 表示 SourceMap。
注意: 不要写成箭头函数,因为 loader 内部的属性和方法,需要通过 this 进行调用,比如默认开启 loader 缓存,配制 this.cacheable(false) 来关掉缓存

同步 loader

需求: 替换 js 里的某个字符串
实现:
新建个 replaceLoader.js:

module.exports = function (source) {
  return `${source.replace('hello', 'world')} `;
};
复制代码

webpack.config.js:

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'main.js',
  },
  module: {
    rules: [{ test: /\.js$/, use: './loaders/replaceLoader.js' }],
  },
};
复制代码

传递参数

上面的 replaceLoader 是固定将某个字符串(hello)替换掉,实际场景中更多的是通过参数传入,即
webpack.config.js:

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'main.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        // 通过 options 参数传参
        use: [
          {
            loader: './loaders/replaceLoader.js',
            options: {
              name: 'hello',
            },
          },
        ],
        // 通过字符串来传参
        // use: './loaders/replaceLoader.js?name=hello'
      },
    ],
  },
};

复制代码

以上两种传参方式,如果使用 query 属性来获取参数,就会出现字符串传参获取到的是字符串, options 传参获取到的是对象格式,不好处理。这里推荐使用 loader-utils 库来处理。
这里使用 getOptions 来接收参数

const { getOptions } = require('loader-utils');
module.exports = function (source) {
  const params = getOptions(this);
  return `${source.replace(params.name, 'world')} `;
};

复制代码

异常处理

第一种: loader 内直接通过 throw 抛出

const { getOptions } = require('loader-utils');
module.exports = function (source) {
  const params = getOptions(this);
  throw new Error('出错了');
};
复制代码

第二种: 通过 this.callback 传递错误

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

第一个参数就是错误信息,当传递 null 里,作用跟前面的直接 return 个字符串作用类似,更建议采用这种方式返回内容

const { getOptions } = require('loader-utils');
module.exports = function (source) {
  const params = getOptions(this);
  this.callback(new Error("出错了"), `${source.replace(params.name, 'world')} `);
};

复制代码

异步处理

当遇到要处理异步需求时,比如获取文件,此时通过 this.async() 告知 webpack 当前 loader 是异步运行。

const fs = require('fs');
const path = require('path');
module.exports = function (source) {
  const callback = this.async();
  fs.readFileSync(
    path.resolve(__dirname, '../src/async.txt'),
    'utf-8',
    (error, content) => {
      if (error) {
        callback(error, '');
      }
      callback(null, content);
    }
  );
};

复制代码

其中 callback 是跟上面 this.callback 一样的用法。

文件输出

通过 this.emitFile 进行文件写入。

const { interpolateName } = require('loader-utils');
const path = require('path');
module.exports = function (source) {
  const url = interpolateName(this, '[name].[ext]', { source });
  this.emitFile(url, source);
  this.callback(null, source);
};

复制代码

resolveLoader

上述设置 loader 时将整个文件路径都配置了,这样写多了,是有些麻烦的,可以通过 resolveLoader 定义 loader 的查找文件路径。

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'main.js',
  },
  resolveLoader: { modules: ['./loaders/', 'node_modules'] },
  module: {
    rules: [
      {
        test: /\.js$/,
        // 通过 options 参数传参
        use: [
          {
            loader: 'asyncLoader.js',
          },
          {
            loader: 'emitLoader.js',
          },
          {
            loader: 'replaceLoader.js',
            options: {
              name: 'hello',
            },
          },
        ],
        // 通过字符串来传参
        // use: './loaders/replaceLoader.js?name=hello'
      },
    ],
  },
};

复制代码

plugin 工作机制

在手写 plugin 之前,先讲下 webpack 里 plugin 的工作机制,方便后续的讲解。
webpack 在对参数处理完之后,会进入 compiler 里的 run 函数,在该函数内部会
在 webpack.js 有如下代码:

compiler = new Compiler(options.context);
compiler.options = options;
if (options.plugins && Array.isArray(options.plugins)) {
	for (const plugin of options.plugins) {
		if (typeof plugin === "function") {
			plugin.call(compiler, compiler);
		} else {
			plugin.apply(compiler);
		}
	}
}
复制代码

可以看到会遍历 options.plugins 并依次调用 apply 方法,当然如果 plugin 是个函数的话,会调用 call,但现在推荐将 plugin 定义个类。

手写 plugin

什么是 plugin

插件是 webpack 的 支柱 功能。webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上!

插件目的在于解决 loader 无法实现的其他事。

插件类似于 React, Vue 里的生命周期,就是个某个时间点会触发,比如 emit 钩子:输出 asset 到 output 目录之前执行;done 钩子:在编译完成时执行。

plugin 代码结构

plugin 就是个类,该类里有个 apply 方法,方法会接收 compiler 参数

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享