写在前面
大多数前端开发者对于loader
可能都清楚它存在两个执行阶段normal
和pitch
阶段,但是大多数同学对于pitch loader
的理解仅仅停留在:
pitch loader
的含义是什么。pitch loader
会产生什么样的作用。
一旦上手loader
开发,在进行相关loader
开发时不清楚pitch loader
在真实业务场景会给我们带来什么样的作用,真实的pitch loader
的应用场景是什么。
以及在设计一款loader
应用时对于将loader
逻辑设计在normal
阶段还是pitch
阶段完全没有概念。
对于如何设计一款loader
大多数开发者可能仅仅停留在基础阶段,但是往往正是这些对于细节知识的把控性才正是一个软件开发工程师综合能力的体现。
这里,这篇文章的目的就是带领大家从开源loader
项目的设计哲学来窥探pitch loader
的真实应用场景,带你真正掌握pitch loader
在实际开发中的适用场景。
如果你不了解
webpack loader
,不用担心。我们会在开头用几张通俗的图片来讲诉何谓pitch loader
和normal loader
。
何谓Loader pitch
首先在开始之前我们会稍微来聊聊简单的前置知识。
对于
loader
进阶知识,有想了解的朋友可以查看这篇文章:[多角度解析Webpack5之Loader核心原理],涵盖loader
基础应用、源码实现、开发企业级loader
各个方面的讲解。
loader的种类
Loader可以分为四种
在webpack
中loader
分为四个阶段,分别是pre
、normal
、inline
以及post
四种loader
,区分它们的依据正如它们的名字:四种loader
会按照不同的执行顺序去执行。
-
Pitching 阶段: loader 上的 pitch 方法,按照
后置(post)、行内(inline)、普通(normal)、前置(pre)
的顺序调用。 -
Normal 阶段: loader 上的 常规方法,按照
前置(pre)、普通(normal)、行内(inline)、后置(post)
的顺序调用。模块源码的转换, 发生在这个阶段。
此时如果不清楚什么是
normal
、pitch
,没关系你需要单纯的记住这loader
分为四种。它们的执行顺序在正常情况下是前置(pre)、普通(normal)、行内(inline)、后置(post)
。
指定Loader种类
在webpack
配置文件中,我们可以通过module
对象上的rule.enforce
配置项规定这个loader
的种类,通过配置文件我们可以配置三种类型的loader
:
-
当
enforce
为pre
时,该配置项目内的loader
为前置pre loader
。 -
当
enforce
为post
时,该配置项目内的loader
为后置post loader
。 -
当
enforce
什么都不配置时,该配置项目内的loader
为默认normal loader
。
此时有的同学会疑问那么inline loader
是不是配置enforce:inline
就好了呢?
其实不然,行内inline-loader
并不在webpack
配置文件中进行配置,它的配置方式在我们的业务代码的模块引入语句中。
import Styles from 'style-loader!css-loader?modules!./styles.css';
复制代码
比如这里,我们在通过import Styles from './styles'
该模块时通过!
分割的规则,配置了两个行内loader
,分别是style-loader
和css-loader
。
inline loader
的执行顺序同样是从右往左,也就是inline-loader
执行时会先执行css-loader
处理文件,再会执行style-loader
处理。
使用行内loader
时,可以额外配置一些规则
通过为内联 import
语句添加前缀,可以覆盖 [配置] 中的所有 loader, preLoader 和 postLoader:
-
使用
!
前缀,将禁用所有已配置的 normal loader(普通 loader)import Styles from '!style-loader!css-loader?modules!./styles.css'; 复制代码
-
使用
!!
前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader)import Styles from '!!style-loader!css-loader?modules!./styles.css'; 复制代码
-
使用
-!
前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoadersimport Styles from '-!style-loader!css-loader?modules!./styles.css'; 复制代码
这里比较简单,就是通过前缀来设置禁用不同种类的loader
,不了解的同学可以自己补习补习。
normal loader & pitch loader
上边我们讲过了loader
存在四种类型,也简单给大家提过四种loader
的执行顺序。这里。我们会详细讲诉四种loader
的执行顺序。
简单说说什么是normal
和pitch
关于normal loader
本质上就是loader
函数本身。
// loader函数本身 我们称之为loader的normal阶段
function loader(source) {
// dosomething
return source
}
复制代码
关于pitch loader
就是normal loader
上的一个pitch
属性,它同样是一个函数:
// pitch loader是normal上的一个属性
loader.pitch = function (remainingRequest,previousRequest,data) {
// ...
}
复制代码
简单来说这就是pitch loader
和normal loader
。
我们将loader
的pitch
属性称为loader
的pitch loader
。
自然而然,我们将loader
函数本身称为noram loader
。
关于
pitch loader
和normal loader
的参数和返回值代表的含义,如果你目前还不是很清楚。强烈建议你首先去阅读[多角度解析Webpack5之Loader核心原理]。
执行阶段
上边照顾了一下基础薄弱的同学,稍微聊了聊pitch
和normal
的基础内容,那么这两个阶段的作用分别是什么呢?
这我们用一张图来看一下对应的执行顺序:
图中我们有8个loader
,它们分别存在对应的种类,webpack
中对于一次文件的引入首先会进入loader
处理文件的阶段,loader
处理完成才会交给webpack
进行编译。
通过上图我们可以看到:
-
首先在一次
webpack
中引入一次资源(无论是通过import
还是require
),首先会进入loader
处理阶段。 -
loader
处理阶段首先会左从往右经过pitch loader
的函数调用,一层一层处理。它的处理顺序是:post
、inline
、normal
、pre
。 -
在
pitch
阶段全部处理完成后,这一步才会读取引入的资源文件内容。 -
将读取到的资源文件内容交给
noraml-loader
函数,一层一层传递处理。它的执行顺序是:pre
、normal
、inline
、post
。 -
最终将
loader
处理后的资源返回给webpack
进行编译处理。
pitch loader
的熔断效果
上边我们说到webpack
编译资源时首先经过loader
的处理,会经过两个阶段分别是pitch
和normal
阶段。
这里关于为什么会存在pitch
阶段,pitch
阶段究竟有什么用。我会在接下里在实践中和你好好讨论这一点,首先这里我们需要清楚pitch
阶段的一个重要特性:
pitch loader
中如果存在非undefeind
返回值的话,那么上述图中的整个loader chain
会发生熔断效果。
你可以会疑惑什么是熔断效果,来看看这张图:
假设我们在inline-loader
的pitch
阶段返回了一个一个字符串19Qingfeng
,那么此时loader
的执行会打破原有的顺序。
它会立马掉头将pitch
函数的返回值去执行前置的noraml loader
。
这里有两点需要额外说明:
-
pitch
阶段返回的非undefeind
的值会造成loader
打破原有顺序掉头执行,这就叫做熔断效果。 -
正常执行时是会读取资源文件的内容交给
normal loader
去处理,但是pitch
存在返回值时发生熔断并不会读取文件内容了。此时pitch
函数返回的值会交给将要执行的normal loader
。
这里你仅需要了解pitch
阶段所谓熔断代表的含义,接下来我会带你深入它的应用场景。
从开源Loader应用源码分析
祝贺可以看到这里的小伙伴,接下来我们就来探索一下绝大多数开发者“知其然而不知其所以然”的地方--何时应该将Loader设计为pitch loader
!
从style-loader
源码思路出发
这里我们先来看看style-loader
的源代码:
你可以大致看下这张图片,没有必要深究它。相信我,快速划过即可。
Emm...它的代码的确又臭又长是吧哈哈!
这里我并不会带你去阅读这段代码,因为阅读它的完整源码对于文章中想表述的内容没有多大帮助。
但是这里我会告诉你这段“又臭又长”的代码究竟在做什么事情::
-
首先,这个
loader
的所有逻辑都是设计在pitch
阶段进行执行,它的normal
函数就是一个空函数。 -
其次,
style-loader
做的事情很简单:它获得对应的样式文件内容,然后通过在页面创建style
节点。将样式内容赋给style
节点然后将节点加入head
标签即可。
这样看来是不是很简单,先来抛开你心中对于pitch
的疑惑。忘掉它!让我们来动手实现一下它。
实现style-loader
核心逻辑
Tip: 真实style-loader
源码中无非是对于一些边界情况的兼容处理,比如判断你用esm
还是cjs
等等之类。
这里我想和你强调的是源码流程,毕竟一个style-loader
完整实现我相信对于大家来说稍微费点神都可以看明白。
function styleLoader(source) {
const script = `
const styleEl = document.createElement('style')
styleEl.innerHTML = ${JSON.stringify(source)}
document.head.appendChild(styleEl)
`;
return script;
}
复制代码
非常简单吧,这里我们通过上边所说的核心思路实现了一个style-loader
的功能,最终导出了一个script
脚本。
webpack
解析到关于require(*.css)
文件时,会交给style-loader
去处理,最终将返回的script
打包成一个module
。
在通过require(*.css)
执行后页面就会添加对应的style
节点了。
有兴趣的同学可以自己搭建一个webpack
环境验证下,将css
结尾的文件交给我们自己写的style-loader
去处理即可。
细心的同学可能发现了,这里我们将style-loader
的逻辑放在了normal
阶段,而源码中放在了pitch
阶段。
那么是不是放在normal
阶段也可以呢? 接下来,让我们来换一种写法。
将style-loader
设计成为normal loader
通常我们在使用style-loader
处理我们的css
样式文件时,都会配合css-loader
去一起处理css
文件中的引入语句。
样式文件首先会经过css-loader
的处理之后才会交给style-loader
处理。
这里,让我们使用我们自己style-loader
在配合css-loader
来处理一下:
yarn add -D css-loader
复制代码
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
devtool: false,
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
resolveLoader: {
modules: [path.resolve(__dirname, './loaders'), 'node_modules'],
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [new HtmlWebpackPlugin()],
};
复制代码
同时我的目录下会存在这样三个业务文件:
src/index.js
: 本次打包的入口文件。
// 它做的事情很简单 引入index.css
const styles = require('./index.css');
复制代码
src/index.css
: 被入口js
文件引入的样式文件。
// index.css 中定义了body的背景色
// 以及通过@import 语句引入了 ./require.css
@import url('./require.css');
body {
color: red;
}
复制代码
src/require.css
:被index.css
引入的样式文件。
div {
color: blue;
}
复制代码
这是我自己的
webpack
配置文件,使用了我们刚刚实现的style-loader
以及原本的css-loader
去处理文件样式文件。
再次运行打包打开生成的html
页面:
我们可以看到body
上我们设置的color:red
丢失了。
其实本质上出现这个问题的原因是css-loader
的normal
阶段会将样式文件处理成为js
脚本并且返回给style-loader
的normal
函数中。
我们可以在自己的style-loader
中打印一下:
function styleLoader(source) {
console.log(source, 'source');
const script = `
const styleEl = document.createElement('style')
styleEl.innerHTML = ${JSON.stringify(source)}
document.head.appendChild(styleEl)
`;
return script;
}
复制代码
source
的内容是一个js
脚本,我们将js
脚本的内容插入到styleEl
中去,当然是任何样式也不会生效。
这是打包后生成html
中style
节点的内容。
这也就意味着,如果我们将style-loader
设计为normal loader
的话,我们需要执行上一个loader
返回的js
脚本,并且获得它导出的内容才可以得到对应的样式内容。
那么此时我们需要在style-loader
的normal
阶段实现一系列js
的方法才能执行js
并读取到css-loader
返回的样式内容,这无疑是一种非常糟糕的设计模式。
将style-loader
设计成为pitch loader
那么,我们尝试按照源码的思路设计成为pitch loader
呢?
这样又会有什么好处呢? 让我们先来分析一下。
首先如果说我们在style-loader
的pitch
阶段直接返回值的话,那么会发生熔断效应。
上边我们说到过,如果发生熔断效果那么此时会立马掉头执行normal loader
,因为style-loader
是第一个执行的过程,相当于:
那么为什么要这么做呢?
我们可以在style-loader
的pitch
阶段通过require
语句引入css-loader
处理文件后返回的js
脚本,得到导出的结果。然后重新组装逻辑返回给webpack
即可。
这样做的好处是,之前我们在normal
阶段需要处理的执行css-loader
返回的js
语句完全不需要自己实现js
执行的逻辑。完全交给webpack
去执行了。
也许大多数同学仍然不是很明白这是什么意思,没关系。我先来带你实现一下它的基本内容:
function styleLoader(source) {}
// pitch阶段
styleLoader.pitch = function (remainingRequest, previousRequest, data) {
const script = `
import style from "!!${remainingRequest}"
const styleEl = document.createElement('style')
styleEl.innerHTML = style
document.head.appendChild(styleEl)
`;
return script;
};
module.exports = styleLoader
复制代码
这里我将style-loader
的处理放在了pitch
阶段进行处理。
pitch
阶段的remainingRequest
表示剩余还未处理loader
的绝对路径以"!"拼接(包含资源路径)的字符串。
这里我们通过在style-loader
的pitch
阶段直接返回js
脚本:
此时webpack
会将style-loader
返回的js
脚本进行编译。
将本次返回的脚本编译称为一个module
,同时会递归编译本次返回的js
脚本,监测到它存在模块引入语句import/require
进行递归编译。
此时style-loader
中返回的module
中包含这样一句代码:
import style from "!!${remainingRequest}"
复制代码
我们在normal loader
阶段棘手的关于css-loader
返回值是一个js
脚本的问题通过import
语句我们交给了webpack
去编译。
webpack
会将本次import style from "!!${remainingRequest}"
重新编译称为另一个module
,当我们运行编译后的代码时候:
- 首先分析
const styles = require('./index.css');
,style-loader pitch
处理./index.css
并且返回一个脚本。
webpack
会将返回的js
脚本编译称为一个module
,同时分析这个module
中的依赖语句进行递归编译。
- 由于
style-loader pitch
阶段返回的脚本中存在import
语句,那么此时webpack
就会递归编译import
语句的路径模块。
webpack
递归编译style-loader
返回脚本中的import
语句时,我们在编译完成就会通过import style from "!!${remainingRequest}"
,在style-loader pitch
返回的脚本阶段获得css-loader
返回的js
脚本并执行它,获取到它的导出内容。
- 这里有一点需要强调的是:我们在使用
import
语句时使用了 !!(双感叹号) 拼接remainingRequest
,表示对于本次引入仅仅有inline loader
生效。否则会造成死循环。
此时重新打包代码,我们来看看页面的展示效果:
此时打开生成的页面,你会发现我们的样式又重新生效了。
让我们再来捎带看一眼打包后js
代码吧:
可以清晰的看到./src/index.css
被编译称为了一个module
,它的内容就是我们style-loader pitch
阶段返回的内容。
同时在这个模块的你内部,你发现通过__webpack_require__
另一个module
,本质上它就是import style from "!!${remainingRequest}"
这句话编译后的结果。
这是import style from "!!${remainingRequest}"
语句中${remainingRequest}
模块编译后的模块代码,这里只是一个部分截图。
我们只需要看到的确对应的remainingRequest
也同时被编译成为了一个module
~
其实这就是style-loader
为什么要实现pitch
阶段来进行逻辑处理内容,你说normal
不可以吗?
如果一定要用normal
的话的确可以,但是我们需要处理太多的import/require
从而实现模块引入,这无疑是一种非常糟糕的设计模式。
如果关于
webpack
打包编译流程有兴趣的同学,可以查看这篇Webapck5核心打包原理全流程解析--300行代码带你实现webpack
核心原理。
真实Pitch应用场景总结
通过上述的style-loader
的例子,当我们希望将左侧的loader
并联使用的时候使用pitch
方式无疑是最佳的设计方式。
通过pitch loader
中import someThing from !!${remainingRequest}
剩余loader
,从而实现上一个loader
的返回值是js
脚本,将脚本交给webpack
去编译执行,这就是pitch loader
的实际应用场景。
简单来说,如果在loader
开发中你的需要依赖loader
其他loader
,但此时上一个loader
的normal
函数返回的并不是处理后的资源文件内容而是一段js
脚本,那么将你的loader
逻辑设计在pitch
阶段无疑是一种更好的方式。
写在文章结尾
在文章的最后,希望和大家稍微来谈一谈为什么我会单独拉出来一个 不常用的pitch loader
来进行长篇大论。
首先感谢每一位可以看到结尾的同学,从我个人角度恰恰是觉得正是这些对于细节的把控性才是一个高级软件工程师必备的素质条件。
在大多数人仅仅停留概念和基础含义时,而你可以轻车熟路的在不同的应用场景下考虑到最佳的应用设计方式,虽然有时用到这种能力的地方的确不是很多。
但是在我看来对于知识深度的把控能力和应用理解能力正是决定一名软件开发者天花板高度的内在体现~
最后的最后,希望大家通过文章可以真正了解loader pitch
阶段设计的含义以及何时你应该去考虑使用pitch
来设计你的loader
。