Webpack4之手动实现一个Loader

下面手动实现一个简单的 Webpack Loader。 先创建一个 Webpack 项目 在根目录中创建 src 文件夹,并在里面中创建一个 index.js 文件,编写下面代码

目录
  1. 搭建项目
  2. 编写 Loader
    1. 简单实现
    2. 选项设置
    3. callback 接口
    4. 异步函数
    5. 多个 Loader
    6. Loader 配置
  3. 小结

下面手动实现一个简单的 Webpack Loader。

搭建项目#

先创建一个 Webpack 项目

mkdir make-loader
cd make-loader
yarn init -y
yarn add webpack webpack-cli -D

在根目录中创建 src 文件夹,并在里面中创建一个index.js文件,编写下面代码

console.log("Hello Webpack");

创建 Webpack 配置文件webpack.config.js

const path = require("path");
 
module.exports = {
  mode: "development",
  entry: path.resolve(__dirname, "src/index"),
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
  },
};

package.json中编写打包脚本命令

"scripts": {
  "build": "webpack"
}

尝试进行编译打包

yarn build

查看根目录下 dist 文件夹里面的main.js文件,这是打包生成的文件,在文件倒数几行里发现下面的代码,编译成功。

eval(
  "console.log('Hello Webpack')\n\n//# sourceURL=webpack:///./src/index.js?"
);

编写 Loader#

简单实现#

下面开始编写 Loader,其实 Loader 就是一个普通函数

在根目录下创建loaders文件夹,然后创建一个replaceLoader.js文件,编写下面代码

module.export = function (source) {
  return source.replace("Webpack", "Hale");
};

这个 Loader 函数非常简单,接受源文件source作为参数,然后把源文件中的Webpack替换成Hale,并且返回替换后的值。

这里有两点需要注意。

  1. Loader 文件导出的模块只能是一个普通的函数,不能是箭头函数,因为后面还需要在普通函数中使用this对象,箭头函数的this指向的函数外面的作用域。
  2. Loader 函数必须返回一个字符串类型或者Buffer类型的值。

下面把我们编写的 Loader 引入到配置文件中

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: path.resolve(__dirname, "./loaders/replaceLoader.js"),
      },
    ],
  },
};

重新进行编译打包

yarn build

查看打包后的文件,在文件倒数几行里发现下面的代码,说明我们编写的 Loader 解析文件成功。

eval("console.log('Hello Hale')\n\n//# sourceURL=webpack:///./src/index.js?");

选项设置#

我们在使用 Webpack 的 Loader 时,有时候会传入options的参数来定制一些操作。比如

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: path.resolve(__dirname, "./loaders/replaceLoader.js"),
          options: {
            name: "Hale",
          },
        },
      },
    ],
  },
};

那么 Loader 是如何接受options传入的参数的呢?是给 Loader 函数传入第二个参数options吗?

答案是否定的,是通过this.query这样属性来获取参数的。

下面修改下 Loader 函数,通过this.query获取到name的值

module.export = function (source) {
  // return source.replace('Webpack', 'Hale')
  return source.replace("Webpack", this.query.name);
};

然后重新进行编译打包

yarn build

查看打包后的文件,在文件倒数几行里发现下面的代码,说明修改后的 Loader 解析文件成功。

eval("console.log('Hello Hale')\n\n//# sourceURL=webpack:///./src/index.js?");

callback 接口#

Webpack 给 Loader 函数提供了很多的 API,大部分都是通过this.xxx来调用的。

下面介绍下this.callback这个 API,它接受四个参数,分别是errcontentsourceMapmeta,使用this.callback可以抛出错误或者查看sourceMap

this.callback(
  err: Error | null, // 可能出现的错误
  content: string | Buffer, // 输出的内容
  sourceMap?: SourceMap, // sourceMap
  meta?: any // 其他配置
);

下面使用下这个 API

module.export = function (source) {
  const result = source.replace("Webpack", this.query.name);
  this.callback(null, result);
};

重新进行编译打包,编译成功。

异步函数#

有时候在 Loader 中可能需要编写异步代码,比如使用定时器setTimeout。先尝试使用编写下面代码

module.export = function (source) {
  const result = source.replace("Webpack", this.query.name);
  setTimeout(() => {
    this.callback(null, result);
  }, 3000);
};

重新进行编译打包,发现编译失败。这时候需要借助另外一个 API this.async来使用异步函数。

module.export = function (source) {
  const result = source.replace("Webpack", this.query.name);
  const callback = this.async();
  setTimeout(() => {
    callback(null, result);
  }, 3000);
};

重新进行编译打包,编译成功。

多个 Loader#

我们知道对于同一个类型的文件,可以使用多个 Loader 进行编译解析,而且 Loader 的调用顺序是从后到前、从右到左的,比如某个文件解析需要两个 Loader,它们的排序是[aLoader, bLoader],那么 Webpack 会先调用b Loader解析文件,然后再调用aLoader继续解析文件。

把现在带异步函数的 Loader 文件重新命名为replaceLoaderAsync.js,然后重新创建一个replaceLoader.js文件,编写下面代码

module.exports = function (source) {
  return source.replace("Hale", "World");
};

现在loaders文件夹下面就有两个自定义的 Loader 了,把它们都引入到 webpack 配置文件中

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve(__dirname, "./loaders/replaceLoader.js"),
          },
          {
            loader: path.resolve(__dirname, "./loaders/replaceLoaderAsync.js"),
            options: {
              name: "Hale",
            },
          },
        ],
      },
    ],
  },
};

然后重新进行编译打包

yarn build

查看打包后的文件,在文件倒数几行里发现下面的代码,说明我们编写的两个 Loader 都生效了。

eval("console.log('Hello World')\n\n//# sourceURL=webpack:///./src/index.js?");

分析:replaceLoaderAsyncLoader 排在后面,Webpack 会先调用它,把字符串Webpack替换成Hale,然后再调用replaceLoaderLoader,把字符串Hale替换成World

Loader 配置#

我们在配置第三方的 Loader 的时候,一般都不会导入一段的很长的路径,而是只填写 Loader 的名称即可。

自定义的 Loader 也能这样配置 Loader 吗?答案是肯定的,需要借助resolveLoader选项。

module.exports = {
  // ...
  resolveLoader: {
    modules: ["node_modules", "./loaders"],
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: "replaceLoader",
          },
          {
            loader: "replaceLoaderAsync",
            options: {
              name: "Deng",
            },
          },
        ],
      },
    ],
  },
};

我们resolveLoader选项,在modules选项中填入node_modules./loaders,这样 Webpack 就会先在node_modules中查找 Loader,如果找不到,再去./loaders文件夹中查找 Loader。

现在,重新进行编译打包,编译成功。

小结#

上面编写的 Loader 只是实现最简单的功能,让大家对 Loader 有一个简单的认识,其实 Loader 远不止这些功能,可以查看它的相关文档进行详细了解。