likes
comments
collection
share

日常开发,我该掌握哪些webpack loader知识

作者站长头像
站长
· 阅读数 15

前言

大家在使用webpack的时候,是不是经常接触webpack loader,每次copy一下loader配置或者从一些脚手架生成了自带的完整的webpack配置,导致很多时候我们就停留在了解或者知道这个东西,往往没有系统的去学习或者总结过webpack相关的内容,本篇聚焦于webpack loader,帮助大家系统的学习webpack loader相关的知识点,学完之后让你对webpack loader有一个相对完整的认知

在本篇文章中,我们将深入探讨webpack loader相关的知识,从为什么要设计loader到怎么具体在项目中使用loader,帮助大家掌握日常开发中的webpack loader相关知识

webpack是一个非常流行的JavaScript模块打包工具,使用它可以将多个JavaScript模块打包成一个或多个bundle文件。webpack loaderwebpack plugin不同的点在于,webpack plugin是聚焦webpack构建全流程的增强,而webpack loader则是聚焦构建流程中的资源转化这个点上,在我的看来,loader也可以看成是一种plugin,只不过与webpackplugin有区别;但是在rollup上我们就能看到其实就一个plugin概念是可以做所有事情的

本文将从以下几个方面介绍webpack loader

  • webpack loader是什么?帮助我们了解webpack为什么设计loader机制
  • webpack loader常用配置?帮助我们快速记住一些常用的loader配置
  • webpack loader执行顺序?帮助我们在使用loader组合,避免因为顺序出现错误
  • 项目内如何使用loader?帮助我们快速写出符合项目需求的webpack loader配置
  • 项目中常用的loader组合推荐:帮助我们积累日常loader组合使用方式
  • 编写自己的webpack loader: 带着大家手写一个完整的loader

本篇重点在于实践,也就是在于怎么用webpack loader,下一篇原理篇,主要讲webpack loader执行原理,及一些常用loader的原理

loader是什么

loader 本质上是导出函数的 JavaScript 模块,下面我们来具体看看为什么设计loader及为什么说loader本质上就是一个函数

为什么设计loader

webpack本质是是一个模块打包器,将js、css、image等资源 合并打包成一个bundle,一图描述webpack的作用,如下图所示 日常开发,我该掌握哪些webpack loader知识

那么webpack是怎么做到将所有的资源都打包合并到一起的呢?大致原理就是从entry入口模块开始,然后将entry模块解析成ast,然后在遍历ast,获取entry模块的依赖模块,如果有模块依赖则继续重复上述步骤,直到所有的依赖模块都包含进来,如下图所示 日常开发,我该掌握哪些webpack loader知识

但是这里有一个问题,就是webpack默认只能处理js、json这两种静态资源,比如cssimage直接由webpack处理就会报错,会中断构建流程,那么webpack为什么不内置支持cssimage这些静态资源的解析呢?原因还是webpack不想做这么多的事情,另外无法穷举所有需要被处理的资源类型

所以webpack 设计了loaderloader的作用就是将非js资源转换成webpack能够处理的资源,比如将css经过css-loader之后转换成module.exports = ".wrap: {color: red}",将image转换成module.exports = base64等,经过loader转化之后,这些资源就能够被webpack处理

怎么定义loader

在我个人看来,我认为loader只有两种类型,一种是normal loader,另一种就是picth loader,前者是为普通资源转化成webpack能够处理的资源准备的,后者是在前者的基础上拓展出来的新能力,是对前者的增强;至于inline loaderpre loaderpost loaderraw loader、同步 loader、异步loader 都是在这两种loader的基础上衍生出来的

比如在loader转化的时候,有一些比较耗时或者异步操作,又不想阻塞js代码执行,这时候就衍生出

  • 同步loader
  • 异步loader

比如normal loader接受的内容默认是字符串类型,我现在想接受的内容是buffer类型,这时候通过设置一个raw属性,来决定loader接受的内容,这时候就衍生出

  • raw loader

比如normal loader默认执行顺序是从右至左,从下至上,这时候我写在左边,或者写在上边,想改变默认的执行顺序,那么可以通过enforce: pre | post属性,改变loader的执行顺序,这时候就衍生出

  • pre loader
  • post loader

比如我们在处理某个资源的时候,需要准确指定的loader或者跳过loader,这时候就衍生出

  • inline loader

虽然上面有哪些多种loader的叫法,但是只要我们只要记住,我们在定义一个loader的时候,只有两种形式

// normal loader
module.exports = function normalLoader() {}
// pitch loader
module.exports.pitch = function pitchLoader() {}

下面围绕着这两种loader继续讲解

normal loader

同步 normal loader

// 直接return 返回结果
module.exports = function (content, map, meta) {
  return someSyncOperation(content);
};
module.exports = function (content, map, meta) {
  // 直接调用this.callback返回内容
  return this.callback(null, someSyncOperation(content), map, meta);
};

如果需要返回sourcemap or meta则需要通过this.callback这种方式返回结果

异步 normal loader

loader最初的设计就只是同步loader,但是javascript引擎是单线程,为了尽可能的提升webpack的构建性能,支持异步loader,通过异步非阻塞的方式来转换资源

module.exports = function (content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function (err, result) {
    if (err) return callback(err);
    callback(null, result, map, meta);
  });
};

raw loader

loader默认接受到的contentstring,而对于一些图片场景,则需要接受buffer的形式,所以loader通过raw参数来控制content接受的数据类型

module.exports = function (content) {
  assert(content instanceof Buffer);
  return someSyncOperation(content);
  // return value can be a `Buffer` too
  // This is also allowed if loader is not "raw"
};
module.exports.raw = true;

pitch loader

为什么设计pitch loader

为什么要设计pitch loader,难道normal loader不能满足业务诉求吗?

我们先回看下loader设计的初衷,是将webpack不能处理的资源比如css等资源转换成webpack能够处理的资源,更多的是一种资源形态的转换,比如css => jses6+ => es5ts => js,不会对原始的代码做过多的新增能力的操作,但是有一些场景,我们可能需要在每个loader转换的时候新增一些功能性或者适配性的代码,以满足业务诉求,那么这时候有两种思路:

  1. 直接在现有的loader里面新增功能性or适配性代码
  2. 采用一种新的形式来对现有的代码做增常能力,且不与当前的loader进行功能耦合

作者最后采用了第二种思路,新增一种pitch loader的概念,类似与dom事件模型中的捕获,先执行pitch loader,然后在执行normal loader,且如果picth loader有返回非undefined的值,则直接中断后续loader的执行,这样就方便对原始模块一些增强功能,而同时不需要改动到原始模块代码

看下webpack作者对于设计picth loader的动机的回答Documentation for pitch vs normal loader isn't very clear

You can/should use it if you don't want to modify the source code, but just insert a module in front for the module. i. e. the bundle-loader uses this. 如果你想不修改原代码,且又想在当前模块之前插入一些模块或者内容,那么你就可以使用pitch loader

我们再来看下作者提到的bundle-loader代码实现

var loaderUtils = require("loader-utils");

module.exports = function() {};
module.exports.pitch = function(remainingRequest) {
	...
	var result = [
			"module.exports = function(cb) {\n",
			"	require.ensure([], function(require) {\n",
			"		cb(require(", loaderUtils.stringifyRequest(this, "!!" + remainingRequest), "));\n",
			"	}" + chunkNameParam + ");\n",
			"}"];
	return result.join("");
}

picth loader相对normal loader

  • 在导出默认方法上不同,picth loader需要明确导出pitch方法
  • 在传入的参数不同,normal loader传入了内容,pitch loader传入的是loader链路

bundle-loader定义了picth 方法并且在picth 方法内返回了非undefiend值,那么来看下具体的执行顺序,及最终的返回结果

原始代码

import bundle from './file.bundle.js';

bundle((file) => {
  file.add();
});

./file.bundle.js这个js文件,变成懒加载的文件,并且在./file.bundle.js加载完之后,可以直接使用./file.bundle.js暴露出来的属性or方法

bundle-loader是这样做的

// 经过bundle-loader处理之后就变成了,./file.bundle.js 会包裹一层如下代码
module.exports = function (cb) {
  require.ensure([], function(require){
    cb(require(loaderUtils.stringifyRequest(this, "!!" + remainingRequest)))
  }
}

// 相当于 bundle这个函数变成了
function (cb) {
  require.ensure([], function(require){
    cb(require(loaderUtils.stringifyRequest(this, "!!" + remainingRequest)))
  }
}
// 加上"!!" 确保剩下的loader可以正常处理,且禁用rules中匹配到的所有loader
require(loaderUtils.stringifyRequest(this, "!!" + remainingRequest)) 

所以整体逻辑就是,在匹配到./file.bundle.js 这个js文件之后,bundle-loader的picth会直接阻断,并返回一层包裹之后的代码,而由于包裹的代码内继续包含了require('!!生效loader!./file.bundle.js')webpack在第一次通过loader处理完./file.bundle.js之后,然后对内容解析成ast,然后遍历ast,发现reuqire./file.bundle.js,则直接使用匹配到的行内loader进行处理,最终./file.bundle.js 文件变成了懒加载

用图表示,如下所示 日常开发,我该掌握哪些webpack loader知识

整体看下来,在不改变原有'./file.bundle.js'模块内部代码的条件下,改变了'./file.bundle.js'模块的加载方式

当然除了这个作用之外,在我看来还有对现有loader的功能增强,我们继续往下看

同步 picth loader

// 直接return 返回结果
module.exports.picth = function (remainingRequest) {
  return `require(", loaderUtils.stringifyRequest(this, "!!" + remainingRequest), "))`;
};
module.exports.picth = function (remainingRequest) {
  // 直接调用this.callback返回内容
  return this.callback(null, `require(", loaderUtils.stringifyRequest(this, "!!" + remainingRequest), "))`);
};

异步 picth loader

module.exports.picth = function (remainingRequest) {
  var callback = this.async();
  someAsyncOperation(content, function (err, result) {
    if (err) return callback(err);
    callback(null, `自定义操作`);
  });
};

pitch loader vs normal loader

现在对pitch loader已经有了更深入的了解,那我们应该知道pitch loadernormal loader针对的场景不一样,normal loader充当的是一个资源转换的角色,不对输入的源代码做一些额外的功能,而picth loader更多的是充当一个对源代码添加一些额外功能的角色或者对现有normal loader功能的增强,比如将模块替换成懒加载模块,比如将css资源通过style标签掺入html

到这里我们知道,loader本质上是导出函数的 JavaScript 模块,无论是normal loader还是pitch loader

loader常用配置

了解了loader的定义形式之后,我们看看loader常用的配置,以webpack提供的类型说明来讲

匹配规则配置

Rule.test

作用:匹配资源,一般用于粗略过滤 取值:string | RegExp | ((value: string) => boolean)

比如匹配所有的css文件,从入口出发找到的所有css相关文件都会匹配到该条rule

{
  module: {
    rules: [
      {
        test: /\.css$/,
      },
      {
        test: function (resource) {
          return resource.indexOf('.css') > -1
      	},
      },
    ]
  }
}

Rule.exclude && Rule.include

作用:对test匹配规则的补充,精确匹配 取值:string | RegExp | ((value: string) => boolean)

比如不处理node_modules下的js文件

{
  module: {
    rules: [
      {
        test: /\.[tj]sx?$/i,
        exclude: /node_modules/,
      },
      {
        test: /\.[tj]sx?$/i,
        exclude: function (resource) {
          return resource.indexOf('node_modules') > -1
      	},
      },
    ]
  }
}

比如不处理node_modules下的js,但是不包括react-abcreact-bbb

{
  module: {
    rules: [
      {
        test: /\.[tj]sx?$/i,
        exclude: /node_modules\/(?!react-aaa|react-bbb)/,
      }
    ]
  }
}

比如只处理src下的css文件

{
  module: {
    rules: [
      {
        test: /\.css$/i,
        include: /xxx\/src/,
      },
      {
        test: /\.css$/i,
        include: function (resource) {
          return resource.indexOf(path.join(__dirname, 'src')) > -1
      	},
      },
    ]
  }
}

Rule.resourceQuery

作用:对test匹配规则的补充,精确匹配 取值:string | RegExp | ((value: string) => boolean)

比如只处理url上包含addAttrcss文件

{
  module: {
    rules: [
      {
        test: /\.css$/i,
        resourceQuery: /addAttr/,
      }
    ]
  }
}

loader配置

Rule.use

作用:设置loader,设置该条rule作用的loader 取值:string | RuleSetUseItem[] | ((data: any) => RuleSetUseItem[])

比如设置处理.cssloader

{
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: 'css-loader' // 字符串形式
      },

      {
        test: /\.css$/i,
        use: ['css-loader'] // 数组形式
      },

      {
        test: /\.css$/i,
        use: { // 对象形式
          loader: 'css-loader',
          options: {}
        }
      },

      {
        test: /\.css$/i,
        use: [{ // 数组对象形式,建议这种方式,因为在webpack里面最终都会转化成这种方式
          loader: 'css-loader',
          options: {}
        }]
      },
    ]
  }
}

Rule.oneOf

作用:设置loader,设置该条rule可以根据不同的条件用不同的loader 取值:RuleSetRule[]

比如我想对css文件做区分处理

{
  module: {
    rules: [
      {
        test: /\.css$/,
        oneOf: [
          {
            resourceQuery: /no-postcss/, // foo.css?no-postcss
            use: ['css-loader']
          },
          {
            use: ['css-loader', 'postcss-loader']
          }
        ]
      }
    ]
  }
}

其它配置

Rule.type

作用:设置资产的处理方式 取值:string

比如在webpack5里面处理.png等这样的静态资源

module: {
  rules: [
   {
     test: /\.png/,
     type: 'asset/resource',
     parser: {
       dataUrlCondition: {
         maxSize: 4 * 1024 // 4kb
       }
     },
     generator: {
       filename: 'static/[hash][ext][query]'
     }
   }
  ]
},

Rule.enforce

作用:控制loader的执行顺序 取值:"pre" | "post"

{
  module: {
    rules: [
      {
        test: /\.css$/,
        enforce: 'pre',
        use: [
          {
            loader: 'css-loader',
          },
        ]
      }
    ]
  }
}

loader 执行顺序

在了解loader的执行顺序之前,一定要弄清楚为什么loader需要有执行顺序? 比如我们需要将less资源转换成webpack能够识别的资源,我们可能需要将less先转换成css、然后在对css进行相应的处理最后转换成能识别的资源,如果我们在一个loader里面把事情都做完,当然最好,那么就不需要与其它loader配合使用,那也就不存在执行顺序的问题,但问题是,我们不仅只考虑less的场景,比如还有sassstyules等等,所以webpck loader遵循单一职责原则,一个loader只做一件事情,然后整个工作由不同的loader协助完成,即降低了单个loader的复杂度,也提升了loader的可维护性

既然一件工作需要多个loader配合使用,那么loader之间的执行顺序就不能有问题,如果顺序颠倒,则会导致最终生成的资源不符合预期,或者在执行的过程中报错,所以保证loader的执行顺序很重要

loader的执行顺序只需要牢记3点即可:

  1. 相同优先级normal loader的执行顺序是从右到走,从下到上
  2. 不同优先级normal loader的执行顺序是pre > normal > inline > post
  3. pitch loadernormal loader的执行顺序完全相反

执行顺序例子

都包含normal loader与pitch loader,且pitch loader无返回非undefined值

use: ['a-loader', 'b-loader', 'c-loader'],

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

具体流程如下图所示 日常开发,我该掌握哪些webpack loader知识

都包含normal loader与pitch loader,且pitch loader无返回非undefined值,且pitch b-loader返回非undefined值

use: ['a-loader', 'b-loader', 'c-loader'],

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    return (
      'module.exports = {}'
    );
  }
};

|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution

具体流程如下图所示 日常开发,我该掌握哪些webpack loader知识

有pre 及 post loader,且pitch loader无返回非undefined值

use: ['a-loader'],
enforce: 'pre',

use: ['b-loader'],

use: ['c-loader'],
enforce: 'post',

|- c-loader `pitch`
  |- b-loader `pitch`
    |- a-loader `pitch`
      |- requested module is picked up as a dependency
    |- a-loader normal execution
  |- b-loader normal execution
|- c-loader normal execution

具体流程如下图所示 日常开发,我该掌握哪些webpack loader知识 更多顺序相关的知识,只要自己写一个demo跑一下就知道了

项目内如何使用loader

好了我们已经知道了loader的执行顺序,那么看下项目中我们可以怎么组合使用loader

集中式

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      },
    ],
  },
}

OR

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [{
          loader: 'style-loader'
        }, {
          loader: 'css-loader'
        }, {
          loader: 'less-loader'
        }]
      },
    ],
  },
}

处理同一类资源的loader,放在一条rule里面

分布式



module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.(css|less)$/,
        use: 'style-loader',
      },
      {
        test: /\.(css|less)$/,
        use: 'css-loader',
      },
      {
        test: /\.less$/,
        use: 'less-loader',
      },
    ],
  },
}

处理同一类资源的loader,放在多条rule里面

内联式

webpack允许在引入模块的时候直接指定loader,这样指定loader的方式称之为inline loader,具体如下所示

import common from 'loader-a!loader-b!loader-c?type=abc!./common.js'

每个loader之前同!隔开,允许携带query参数,最后的模块也使用!隔开

loader的执行顺序与rules中定义的loader执行顺序保持一致

当通过inline loader方式引入的模块,同时命中rules中的loader时,默认场景下二者都会执行,且normal loader执行顺序是 normal > inline,当然允许通过增加前缀来控制rules中的loader是否执行

  • 使用前缀,禁用rules中匹配到的normal loader
import common from '!loader-a!loader-b!loader-c?type=abc!./common.js'
  • 使用!!前缀,禁用rules中匹配到的所有loader
import common from '!!loader-a!loader-b!loader-c?type=abc!./common.js'
  • 使用-!前缀,禁用rules中匹配到的所有pre loadernormal loader
import common from '-!loader-a!loader-b!loader-c?type=abc!./common.js'

其实集中式、分布式或者内联式,并无哪种更好哪种不好,主要还是看项目,及最终的需求

项目中常用的loader组合推荐

处理js && ts

推荐直接使用babel-loader处理ts,而不是使用ts-loader处理ts,之所以推荐babel-loader处理快之外(因为ts-loader相对于babel-loader多了一层类型检查),我们可以在vscode中使用typescript类型检查,同时也可以通过git hook在代码提交的时候进行类型检查,所以推荐直接使用babel-loader即可

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.[tj]sx?$/i,
        use: [
          {
            loader: 'babel-loader'
          }
        ],
      },
    ],
  },
}

不推荐直接使用eslint-loader 原因:开发体验不好,很容易中断构建流程,推荐使用vscode编辑器在开发的过程中进行eslint检查,或者通过husky+lint-statge的方式进行eslint检查

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.[tj]sx?$/i,
        use: [
          {
            loader: 'eslint-loader'
          }
        ],
      },
    ],
  },
}

对于大项目,排查之后,定位到是babel-loader耗时比较长的问题,我们可以使用多进程、swcesbuild等方式优化,如下所示 推荐1:thread-loader + babel-loader组合

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.[tj]sx?$/i,
        use: [
          {
            loader: 'thread-loader',
          },
          {
            loader: 'babel-loader'
          }
        ],
      },
    ],
  },
}

推荐2: 使用swc-loader替换babel-loader

module: {
 rules: [
  {
    test: /\.[tj]sx?$/i,
    exclude: /(node_modules)/,
    use: {
      // `.swcrc` can be used to configure swc
-      loader: "babel-loader",
+      loader: "swc-loader",
+    	 options: {
        	jsc: {
            parser: {
              syntax: "typescript",
              tsx: true,
              decorators: true,
            },
            transform: {
              legacyDecorator: true,
            },
            externalHelpers: true, // 注意这里设置true时,需要在项目下安装@swc/helpers
            target: 'es5',
          },
          env: {
            targets: "last 3 major versions, > 0.1%", // 根据项目设置
            mode: "usage",
            coreJs: "3" // 根据项目选择
          },
          isModule: 'unknown'
       }
    }
  }
  ]
}

推荐3: 使用esbuild-loader替换babel-loader

module: {
  rules: [
-   {
-     test: /\.js$/,
-     use: 'babel-loader'
-   },
+   {
+     test: /\.[tj]sx?$/i,
+     loader: 'esbuild-loader',
+     options: {
+       // JavaScript version to compile to
+        target: 'es2015',
+        loader: 'tsx',
+      }
+    },

    ...
  ],
},

更详细的swc-loaderesbuild-loader使用方式可以参考swc与esbuild-将你的构建提速翻倍

处理样式

对于样式的处理,主要涉及到

  • css-loader:将css资源转化成webpack能够处理的资源
  • style-loader:用于开发环境将css-loader处理后的样式通过style等方式插入到html
  • mini-css-extract-plugin.loader: 用于生成环境构建时将css-loader处理后的样式抽离到单独的文件中
  • postcss-loader: 用于处理css,将css添加前缀,支持css新语法等
  • less-loader: 将less转化为css-loader能够处理样式
  • sass-loader: 将sass转化为css-loader能够处理样式

在我们知道了处理样式相关的loader之后,那么我们一般的使用组合如下所示

  • sass-loader > postcss-loader > css-loader > (mini-css-extract-plugin.loader or style-loader)
  • postcss-loader > css-loader > (mini-css-extract-plugin.loader or style-loader)
  • css-loader > (mini-css-extract-plugin.loader or style-loader)

一般项目配置如下所示

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.(css|less|s[a|c]ss)(\?.*)?$/i,
        use: [
          {
            loader: process.env.NODE_ENV === 'development' ? 'style-loader' : miniCssExtractPlugin.loader 
          },
        	{
            loader: 'css-loader'
          },
        	{
            loader: 'postcss-loader'
          },
        	{
            loader: 'less-loader'
          },
        ],
      },
    ],
  },
}

或者这样写

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.(css|less|s[a|c]ss)(\?.*)?$/i,
        use: [
          {
            loader: process.env.NODE_ENV === 'development' ? 'style-loader' : miniCssExtractPlugin.loader 
          },
        ],
      },
      {
        test: /\.css$/i,
        use: [
        	{
            loader: 'css-loader'
          },
          {
            loader: 'postcss-loader'
          },
        ],
      },
      {
        test: /\.less$/i,
        use: [
          {
            loader: 'css-loader'
          },
          {
            loader: 'postcss-loader'
          },
        	{
            loader: 'less-loader'
          },
        ],
      },
      {
        test: /\.s[a|c]ss$/i,
        use: [
          {
            loader: 'css-loader'
          },
          {
            loader: 'postcss-loader'
          },
        	{
            loader: 'sass-loader'
          },
        ],
      },
    ],
  },
}

你要你清楚了每个loader的作用及loader的执行顺序,那么自己看着组合就行

处理图片

webpack5已经内置了图片的处理能力,所以我们一般只需要image-webpack-loader这个压缩图片的插件就可以

具体配置如下

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 10kb
          },
        },
        generator: {
          filename: 'image/[name].[hash][ext]',
        },
        use: [
        	{
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                enabled: false,
              },
              gifsicle: {
                interlaced: false,
              },
              optipng: {
                optimizationLevel: 7,
              },
              pngquant: {
                quality: [0.65, 0.9],
                speed: 4,
              }
            },
          },
        ],
      },
    ],
  },
}

编写自己的webpack loader

到这里我们已经知道,webpack loader只有两种写法normal loaderpicth loader,所以我们在写自己的loader的时候可以如下分析

  • 写这个loader的作用是什么,根据自己的需求确认是normal loader还是picth loader,还是二者相结合
  • loader内的逻辑,是不是涉及到网络请求、文件IO等比较耗时的操作,如果是推荐使用异步loader,反之使用同步loader
  • normal loader确认是否接受到的内容是buffer类型,如果是则设置raw属性,反之不需要设置raw属性
  • 最后就是实现具体的业务逻辑

markdown-loader

需求:在markdown中直接渲染react组件,并同时能够调试react组件,类似dumi文档那样,又不需要那么多能力,所以自己简单写一个

不依赖现有loader,直接定义成normal loader,然后借助@mdx-js/mdx实现markdown中直接渲染react组件的能力,@mdx-js/mdx处理markdown可能有点耗时,所以选择异步loader

loader context

loader提供了上下文,供loader在转换资源的时候可以做更多的事情,常用的loader上下文字段

  • this.addDependency()webpack模块图中添加新的依赖项
  • this.async() 获取异步callback具柄
  • this.cacheable() 开启loader缓存
  • this.callback() 返回loader结果
  • this.data 获取loader之前传递的数据
  • this.emitError() webpack输出一个错误
  • this.emitFile()webpack 最终的assets列表中输出一个新的asset
  • this.fs 获取webpack内部的inputFileSystem
  • this.getOptions() 获取loader传入参数
  • this.getResolve() 获取新的module resolve 函数
  • this.query 获取loader?后面的query参数
  • this.request 获取当前包含所有loader的绝对路径
  • this.resolve() 获取模块绝对路径
  • this.resource 获取当前被loader处理的模块绝对路径,包含query参数
  • this.resourcePath 获取当前被loader处理的模块绝对路径,不包含query参数
  • this.resourceQuery 获取当前被loader处理的模块绝对路径上的query参数
  • this._module 获取当前被loader处理的module实例对象
  • this._compilaction 获取当前webpack构建过程中的compilaction对象
  • this._compiler 获取当前webpack构建过程中的compiler对象

实现伪代码

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import frontmatter from 'remark-frontmatter';

import codePlugin from './plugin/code';
import yaml from './plugin/yaml';
import sourceCode from './plugin/sourceCode';
import removeComment from './plugin/removeComment';

function processMdx(code: string, options: { filename: string }) {
  const { filename } = options;
  const importMap = new Map();
  return unified()
    .use(remarkParse)
    .use(sourceCode)
    .use(codePlugin)
    .process(code)
      return {
        code: res.value,
        data: res.data,
      };
    });

}

export function loader(value: string) {
  const defaultConfig = {
    jsxRuntime: 'classic',
    format: 'mdx',
    remarkPlugins: [remarkGfm, math],
    rehypePlugins: [deleteSemicolon, addWrap, mathjax, table, p, slug, headingLink, link, img, [meta, { filename: this.resourcePath }], rehypePrism],
  };

  // 调用this.async()获取callback具柄,让normal loader成为异步loader
  const callback = this.async();

  const defaults = this.sourceMap ? { SourceMapGenerator, ...defaultConfig } : { ...defaultConfig };

  // 获取传入的参数
  const options = this.getOptions();

  // 对markdown语法进行对应的转化
  processMdx(value, { filename: this.resourcePath} )
    .then(({ code, data }: { code: string; data: any}) => {
      return compile({ value: code, path: this.resourcePath }, { ...defaults, ...options }).then(
        (file) => {
          // 通过callback具柄返回当前loader处理之后的内容
          callback(null, file.value, file.map);
        },
      );
    }).catch(callback);

}

使用

{
  module: {
    rules: [
      {
        test: /\.mdx?$/i,
        use: [{ // 数组对象形式,建议这种方式,因为在webpack里面最终都会转化成数组形式
          loader: 'babel-loader',
          options: {}
        },{
          loader: path.join(__dirname, './markdown-loader.js'),
          options: {}
        }]
      },
    ]
  }
}

总结

日常开发掌握以下知识点就足以熟练使用webpack loader

loader的作用就是资源转化。loader可以转换各种类型的资源,并在 webpack 构建过程中让webpack可以正确使用这些资源。

loader 分为normal loaderpicth loader。然后又根据不同的场景进行衍生,衍生出了inline loaderraw loaderpre loaderpost loader,但是本质上还是normal loaderpicth loader

normal loader 执行顺序是从左往右,从下往上。每个 loader 接收前一个 loader 的输出作为输入,并返回自己的输出,直到所有 loader 都被调用并返回结果为止。

使用enforce书写可以改变loader的执行顺序,最终的执行顺序以normal loader为例为pre > normal > inline > post

loader遵循单一职责原则,要在项目中使用 loader,一般需要组合使用loader,在使用loader之前我们需要清楚每个loader的职责,然后根据职责组合使用

pitch loader具有阻断能力,用于模块功能增强及loader功能增强

raw loader用于声明normal loader接受的内容要是buffer类型,而不是默认的string类型

感谢各位看官老爷耐心看完,如果觉得对看官老爷有帮助,动动手指头点个👍吧!

转载自:https://juejin.cn/post/7246056370626068535
评论
请登录