loader是用与对模块的源代码进行转换(处理),之前我们已经使用过很多loader,比如css-loader、style-loader、babel-loader等。

这里我们来学习如何自定义自己的loader:

  • loader本质上是一个导出为函数的JavaScript模块
  • loader runner库会调用这个函数,然后将上一个loader产生的结果或者资源文件传入进去

    创建自己的loader

我们可以在本地创建自己的loader,新建文件夹,创建loader文件
image.png
在webpack中配置引入

  1. const path = require("path");
  2. module.exports = {
  3. mode: "development",
  4. context: path.resolve(__dirname, "."),
  5. entry: "./src/main.js",
  6. output: {
  7. filename: "bundle.js",
  8. path: path.resolve(__dirname, "./build"),
  9. },
  10. module: {
  11. rules: [
  12. {
  13. test: /\.js$/i,
  14. use: "jujuul-loader01",
  15. },
  16. ],
  17. },
  18. resolveLoader: {
  19. modules: ["node_modules", "./jujuul-loader"],
  20. },
  21. };

这里通过 resolveLoader 这个配置,修改了 webpack 默认读取模块的配置,添加了我们本地新建的 jujuul-loader 文件夹,我们rules中的文件会按照resolveLoader中modules数组的顺序去各个文件夹中查找,比如我们rules中use使用了jujuul-loader01这个文件,首先在node_modules中查找,找不到再去jujuul-loader中查找。

resolveLoader属性

这个属性的作用就是指定解析loader的文件的位置

loader的执行顺序

在3个loader文件中暴露一个pitch方法,

  1. module.exports = function (content, sourcemap, meta) {
  2. console.log(content, "这是我们的loader01");
  3. return content;
  4. };
  5. module.exports.pitch = function () {
  6. console.log("loader pitch 01");
  7. };

在该方法中我们做一个打印
image.png
我们发现打印的顺序跟默认暴露方法中打印的顺序不同

这是因为webpack中有pitchLoader和normalLoader,我们平时调用的都是normalLoader,而对于pitchLoader,webpack有不同的处理,具体是webpack的loader-runner这个文件夹中的LoaderRunner.js文件,其中定义了一个变量 loaderIndex,初始值为0,每次执行webpack的loader会先执行一个递归方法,递归方法中每有一个pitchLoader,loaderIndex就加一,并且其是被递归调用的,然后会去调用normalLoader,normalLoader被调用后loaderIndex减一,也是递归调用,这个时候因为loaderIndex的值是从大到小,所以webpack处理的loader顺序也是从后到前(从数组最后执行到数组最前)

pitch和enforce

patch

  • run-loader先优先执行pitchLoader,在执行时会给变量loaderIndex做加一,loaderIndex++
  • run-loader之后会执行normalLoader,执行normalLoader时进行loaderIndex—
  • 而loaderIndex初始值为0,所以pitchLoader会按顺序执行,而normalLoader则是反序执行

那么,能不能改变它们的执行顺序呢?

可以,我们可以将其拆分为多个 rule 对象,通过 enforce 来改变它们的顺序

enforce

enfore一共有四种方式:

  • 默认所有的loader都是normal
  • 在行内设置的loader是inline
  • 也可以通过enforce设置 pre 和 post

在 Pitching 和 Normal 它们的执行顺序分别是:

  • post,inline,normal,pre
  • pre,normal,inline,post

    同步loader和异步loader

    同步loader

我们的loader必须有返回值,如果是同步返回,那么有两种返回方式

方式一:直接 return 返回

  1. module.exports = function (content, sourcemap, meta) {
  2. console.log(content, "这是我们的loader01");
  3. return content;
  4. };

方式二:通过 this.callback 返回

  1. module.exports = function (content, sourcemap, meta) {
  2. console.log(content, "这是我们的loader01");
  3. this.callback(null, content);
  4. };

异步loader

如果是异步返回的loader,需要处理之后才能实现异步

  1. module.exports = function (content, sourcemap, meta) {
  2. console.log(content, "这是我们的loader01");
  3. const callback = this.async();
  4. setTimeout(() => {
  5. this.callback(null, content);
  6. }, 3000);
  7. };

传入和验证参数

如果我们想要像其他的loader一样,传入一些参数,并且验证这些参数,那么可以这样做

传入参数

  1. 安装插件:

npm install loader-utils -D

  1. 配置我们要传入的值

    1. module: {
    2. rules: [
    3. {
    4. test: /\.js$/i,
    5. use: {
    6. loader: "jujuul-loader01",
    7. // 在这里配置要传入的值
    8. options: {
    9. name: "jujuul",
    10. age: "string",
    11. },
    12. },
    13. },
    14. ]
    15. }
  2. 引用插件并取值 ```javascript const { getOptions } = require(“loader-utils”);

module.exports = function (content, sourcemap, meta) { // …

// 获取传入的参数: const options = getOptions(this); console.log(“传入的参数”, options);

  1. // ...

};

这里有一点要注意,getOptions方法在 2.0.2 版本的插件中还存在,在最新版已经没有这个方法了
<a name="wOrnI"></a>
## 验证参数

1. 安装插件:

`npm install schema-utils -D`

2. 使用
```javascript
const { validate } = require("schema-utils");

module.exports = function (content, sourcemap, meta) {
  // ...

  // 验证传入的参数:
  validate(schema, options, {
    name: "jujuul-loader02",
  });

    // ...
};

自定义babel-loader案例

实现一个自定义 loader 的案例

  1. 首先,告知 webpack.config 使用我们自定义的 jujuulbabel-loader 来处理 js 文件

    {
    test: /\.js$/i,
     use: {
       loader: "jujuulbabel-loader",
         options: {
           presets: ["@babel/preset-env"],
         },
     },
    },
    
  2. 实现 jujuulbabel-loader ```javascript const babel = require(“@babel/core”); const { getOptions } = require(“loader-utils”);

module.exports = function (content) { // 0. 设置为异步loader this.callback = this.async(); // 1. 获取传入的参数 const options = getOptions(this);

// 2. 对源代码进行转换 babel.transform(content, options, (err, result) => { if (err) { this.callback(err); } else { this.callback(null, result.code); } }); };

需要注意的点:

1. 因为是实现类似于 babel-loader 的功能,所以需要引入 @babel/core,又因为要接收传值,所以引入 loader-utils ,使用 getOptions 接收值
1. 因为处理需要时间,所以需要将 loader 转为异步 loader
<a name="UcMd8"></a>
# 自定义md-loader
目标:<br />自定义一个 md-loader,实现对 md 文件的处理。<br />实现过程:

1. 首先,webpack配置解析md文件规则
```javascript
{
  test: /\.md$/i,
    use: [
      "jujuulmd-loader",
    ],
},
  1. 引入marked插件,实现对md文档的转换 ```javascript const marked = require(“marked”);

module.exports = function (content) { const htmlContent = marked.parse(content); return moduleCode; };


3. 页面中显示md转换出的html结构
```javascript
// 引入md并且赋值给code变量
import code from "./doc.md";

console.log("hello loader");

const message = "hello world";
console.log(message);

const foo = () => {
  console.log("foo");
};

foo();

// 在html文件中插入md
document.body.innerHTML = code;

自定义loader总结

没学以前以为很难,学了才知道原来这么简单。

首先是在webpack中配置规则,也就是你想要解析什么类型的文件,更具体点就是以什么作为后缀的文件,是css还是js,或者是png、md、html之类的。

{
  test: /\.md$/i,
    use: [
      "jujuulmd-loader",
    ],
},

然后就是指定它们使用什么loader,可以给这个loader传入一些额外的参数,当然,这一切的前提是必须先安装官方提供的 loader-utils 等工具插件。

然后就是在具体的 xxx-loader.js 文件中做处理。

需要暴露一个函数,函数的形参 content 就是传入的要处理的文件,然后对 content 做处理,比如 md-loader 中就是通过 marked 插件解析 content,再通过 highlight-js 插件对 js 代码做高亮处理,最后再把处理过后的 content 或者是 别的什么替代 content 的变量导出,就完成了。

const marked = require("marked");
const hljs = require("highlight.js");

module.exports = function (content) {
  marked.setOptions({
    highlight: function (code, lang) {
      return hljs.highlight(lang, code).value;
    },
  });
  const htmlContent = marked.parse(content);

  const innerContent = "`" + htmlContent + "`";
  const moduleCode = `var code=${innerContent};export default code;`;

  return moduleCode;
};