一文讲清楚 Loader 的调用链

一、前言

在上一篇 《手把手教你实现 Loader》 中已经介绍了 loader 的基础知识以及如何自定义 loader。这篇文章主要聊聊 loader 的调用链,从这篇文章中,你将了解到以下内容:

  • loader 的 normal & pitch 阶段
  • loader 在不同阶段的执行顺序
  • pitch 的应用(style-loader)

相关代码基于上篇文章,建议优先阅读上一篇。文章如有误,欢迎指出,大家一起学习交流~。 项目地址,期待大家的一键三连 💗

二、loader的两大阶段

一个 loader 可以分为两个阶段: 一个是 normal 阶段 ,另一个是 pitch 阶段。不同阶段对应的代码如下:

// a-loader.js
module.exports = function (source, sourceMap, data) { 
    // normal 阶段
}

// pitch 阶段 
module.exports.pitch = function (remainingRequest, previousRequest, data) {}

pitch 阶段的执行时期会早于 normal 阶段。那么具体的执行顺序是怎么样的呢?下面我们分开来聊一聊。

三、loader 的 normal 阶段

在开始之前,我们先来聊聊 enforce 这个属性 ,在配置文件中,可以通过 enforce 属性定义 loader 的类型, enforce 属性的取值为 pre || post ,没有则视为普通( normal)的 loader 。即官方提到的 pre-loaderpost-loadernormal-Loader, 顾名思义,它们执行顺序为 pre → normal → post

理论不如实战,我们还是基于上一个项目来实战一下。在 xq-loader 的文件夹下创建三个文件 a-loader.jsb-loader.jsc-loader.js

在这里插入图片描述

分别在每个文件中填充内容,并进行打印。

    // a-loader.js
    module.exports = function (source) {
      console.log('a-loader')
      return source
    }
    
    // b-loader.js
    module.exports = function (source) {
      console.log('b-loader')
      return source
    }
    
    // c-loader.js
    module.exports = function (source) {
      console.log('c-loader')
      return source
    }
    
    // my-async-loader.js
    ...// 省略
    module.exports = function (source) {
      console.log('my-async-loader')
      // 通过 this.async 来返回一个异步函数,第一个参数 Error, 第二个参数是处理的结果。
      const callback = this.async()
      fs.readFile(path.join(__dirname, '../async.txt'), 'utf-8', (err, data) => {
        const html = `module.exports = ${JSON.stringify(data)}`
        callback(null, html)
      })
    }

接着在配置文件中,配置着三个loader。为了方便理解 loader 的执行顺序,减少项目中其他 loader 的影响,我们用这三个 loader 来处理 txt 文件,并使用 enforce 属性进行标识 。因 webpack 不能识别 txt 文件,所以我们保留之前的 my-async-loader 调用。

    module.exports = {
      ...// 省略
      module: {
    	  rules: [
    	    ...// 省略
    		   {
    	       test: /\.txt$/,
    	       use: {
    	         loader: 'my-async-loader',
    	       },
           },
          // start ---Loader 顺序----
          {
            test: /\.txt$/,
            use: {
              loader: 'a-loader',
            },
            enforce: 'pre',
          },
          {
            test: /\.txt$/,
            use: {
              loader: 'c-loader',
            },
            enforce: 'post',
          },
          {
            test: /\.txt$/,
            use: {
              loader: 'b-loader',
            },
          },
          // end ---Loader 顺序----
    	  ]
      }
    } 

配置完毕后, pnpm run build 打包之后,可以看到控制台的打印如下:
在这里插入图片描述

可以验证前面的执行顺序 pre → normal → post ,那么对于未使用 enforce 字段标识的 b-loadermy-async-loader 的执行顺序是啥呢?咱们可以记个口诀,对于相同类型的 loader 的执行顺序是自下而上,从左到右。

为了更好的理解这个口诀,我们再增加一个普通的 d-loader.js, 调整一下配置:

    module.exports = {
      ...// 省略
      module: {
    	  rules: [
    	    ...// 省略
    		   {
    	       test: /\.txt$/,
    	       use: {
    	         loader: 'my-async-loader',
    	       },
           },
          // start ---Loader 顺序----
          {
            test: /\.txt$/,
            use: {
              loader: 'a-loader',
            },
            enforce: 'pre',
          },
          {
            test: /\.txt$/,
            use: {
              loader: 'c-loader',
            },
            enforce: 'post',
          },
          {
            test: /\.txt$/,
            use: [
              {
                loader: 'd-loader',
              },
              {
                loader: 'b-loader',
              },
            ],
          },
          // end ---Loader 顺序----
    	  ]
      }
    }

此时我们将三个普通的 loader 抽象出来, 可以将它们理解为一个二维数组,大致是这样:

my-async-loader
d-loaderb-loader

套用一下口诀,执行顺序应该是 b-loader → d-loader → my-async-loader ,打包编译下:
在这里插入图片描述

没错,符合预期,看起来这个口诀还是挺管用的呢!

除了 enforce 字段可以决定 loader 的执行顺序,还有一种内联 import 的写法也可以改变loader 的执行顺序,让我们来尝试下这种内联写法。调整下 d-loader 的位置,先将它从配置文件中移除,然后在引入.txt 的 index.js 文件中通过 import 的方式引入它。多个 loader 之间的连接可以通过 来进行分割。

    // index.js
    import txt from 'd-loader!./async.txt'
    ...// 省略 

我们再来看一看增加了这种行内(inline)写法之后的 loader 执行顺序如下,即:pre → normal → inline → post 。
在这里插入图片描述

行内写法也可以通过添加前缀的方式来改变 loader 的执行顺序,一共有 3 种前缀的语法:

  • ! : 忽略已配置的 normol-loader
  • -!:忽略已配置的 pre-oader、normol-loader
  • !!:忽略已配置的 (pre、normol、post) loader

让我们来实践一下 ,先整理下 webpack.config.js 中的配置。为了更好的测试,将 my-async-loader放到行内写法中,保留 a-loaderb-loaderc-loader


// config.js 配置
module.exports = {
	...//
	module: {
		rules: [
			// start ---Loader 顺序----
	      {
	        test: /\.txt$/,
	        use: {
	          loader: 'a-loader',
	        },
	        enforce: 'pre',
	      },
	      {
	        test: /\.txt$/,
	        use: {
	          loader: 'c-loader',
	        },
	        enforce: 'post',
	      },
	      {
	        test: /\.txt$/,
	        use: [
	          {
	            loader: 'b-loader',
	          },
	        ],
	      },
      // end ---Loader 顺序----
		]
	}
}
  • ! 前缀

     import txt from '!d-loader!my-async-loader!./async.txt'
    

    执行结果:a-loader → my-async-loader → d-loader → c-loader
    在这里插入图片描述

  • -! 前缀

      import txt from '-!d-loader!my-async-loader!./async.txt'
    

    执行结果:my-async-loader → d-loader → c-loader
    在这里插入图片描述

  • !! 前缀

     import txt from '!!d-loader!my-async-loader!./async.txt'
    

    执行结果: my-async-loader → d-loader
    在这里插入图片描述

3.1 loader-normal 的参数

normal 阶段接收三个参数:

  • source

    表示需要处理的源文件的内容。对于第一个loader 来说,source 是源文件,对于后续的 loader 来说是前一个loader 执行之后的结果。

        // a-loader.js
        module.exports = function (source, sourceMap,data) {
          console.log('a-loader')
          return source + "a-loader"
        }
    
  • sourceMap

    可选参数,代码的sourceMap

  • data

    可选参数,其他需要在loader 链中传递的信息。比如这个posthtml-loader 使用了data 来传递ast 信息。

       module.exports = function (source, sourceMap, data) {
          console.log('a-loader')
          if (!data) {
            data = {}
          }
          data.name = 'xiaoqi'
          this.callback(null, source, sourceMap, data)
        } 
    

到这里我们就聊完了loader的 normal 阶段的执行顺序,接下来我们来了解下 loader 的 pitch 阶段的执行顺序。

四、loader 的 pitch 阶段

对于一个 loader ,可以用 module.exports 来导出一个函数外(normal 阶段), 也可以用 module.exports.pitch 来导出(pitch 阶段)一个函数。前面说过对于 normal 阶段来说同一类型的 loader 可以套用口诀,自下而上,从右到左,不同类型遵循 pre → normal → inline → post 。 那么对于 pitch 阶段来说反过来即可。

我们来实践一下,先更改下配置,每个 loader 中导出 pitch 函数:

 import txt from 'my-async-loader!./async.txt' // inline loader 

 // config.js 配置
 module.exports = {
 	...//
 	module: {
 		rules: [
 			// start ---Loader 顺序----
 	      {
 	        test: /\.txt$/,
 	        use: {
 	          loader: 'a-loader',
 	        },
 	        enforce: 'pre',
 	      },
 	      {
 	        test: /\.txt$/,
 	        use: {
 	          loader: 'c-loader',
 	        },
 	        enforce: 'post',
 	      },
 	      {
 	        test: /\.txt$/,
 	        use: [
 	           {
             loader: 'd-loader',
 	          },
 	          {
             loader: 'b-loader',
 	          }, 
 	        ],
 	      },
       // end ---Loader 顺序----
 		]
 	}
 }

 // a-loader.js  其他loader文件类似
 module.exports = function (source) {
   console.log('a-loader')
   return source
 }

 module.exports.pitch = function () {
   console.log('a-pitch-loader')
 }

最终打印的结果如下:
在这里插入图片描述
可以看到,pitch 阶段先执行, normal 阶段后执行,pitch 阶段正好和 normal 阶段相反。

4.1 pitch 的熔断作用

上述的例子中,在 ptich 阶段我们都没有任何的返回操作,那如果在 pitch 阶段有返回值,loader 的执行顺序是什么样呢?

loader 的调用过程:
在这里插入图片描述
如果 pitch 过程中有非 undefined 的返回值时,将熔断 loader 的调用链,跳过后续的 loader ,将结果传递给前一个loader 。

在这里插入图片描述

d-loader 的pitch 阶段返回一个值:

    // d-loader.js
    module.exports.pitch = function () {
      console.log('d-pitch-loader')
      return 1
    }

执行结果如下,直接熔断后续 loader 的调用:
在这里插入图片描述
常见的 style-loader 、vue-loader 都用到了pitch 阶段的熔断作用来实现。

4.2 loader - pitch 的参数

ptich 方法接受3个参数:

  • remainingRequest

    表示剩余未处理的loader的绝对路径和资源文件的绝对路径 用 !分割 组合成的字符串。

    对于上面的调用链图来说,以 my-async-loader 为例,remainRequest 表示的是右侧这部分的 loader的绝对路径+资源文件的绝对路径,即:xxx/d-loader.js!xxx/b-loader.js!xxx/a-loader.js!xxx/.txt
    在这里插入图片描述

  • previousRequest

    表示已经处理的loader的绝对路径用 !分割 组合成的字符串。

    还是以 my-async-loader 为例,previousRequest的是左侧这一部分的 loader 的绝对路径,即: xxx/c-loader.js

    在这里插入图片描述

  • data

    normal 阶段与pitch阶段之间的数据交互可以用 data 对象来传递,默认是一个空对象 {}。

    当在一个 loader 的 pitch 阶段中对其 data 参数做处理后,在 normal 阶段可以通过 this.data 进行获取。

        // my-async-loader.js
        module.exports = function (source) {
          console.log('my-async-loader', this.data.id) // 获取this.data.id
          ...// 省略
        }
        module.exports.pitch = function (remainingRequest, previousRequest, data) {
          data.id = '010101'
          console.log('my-async-pitch-loader')
          console.log('my-async-pitch-loader-remainingRequest:', remainingRequest)
          console.log('my-async-pitch-loader-previousRequest:', previousRequest)
        }
    

    在这里插入图片描述

4.3 pitch的应用 — style-loader

一般情况下处理 css/less 文件的 loader 调用链为 [style-loader、css-loader, less-loader]

  • less-loader: 将 less 转换为标准 css。
  • css-loader : 将 css 转换为 js 模块。
  • style-loader : 将 js 模块以 link 、style 标签等方式挂在到 html 中。

实际上,style-loader 的作用只是将 css 如何挂在到 html 中, 其他的并不关心。style-loader 的核心代码如下:

// <https://github.com/webpack-contrib/style-loader/blob/master/src/index.js>
loader.pitch = function pitch(request) {
	// ... 省略
	const options = this.getOptions(schema);
	// 以什么方式插入,默认 style 标签
  const injectType = options.injectType || "styleTag";
  // ... 省略
  switch (injectType) { 
	  case "linkTag": {
		   ...// 省略
	  }
	  case "lazyStyleTag":
    case "lazyAutoStyleTag":
    case "lazySingletonStyleTag": {
	    ...// 省略
    }
	  case "styleTag":
    case "autoStyleTag":
    case "singletonStyleTag":
    default {
		   ... // 省略
		   return `
				   ... // 省略
				   ${getImportStyleAPICode(esModule, this)}  // 1、导入runtime
				   ${getImportInsertStyleElementCode(esModule, this)} // 2、导入创建 style 标签的方法
				   ${getImportStyleContentCode(esModule, this, request)} // 3、导入 css 模块的内容
				   ... // 省略
				   
				   var options = ${JSON.stringify(runtimeOptions)};
					 options.setAttributes = setAttributes;
					 options.domAPI = ${getdomAPI(isAuto)};
					 options.insertStyleElement = insertStyleElement;
				   var update = API(content, options); // 4、将 css 样式插入到 style 标签中
		   `
    }
  }
}

injectType 表示将 css 以什么形式插入到 DOM 中,不同的 type 处理方式不同, 我们直接看 default 部分,比较符合日常的使用,即使用 <style> 标签插入到 DOM 中。这一部分关键点可以分为4步。

// 导入runtime
${getImportStyleAPICode(esModule, this)} 
// 实际返回
import API from "!../node_modules/.pnpm/style-loader@3.3.1_webpack@5.73.0/node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js";
// 导入空 style 标签
${getImportInsertStyleElementCode(esModule, this)}
// 实际返回
import insertStyleElement from "!../node_modules/.pnpm/style-loader@3.3.1_webpack@5.73.0/node_modules/style-loader/dist/runtime/insertStyleElement.js";

// insertStyleElement 方法如下
function insertStyleElement(options) {
  const element = document.createElement("style");
  options.setAttributes(element, options.attributes);
  options.insert(element, options.options);
  return element;
} 
  • 第三步:导入 css-loader 处理的结果,是一个被 webpack 处理好的 module。
 // 导入 css 结果
 ${getImportStyleContentCode(esModule, this, request)}
 // 实际返回
 import content, * as namedExport from "!!../node_modules/.pnpm/css-loader@6.7.1_webpack@5.73.0/node_modules/css-loader/dist/cjs.js!./xq-loader/sprite-loader.js!./index.css"

这里 getImportStyleContentCode 方法调用上下文的 this.utils api 中的 contextify 方法将 request 路径 (即pitch 方法的第一个参数 remainingRequest)转换为一个新的请求路径(即一个相对路径)。

function getImportStyleContentCode(esModule, loaderContext, request) {
  const modulePath = stringifyRequest(loaderContext, `!!${request}`);

  return esModule
    ? `import content, * as namedExport from ${modulePath};`
    : `var content = require(${modulePath});`;
}
// stringifyRequest 方法返回的内容
loaderContext.utils.contextify(loaderContext.context, request) => "../node_modules/.pnpm/css-loader@6.7.1_webpack@5.73.0/node_modules/css-loader/dist/cjs.js!./xq-loader/sprite-loader.js!./index.css"

注意这里的 !! 符号,即忽略已经配置过的 loader ,避免循环执行。webpack 执行到这里时,会递归执行这个行内loader 最终返回一个 module(即这里的conent),可供 runtime 阶段直接使用。(这里是按照项目中配置的loader来,所以会有一个 xq-loader/sprite-loader.js

import content, * as namedExport from "!!../node_modules/.pnpm/css-loader@6.7.1_webpack@5.73.0/node_modules/css-loader/dist/cjs.js!./xq-loader/sprite-loader.js!./index.css"
  • 第四步:runtime阶段,将前面导入的 css内容插入到style 标签中。
var update = API(content, options);

以上就是 style-loader 的工作原理。假如这里 style-loader 设计为一个 normal loader,按照执行顺序,style-loader 需要额外处理 css-loader 返回的 js 脚本内容,提取需要的样式内容,这样无疑需要写一堆处理逻辑,设计成 pitch loader 只需要关注如何将 css 内容插入到 DOM 中即可,其他的交给 webpack 处理。将这一部分转换为流程图更好理解。

  • 执行 pitch 方法,返回一段 js 脚本给 webpack ,后续 loader 不再执行
    在这里插入图片描述

  • webpack 继续解析构建 style-loader 返回的内容,遇到 inline-loader 之后

在这里插入图片描述

五、总结

  • loader的两个阶段:对于一个 loader 来说,都有自己的 normal 方法和 pitch 方法。
  • loader 的调用链:对于多个 loader 的调用链即先从低到高(post → pre)依次调用 pitch 方法,再依次从高到低 (pre → post)调用各自的 normal 方法。
  • pitch 的熔断作用:pitch 方法中如有非undefined的返回,则会熔断后续loader的执行。style-loader 、vue-loader 都巧妙的利用了 pitch 的熔断作用。
<think>好的,我现在需要给用户讲解清楚JavaScript中的class类。首先,我得回忆一下自己对这个主题的理解,确保没有遗漏或者错误的地方。嗯,JavaScript的class是在ES6引入的,它主要是基于原型继承的语法糖,对吧?所以它的底层还是原型链,但语法更接近传统面向对象语言,比如Java或C++,这样更容易让开发者理解和组织代码。 接下来,我应该从基础开始。首先解释class的基本结构,比如如何声明一个类,构造函数constructor的作用,以及如何定义方法。用户可能需要一个简单的例子,比如定义一个Person类,有name属性和sayHello方法。这时候要注意语法,使用class关键字,constructor方法,以及普通方法。 然后,需要讨论继承,也就是extends关键字。这里要提到super的作用,在派生类中调用父类的构造函数。可能还需要举例,比如Student类继承Person类,并添加自己的属性如grade和方法study。这个例子能帮助用户理解继承的用法和super的重要性。 接下来是静态方法和静态属性。静态方法是属于类本身的方法,而不是实例,所以要用static关键字。比如在Person类中添加一个静态方法compare,比较两个人的年龄。这里需要强调静态方法不能通过实例调用,而是通过类名直接调用。 还有私有字段,这是ES2022引入的特性,用#前缀表示。比如在类中定义#age,这样外部无法直接访问,只能通过公共方法来获取或修改。这个例子能说明封装的概念,但要注意浏览器兼容性问题。 然后,需要对比class和原型继承的区别,说明class只是语法糖,底层还是基于原型的。可能用代码例子展示传统的构造函数和原型方法,与class写法对比,让用户明白两者本质相同,但class更简洁易读。 可能用户还会关心class的一些高级特性,比如getter和setter,或者类表达式。不过作为入门介绍,可能暂时不需要深入这些细节,但可以稍微提一下,让用户知道有这些功能存在。 还要注意常见问题,比如类声明不会提升,必须先定义后使用,而函数声明会提升。此外,类的方法之间不需要逗号分隔,这点和对象字面量不同,容易出错,需要提醒用户。 最后,总结class的优势:代码结构清晰,易于继承,更接近传统OOP,适合中大型项目。但也要指出其局限性,比如私有字段的兼容性问题,以及无法直接多继承等。 在组织内容时,要确保结构清晰,分点讲解,每个部分都有代码示例,帮助用户逐步理解。同时,检查LaTeX格式是否正确,行内公式用$...$,独立公式用$$...$$,不过在这个问题中可能数学公式不多,但如果有提到数学相关的例子,比如计算类实例的数量,可能需要用到。不过当前主题是JavaScript的class,可能数学公式较少,但需要确认是否有必要。 在写作过程中,要使用中文,保持口语化,避免使用专业术语过多,或者解释清楚术语。比如“语法糖”这样的术语,可能需要简单解释一下,让新手也能理解。 现在,检查是否有遗漏点,比如类的访问器属性(getter和setter),或者类的表达式(匿名类),这些可以作为扩展内容,但如果是“一文讲清楚”,可能需要涵盖。不过根据用户要求,可能需要保持简洁,重点突出核心概念。 最后,确保所有代码示例正确,符合JavaScript语法,比如类名首字母大写,方法定义不用function关键字,正确使用super等。如果有错误,可能会误导用户,所以需要仔细检查。 总结下来,结构应该是: 1. Class基本概念 2. 基本结构(constructor、方法) 3. 继承(extends、super) 4. 静态方法和属性 5. 私有字段 6. Class与原型继承的关系 7. 注意事项和常见问题 8. 总结优缺点 每个部分配合代码示例,确保用户能一步步理解。同时,注意用户可能存在的疑问点,比如为什么使用class而不是原型,或者如何处理私有变量,需要提前解答。</think>好的,我将通过以下步骤带您理解JavaScript中的`class`类。本文结构清晰,代码示例丰富,适合新手入门。 --- ### 一、Class的基本概念 JavaScript的`class`是**ES6引入的语法糖**,本质仍是基于原型的继承,但写法更接近传统面向对象语言(如Java),解决了ES5构造函数和原型链写法冗长的问题。 --- ### 二、Class基本结构 #### 1. 定义类与构造函数 ```javascript class Person { constructor(name) { this.name = name; // 实例属性 } // 实例方法(添加到原型链) sayHello() { console.log(`你好,我是${this.name}`); } } // 使用类 const person = new Person("小明"); person.sayHello(); // 输出:你好,我是小明 ``` #### 2. 静态方法 用`static`定义,直接通过类名调用: ```javascript class MathUtils { static sum(a, b) { return a + b; } } console.log(MathUtils.sum(2, 3)); // 输出:5 ``` --- ### 三、继承(extends和super) #### 1. 基础继承 ```javascript class Student extends Person { constructor(name, grade) { super(name); // 调用父类构造函数 this.grade = grade; } study() { console.log(`${this.name}正在学习${this.grade}年级课程`); } } const student = new Student("小红", 6); student.sayHello(); // 继承自Person student.study(); // 输出:小红正在学习6年级课程 ``` #### 2. 重写方法 ```javascript class Student extends Person { sayHello() { super.sayHello(); // 调用父类方法 console.log("我是一名学生"); } } ``` --- ### 四、私有字段(ES2022) 使用`#`前缀定义私有属性,外部无法直接访问: ```javascript class User { #password; // 私有字段 constructor(username, password) { this.username = username; this.#password = password; } checkPassword(inputPwd) { return this.#password === inputPwd; } } const user = new User("admin", "123456"); console.log(user.#password); // 报错:私有字段无法访问 console.log(user.checkPassword("123456")); // true ``` --- ### 五、Class vs 原型继承 #### 传统ES5写法 ```javascript function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log("Hello, " + this.name); }; ``` #### ES6 Class写法(等价但更简洁) ```javascript class Person { constructor(name) { this.name = name; } sayHello() { console.log(`Hello, ${this.name}`); } } ``` --- ### 六、注意事项 1. **类声明不会提升**:必须先声明后使用。 2. **方法不用逗号分隔**:类方法之间无需`,`,与对象字面量不同。 3. **严格模式**:类内部默认启用严格模式。 --- ### 七、总结 #### 优点 - 代码结构清晰,易于实现继承 - 更接近传统OOP,降低学习成本 - 适合中大型项目代码组织 #### 局限 - 私有字段(`#`)存在兼容性问题 - 底层仍是原型链,无法直接实现多继承 --- 通过以上内容,您应该能掌握JavaScript类的核心用法。如需深入,可继续探索`getter/setter`、`Mixin模式`等高级特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

crazy的蓝色梦想

如果对你有帮助,就鼓励我一下吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值