你不知道的「pitch loader」应用场景

2022-02-28 09:30:55 浏览数 (1)

写在前面

大多数前端开发者对于loader可能都清楚它存在两个执行阶段normalpitch阶段,但是大多数同学对于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 loadernormal loader

何谓Loader pitch

首先在开始之前我们会稍微来聊聊简单的前置知识。

对于loader进阶知识,有想了解的朋友可以查看这篇文章:[多角度解析Webpack5之Loader核心原理],涵盖loader基础应用、源码实现、开发企业级loader各个方面的讲解。

loader的种类

Loader可以分为四种

webpackloader分为四个阶段,分别是prenormalinline以及post四种loader,区分它们的依据正如它们的名字:四种**loader**会按照不同的执行顺序去执行

  1. Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。
  2. Normal 阶段: loader 上的 常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换, 发生在这个阶段

此时如果不清楚什么是normalpitch,没关系你需要单纯的记住这loader分为四种。它们的执行顺序在正常情况下是 前置(pre)、普通(normal)、行内(inline)、后置(post)

指定Loader种类

webpack配置文件中,我们可以通过module对象上的rule.enforce配置项规定这个loader的种类,通过配置文件我们可以配置三种类型的loader

  • enforcepre时,该配置项目内的loader为前置pre loader
  • enforcepost时,该配置项目内的loader为后置post loader
  • enforce什么都不配置时,该配置项目内的loader为默认normal loader

此时有的同学会疑问那么inline loader是不是配置enforce:inline就好了呢?

其实不然,行内**inline-loader**并不在**webpack**配置文件中进行配置,它的配置方式在我们的业务代码的模块引入语句中。

代码语言:javascript复制
import Styles from 'style-loader!css-loader?modules!./styles.css';

比如这里,我们在通过import Styles from './styles'该模块时通过!分割的规则,配置了两个行内loader,分别是style-loadercss-loader

inline loader的执行顺序同样是从右往左,也就是inline-loader执行时会先执行css-loader处理文件,再会执行style-loader处理。

使用行内loader时,可以额外配置一些规则

通过为内联 import 语句添加前缀,可以覆盖 配置 中的所有 loader, preLoader 和 postLoader:

使用 ! 前缀,将禁用所有已配置的 normal loader(普通 loader)

代码语言:javascript复制
import Styles from '!style-loader!css-loader?modules!./styles.css';

使用 !! 前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader)

代码语言:javascript复制
import Styles from '!!style-loader!css-loader?modules!./styles.css';

使用 -! 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders

代码语言:javascript复制
import Styles from '-!style-loader!css-loader?modules!./styles.css';

这里比较简单,就是通过前缀来设置禁用不同种类的loader,不了解的同学可以自己补习补习。

normal loader & pitch loader

上边我们讲过了loader存在四种类型,也简单给大家提过四种loader的执行顺序。这里。我们会详细讲诉四种loader的执行顺序。

简单说说什么是normalpitch

关于normal loader本质上就是loader函数本身。

代码语言:javascript复制
// loader函数本身 我们称之为loader的normal阶段
function loader(source) {
    // dosomething
    return source
}

关于pitch loader就是normal loader上的一个pitch属性,它同样是一个函数:

代码语言:javascript复制
// pitch loader是normal上的一个属性
loader.pitch = function (remainingRequest,previousRequest,data) {
    // ...
}

简单来说这就是pitch loadernormal loader

我们将**loader**的**pitch**属性称为**loader**的**pitch loader**。

自然而然,我们将**loader**函数本身称为**noram loader**。

关于pitch loadernormal loader的参数和返回值代表的含义,如果你目前还不是很清楚。强烈建议你首先去阅读[多角度解析Webpack5之Loader核心原理]。

执行阶段

上边照顾了一下基础薄弱的同学,稍微聊了聊pitchnormal的基础内容,那么这两个阶段的作用分别是什么呢?

这我们用一张图来看一下对应的执行顺序:

图中我们有8个loader,它们分别存在对应的种类,**webpack中对于一次文件的引入首先会进入loader处理文件的阶段,loader处理完成才会交给webpack进行编译。**

通过上图我们可以看到:

  • 首先在一次webpack中引入一次资源(无论是通过import还是require),首先会进入loader处理阶段。
  • loader处理阶段首先会左从往右经过pitch loader的函数调用,一层一层处理。**它的处理顺序是:postinlinenormalpre**。
  • pitch阶段全部处理完成后,这一步才会读取引入的资源文件内容
  • 将读取到的资源文件内容交给noraml-loader函数,一层一层传递处理。**它的执行顺序是:prenormalinlinepost**。
  • 最终将loader处理后的资源返回给webpack进行编译处理。

pitch loader的熔断效果

上边我们说到webpack编译资源时首先经过loader的处理,会经过两个阶段分别是pitchnormal阶段。

这里关于为什么会存在pitch阶段,pitch阶段究竟有什么用。我会在接下里在实践中和你好好讨论这一点,首先这里我们需要清楚pitch阶段的一个重要特性:

pitch loader**中如果存在非**undefeind**返回值的话,那么上述图中的整个**loader chain**会发生熔断效果**。

你可以会疑惑什么是熔断效果,来看看这张图:

假设我们在inline-loaderpitch阶段返回了一个一个字符串19Qingfeng,那么此时loader的执行会打破原有的顺序。

它会立马掉头将**pitch**函数的返回值去执行前置的**noraml loader**。

这里有两点需要额外说明:

  1. pitch阶段返回的非undefeind的值会造成loader打破原有顺序掉头执行,这就叫做熔断效果。
  2. 正常执行时是会读取资源文件的内容交给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完整实现我相信对于大家来说稍微费点神都可以看明白。

代码语言:javascript复制
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来处理一下:

代码语言:javascript复制
yarn add -D css-loader
代码语言:javascript复制
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: 本次打包的入口文件。
代码语言:javascript复制
// 它做的事情很简单 引入index.css
const styles = require('./index.css');
  • src/index.css: 被入口js文件引入的样式文件。
代码语言:javascript复制
// index.css 中定义了body的背景色
// 以及通过@import 语句引入了 ./require.css
@import url('./require.css');
body {
  color: red;
}
  • src/require.css:被index.css引入的样式文件。
代码语言:javascript复制
div {
  color: blue;
}

这是我自己的webpack配置文件,使用了我们刚刚实现的style-loader以及原本的css-loader去处理文件样式文件。

再次运行打包打开生成的html页面:

我们可以看到body上我们设置的color:red丢失了。

其实本质上出现这个问题的原因是**css-loader**的**normal**阶段会将样式文件处理成为**js**脚本并且返回给**style-loader**的**normal**函数中

我们可以在自己的style-loader中打印一下:

代码语言:javascript复制
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中去,当然是任何样式也不会生效。

这是打包后生成htmlstyle节点的内容。

这也就意味着,如果我们将style-loader设计为normal loader的话,我们需要执行上一个**loader**返回的**js**脚本,并且获得它导出的内容才可以得到对应的样式内容。

那么此时我们需要在**style-loader**的**normal**阶段实现一系列**js**的方法才能执行**js**并读取到**css-loader**返回的样式内容,这无疑是一种非常糟糕的设计模式。

style-loader设计成为pitch loader

那么,我们尝试按照源码的思路设计成为pitch loader呢?

这样又会有什么好处呢? 让我们先来分析一下。

首先如果说我们在style-loaderpitch阶段直接返回值的话,那么会发生熔断效应。

上边我们说到过,如果发生熔断效果那么此时会立马掉头执行normal loader,因为style-loader是第一个执行的过程,相当于:

那么为什么要这么做呢?

我们可以在**style-loader**的**pitch**阶段通过**require**语句引入**css-loader**处理文件后返回的**js**脚本,得到导出的结果。然后重新组装逻辑返回给**webpack**即可。

这样做的好处是,之前我们在**normal**阶段需要处理的执行**css-loader**返回的**js**语句完全不需要自己实现**js**执行的逻辑。完全交给**webpack**去执行了。

也许大多数同学仍然不是很明白这是什么意思,没关系。我先来带你实现一下它的基本内容:

代码语言:javascript复制
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-loaderpitch阶段直接返回js脚本:

此时webpack会将style-loader返回的js脚本进行编译。

将本次返回的脚本编译称为一个**module**,同时会递归编译本次返回的**js**脚本,监测到它存在模块引入语句**import/require**进行递归编译

此时style-loader中返回的module中包含这样一句代码:

代码语言:javascript复制
 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也同时被编译成为了一个module

其实这就是style-loader为什么要实现pitch阶段来进行逻辑处理内容,你说normal不可以吗?

如果一定要用normal的话的确可以,但是我们需要处理太多的import/require从而实现模块引入,这无疑是一种非常糟糕的设计模式。

如果关于webpack打包编译流程有兴趣的同学,可以查看这篇Webapck5核心打包原理全流程解析--300行代码带你实现webpack核心原理。

真实Pitch应用场景总结

通过上述的style-loader的例子,当我们希望将左侧的loader并联使用的时候使用pitch方式无疑是最佳的设计方式。

通过pitch loaderimport someThing from !!${remainingRequest}剩余loader,从而实现上一个loader的返回值是js脚本,将脚本交给**webpack**去编译执行,这就是pitch loader的实际应用场景。

简单来说,如果在**loader**开发中你的需要依赖**loader**其他**loader**,但此时上一个**loader**的**normal**函数返回的并不是处理后的资源文件内容而是一段**js**脚本,那么将你的**loader**逻辑设计在**pitch**阶段无疑是一种更好的方式。

写在文章结尾

在文章的最后,希望和大家稍微来谈一谈为什么我会单独拉出来一个 不常用的**pitch loader** 来进行长篇大论。

首先感谢每一位可以看到结尾的同学,从我个人角度恰恰是觉得正是这些对于细节的把控性才是一个高级软件工程师必备的素质条件。

在大多数人仅仅停留概念和基础含义时,而你可以轻车熟路的在不同的应用场景下考虑到最佳的应用设计方式,虽然有时用到这种能力的地方的确不是很多。

但是在我看来对于知识深度的把控能力和应用理解能力正是决定一名软件开发者天花板高度的内在体现~

最后的最后,希望大家通过文章可以真正了解loader pitch阶段设计的含义以及何时你应该去考虑使用pitch来设计你的loader

0 人点赞