配置过webpack的朋友应该都知道webpack中有一个loader的概念,当webpack进行编译时,会从入口文件出发,调用所有配置的loader对模块进行编译。本文来简单梳理一下何为loader?loader有哪些类型以及loader?loader具体运行机制是怎样的?
在webpack配置文件中,我们可以通过module中的rules字段进行loader配置
// webpack.config.js
module: {
rules: [
{
test: /.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
url: true,
import: true,
modules: false,
},
},
'less-loader',
],
},
],}
这个是一个针对.less
文件的loader经典配置。
一个正则表达式,用来匹配资源文件名称,如果命中此规则,则交由配置的loader进行处理。
如果是一个数组,表示匹配到的资源文件用这些loader进行处理。use中的每一项可以是一个字符串,这代表着一个loader的名字,比如上面的less-loader。如果你想针对loader传入一些配置项,则需要将其配置为一个对象形式,loader字段对应loader的名字,options字段则为传入的配置项,如上css-loader。
如果某种资源文件只需要使用一个loader,你也可以将其配置成一个loader名称字符串,如
use: css-loader
配置的loaders默认执行顺序是从下往上。如上配置的loader的执行顺序为less-loader -> css-loader -> style-loader
。当然了你也可以通过一个enforce
字段标明。
标明loader的执行顺序。enfore有两个值分别为pre
和 post
:
当loader rule中没有配置enfore时,默认为normal loader(正常loader)
当loader rule中配置enfore: 'pre'
时,我们称之为pre loader(前置loader)
当loader rule中配置enfore: 'post'
时,我们称之为post loader(后置loader)
在正常情况下,这三种loader的执行顺序为
flowchart LRA["resource file"]-->B["pre loader"]B["pre loader"]-->C["normal loader"] C["normal loader"]-->D["post loader"]
为什么要强调正常情况下呢?肯定是有非正常情况嘛,下文会提到
上面的配置等同于
module: {
rules: [
{
test: /.less$/,
use: 'less-loader',
enforce: 'pre',
},
{
test: /.less$/,
use: 'css-loader',
},
{
test: /.less$/,
use: 'style-loader',
enforce: 'post',
},
],},
webpack是如何找到loader的呢?webpack配置loader的方式有三种:
当我们想配置自己实现的但未发布到npm的loader时该怎么办呢?方法一是写绝对路径标明loader的具体文件位置。比如
const path = require('path');
module.exports = {
...
module: {
rules: [
{
test: /.js$/,
loader: path.resolve(__dirname, './my-loaders/babel-loader.js'),
},
],}
}
我们在loader字段中配置了一个绝对路径,这样webpack就会去该路径找到对应的loader。
第二种方式是通过resolveLoader.alias来配置loader的别名,比如
const path = require('path');
module.exports = {
...
resolveLoader: {
alias: {
'babel-loader': path.resolve(__dirname, 'my-loaders/babel-loader.js'),
},},
module: {
rules: [
{
test: /.js$/,
loader: 'babel-loader.js',
},
],}
}
当webpack解析到需要使用babel-loader时,会查找resolveLoader.alias对应的绝对路径,从而找到对应的loader对应的js文件进行处理。
如果我们每当要配置一个loader时都要写绝对路径或者resolveLoader.alias,这是非常繁琐且冗余的,那有什么更好的方式呢?这个时候resolveLoader.modules就登场了。比如
const path = require('path');
module.exports = {...resolveLoader: {
modules: ['my-loaders', 'node_modules'],},module: {
rules: [
{
test: /.js$/,
loader: 'babel-loader',
},
],}
}
我们将resolveLoader.modules配置为['my-loaders', 'node_modules']
,在loader字段中就可以只写loader名字了。原因在于当webpack查找babel-loader时,会先从my-loader文件夹中查找是否存在babel-loader.js这个文件,如果存在就直接使用,如果不存在就接着从node_modules中查找是否已经安装了babel-loader。关于node如何查找模块的更多信息可以参考 node module resolution algorithm。
resolveLoader.modules的默认配置是['node_modules']
, 这也是为什么当我们安装了一些第三方loader后,可以直接写loader名字的原因。
在上文中我们提到webpack中loader有pre、normal和post三种类型,其实webpack还支持一种特殊的loader,就是inline loader
(内联loader),即在引入资源模块时在模块名前面加上loader,比如
import styles from 'style-loader!css-loader!less-loader!./index.less';
如上所示,在引入index.less文件时,在文件名前面加入了三个loader,用 !
进行分隔,这种方式引入的loader称之为inline loader。
关于inline loader,还有一些特殊配置,可以在inline loader中加入以下前缀来进行一些配置,这样会影响配置中的pre loader、normal loader和post loader。
// 这里的意思是不使用配置文件配置的任何less文件的loader, 只使用这里配置内联loader
import styles from '!!style-loader!css-loader!less-loader!./index.less';
符号 | 变量 | 含义 |
---|---|---|
-! |
noPreAutoLoaders | 不要前置和普通 loader |
! |
noAutoLoaders | 不要普通 loader |
!! |
noPrePostAutoLoaders | 不要前后置和普通 loader,只要内联 loader |
正常loader的执行顺序
flowchart LRA["resource file"]-->B["pre loader"]B["pre loader"]-->C["normal loader"]C["normal loader"]-->D["inline loader"]D["inline loader"]-->E["post loader"]
上边我们说到webpack loader存在四种类型,这里我们简单说一下何为Normal Loader
,何为Pitch Loader
。
Normal loader
本质就是loader函数本身,比如
function loader(source) {return source;
}
module.exports = myLoader
当在loader上添加pitch属性,且值也为一个函数时,这个loader就成为Pitch loader
function myLoader(source) {
return source;
}
myLoader.patch = (remainingRequest, previousRequest, data) => {
}
module.exports = myLoader
我们可以理解为loader函数本身就是normal loader
,而loader上的pitch属性就是pitch loader
上文我们说到loader有四种类型以及他们的执行顺序,其实loader有两个执行阶段,分别为 normal
阶段和 pitch
阶段。上面我们说的执行顺序其实是normal阶段的执行顺序。
比如我们有loader1、loader2和loader3三个loader用来处理file.js文件,他们的执行顺序如下
这里需要注意的是,normal阶段的最后一个loader一定要返回一串js代码,否则webpack将无法处理。在这个例子中,loader1函数必须要返回一串js代码。
上文我们说到loader的处理过程存在一个pitch阶段,那么为什么会存在这个阶段呢,其有什么作用呢?这里我们需要重点记住pitch loader的一个重要
特性:
当pitch loader存在非undefined的返回值时,会跳过剩下的loader和读取文件资源,直接将返回值传入上一个normal loader中执行。如果是左边第一个pitch loader,则直接将返回值传给webpack。 这个特性被称之为pitch loader的熔断效果。
比如当 loader2.pitch
存在非undefined的返回值时,loader的执行顺序为:
可以看到当执行完loader2.pitch后,直接跳到了loader1执行,忽略了后面的loader3、资源文件读取和loader2 normal。
这里只需要记住pitch loader的特性即可,至于为什么要存在pitch loader,下文会讲。
在loader执行之前,webpack会将所有的loader叠加好(或者说组装),然后通过loader-runner
这个库依次执行叠加好的loader。
我们来简单模拟一下nomal阶段loader的叠加顺序。
创建一个webpack-loader项目,文件夹结构如下:
src/index.js
module.exports = 'this is index.js';
loaders/inline1-loader.js
function loader(source) {
console.log("inline1");
return source + "//inline1";
}
module.exports = loader;
loaders/inline1-loader.js
function loader(source) {
console.log("inline2");
return source + "//inline2";
}
module.exports = loader;
loaders/normal1-loader.js
function loader(source) {
console.log("normal1");
return source + "//normal1";
}
module.exports = loader;
loaders/normal2-loader.js
function loader(source) {
console.log("normal2");
return source + "//normal2";
}
module.exports = loader;
loaders/post1-loader.js
function loader(source) {
console.log("post1");
return source + "//post1";
}
module.exports = loader;
loaders/post2-loader.js
function loader(source) {
console.log("post2");
return source + "//post2";
}
module.exports = loader;
loaders/pre1-loader.js
function loader(source) {
console.log("pre1");
return source + "//pre1";
}
module.exports = loader;
loaders/pre2-loader.js
function loader(source) {
console.log("pre2");
return source + "//pre2";
}
module.exports = loader;
通过runner.js
来测试loader叠加和执行顺序
runnner.js
const { runLoaders } = require('loader-runner');
const path = require('path');
const fs = require('fs');
// 入口文件
const entryFile = path.resolve(__dirname, 'src/index.js');
// 配置inline loader
const request = `inline1-loader!inline2-loader!${entryFile}`;
// 配置文件中的loader
// 是不是pre或post跟loader本身没有关系,和你写在配置文件里的enforce的值有关系
const rules = [{
test: /.js$/,
use: ['normal1-loader', 'normal2-loader'],},{
test: /.js$/,
enforce: 'pre',
use: ['pre1-loader', 'pre2-loader'],},{
test: /.js$/,
enforce: 'post',
use: ['post1-loader', 'post2-loader'],},
];
const parts = request.replace(/^-?!+/, '').split('!');
// 真正的模块路径
const resource = parts.pop();
// 所有的inline loader
const inlineLoaders = parts;
// 存放pre loader
const preLoaders = [],
// 存放post loader
const postLoaders = [],
// 存放normal loader
const normalLoaders = [];
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
if (rule.test.test(resource)) {
if (rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else if (rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}}
}
let loaders = [];
// 解析特殊配置
if (request.startsWith('!!')) {
//noPreAutoLoaders不要前置和普通 loader
loaders.push(...inlineLoaders);
} else if (request.startsWith('-!')) {
//noAutoLoaders 不要前置和普通 loader
loaders.push(...postLoaders, ...inlineLoaders);
} else if (request.startsWith('!')) {
// noAutoLoaders 不要普通 loader
loaders.push(...postLoaders, ...inlineLoaders, ...preLoaders);
} else {
// 没有特殊配置即全都要
loaders.push(...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders);
}
// 把loader数组从名称变成绝对路径
loaders = loaders.map(loader => path.resolve(__dirname, 'loaders', loader));
runLoaders({
resource, //你要加载的资源
loaders,
context: { name: "xiao", age: 100 }, // 保存一些状态和值
readResource: fs.readFile.bind(this), // 使用什么来读取原始文件内容},(err, result) => {
console.log(result); //运行的结果}
);
可以看到loader的叠加顺序为: post(后置)+inline(内联)+normal(正常)+pre(前置),可以简单记为 厚脸挣钱
运行runner.js
可以看到loader的执行顺序确实是和我们前面说的normal 阶段一样。
现在修改normal-loader1
, 加入pitch函数:
src/normal1-loader.js
function loader(source) {
console.log('normal1');
return source + '//normal1';
}
loader.pitch = function () {
return 'normal1pitch';
};
module.exports = loader;
再次执行runner.js
可以看到,确实发生了熔断效果
下面我们来简单实现loader runner,其主要流程如下
loader-runner.js
const fs = require('fs');
// 根据loader模块的绝对路径得到loader对象
function createLoaderObject(loader) {
const normal = require(loader);
const pitch = normal.pitch;
return {
path: loader, // loader的绝对路径
normal,
pitch,
raw: normal.raw, // 决定normal函数的参数是字符串还是Buffer
data: {}, // 每个loader对象都会有一个自定义data对象
pitchExecuted: false, // 标识此loader的pitch函数是否已经执行过
normalExecuted: false, // 标识此loader的normal函数是否已经执行过};
}
// 处理资源文件
function processResource(processOptions, loaderContext, pitchingCallback) {
processOptions.readResource(loaderContext.resource, (err, resourceBuffer) => {
processOptions.resourceBuffer = resourceBuffer;
loaderContext.loaderIndex--; // 减1会后会指向最后一个loader
// 开始迭代执行normal loader
iterateNormalLoaders(processOptions, loaderContext, [resourceBuffer], pitchingCallback);});
}
// 转换参数
function convertArgs(args, raw) {
if (raw && !Buffer.isBuffer(args[0])) {
args[0] = Buffer.from(args[0]);} else if (!raw && Buffer.isBuffer(args[0])) {
args[0] = args[0].toString('utf8');}
}
// 迭代执行normal loader
function iterateNormalLoaders(processOptions, loaderContext, args, pitchingCallback) {
if (loaderContext.loaderIndex < 0) {
return pitchingCallback(null, args);}
const currentLoader = loaderContext.loaders[loaderContext.loaderIndex];
if (currentLoader.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(processOptions, loaderContext, args, pitchingCallback);}
let normalFn = currentLoader.normal;
currentLoader.normalExecuted = true;
convertArgs(args, currentLoader.raw);
runSyncOrAsync(normalFn, loaderContext, args, (err, ...returnArgs) => {
if (err) return pitchingCallback(err);
return iterateNormalLoaders(processOptions, loaderContext, returnArgs, pitchingCallback);});
}
// 迭代执行pitch loader
function iteratePitchingLoaders(processOptions, loaderContext, pitchingCallback) {
// 说明所有的loader的pitch都已经执行完成
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
// 开始处理资源文件
return processResource(processOptions, loaderContext, pitchingCallback);}
const currentLoader = loaderContext.loaders[loaderContext.loaderIndex];
if (currentLoader.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(processOptions, loaderContext, pitchingCallback);}
let pitchFn = currentLoader.pitch;
// 不管pitch函数有没有,都把这个pitchExecuted设置为true
currentLoader.pitchExecuted = true;
// 如果pitch函数不存在,则递归iteratePitchingLoaders
if (!pitchFn) {
return iteratePitchingLoaders(processOptions, loaderContext, pitchingCallback);}
//如果pitchFn有值 以同步或者异步调用pitchFn方法,以loaderContext为this指针
runSyncOrAsync(
pitchFn,
loaderContext,
[loaderContext.remainingRequest, loaderContext.previousRequest, loaderContext.data],
(err, ...args) => {
// 判断有没有返回值, 如果有返回值,需要掉头执行前一个loader的normal
if (args.length > 0 && args.some(item => item)) {
loaderContext.loaderIndex--;
iterateNormalLoaders(processOptions, loaderContext, args, pitchingCallback);
} else {
// 如果没有返回值,则继续执行下一个pitch loader
return iteratePitchingLoaders(processOptions, loaderContext, pitchingCallback);
}
});
}
function runSyncOrAsync(fn, loaderContext, args, runCallback) {
let isSync = true; // 默认loader的执行是同步的
let isDone = false; // 表示此loader函数是否已经执行完成
loaderContext.callback = (err, ...args) => {
if (isDone) {
// runCallback 只能调用一次
throw new Error('async(): The callback was already called.');
}
isDone = true;
runCallback(err, ...args);};
loaderContext.async = () => {
isSync = false; // 把isSync是否同步执行的标志从同步变成异步
return loaderContext.callback;};
let result = fn.apply(loaderContext, args);
if (isSync) {
// 如果isSync同步的话,由本方法直接调用runCallback,用来执行下一个loader
isDone = true;
runCallback(null, result);}
}
function runLoaders(options, finalCallback) {
debugger;
const { resource, loaders = [], context = {}, readResource = fs.readFile } = options;
const loaderObjects = loaders.map(createLoaderObject);
const loaderContext = context; // 会成为loader执行过程中的this指针
loaderContext.resource = resource; //要加载的资源文件路径
loaderContext.readResource = readResource; // 读取资源文件内容的方法,默认是fs.readFile
loaderContext.loaders = loaderObjects;
loaderContext.loaderIndex = 0; // 当前正在执行的loader的索引
loaderContext.callback = null; // 调用callback可以让当前的loader执行结束,并且向后续 的loader传递多个值
loaderContext.async = null; // 是内置方法,可以把loader的执行从同步变成异步
// 所有的loader加上resouce。
// 假设当前有loader1、loader2和loader3三个loader用来处理file.js文件
Object.defineProperty(loaderContext, 'request', {
get() {
// loader1!loader2!loader3!file.js
return loaderContext.loaders
.map(loader => loader.path)
.concat(loaderContext.resource)
.join('!');
},});
// 从当前的loader下一个开始一直到结束,加上要加载的资源。 假设当前执行到loader2
Object.defineProperty(loaderContext, 'remainingRequest', {
get() {
//loader3!file.js
return loaderContext.loaders
.slice(loaderContext.loaderIndex + 1)
.map(loader => loader.path)
.concat(loaderContext.resource)
.join('!');
},});
// 从当前的loader开始一直到结束 ,加上要加载的资源
Object.defineProperty(loaderContext, 'currentRequest', {
get() {
//loader2!loader3!file.js
return loaderContext.loaders
.slice(loaderContext.loaderIndex)
.map(loader => loader.path)
.concat(loaderContext.resource)
.join('!');
},});
// 从第一个到当前的loader的前一个
Object.defineProperty(loaderContext, 'previousRequest', {
get() {
// loader1
return loaderContext.loaders
.slice(0, loaderContext.loaderIndex)
.map(loader => loader.path)
.concat(loaderContext.resource)
.join('!');
},});
Object.defineProperty(loaderContext, 'data', {
get() {
return loaderContext.loaders[loaderContext.loaderIndex].data;
},});
let processOptions = {
resourceBuffer: null, // 存放着要加载的模块的原始内容
readResource, // 读取文件的方法,默认值是fs.readFile};
// 开始从左向右迭代执行loader的pitch
iteratePitchingLoaders(processOptions, loaderContext, (err, result) => {
finalCallback(err, {
result,
resourceBuffer: processOptions.resourceBuffer,
});});
}
exports.runLoaders = runLoaders;
更多细节请参考 webpack loader-runner
接下来我们来实现一些常用loader,注意这里只是简单的实现,主要是实现的是主要功能,一些细节和边界条件可能不会考虑。
const babel = require('@babel/core');
const path = require('path');
function loader(source) {
let options = this.getOptions();
const { code } = babel.transformSync(source, options);
return code;
}
module.exports = loader;
在webpack5中,已经在loaderContext
中添加了这个getOptions
方法,webpack5之前并不存在this.getOptions
方法,需要额外通过loader-utils
这个包实现获取外部loader
配置参数。
function loader(source) {
let filename = Date.now() + ".png";
//用于向输出目录里写一个新的文件
this.emitFile(filename, source);
return `module.exports = ${JSON.stringify(filename)}`;
}
loader.raw = true;
module.exports = loader;
webpack5以前加载图片等二进制文件需要使用file-loader或者 url-loader, 但是在webpack5中不再需要了。
const path = require('path');
const mime = require('mime');
function loader(content) {
// content默认格式是字符串
let options = this.getOptions(this) || {};
let { limit = 8 * 1024, fallback = 'file-loader' } = options;
const mimeType = mime.getType(this.resourcePath); // image/jpeg
if (content.length < limit) {
let base64Str = `data:${mimeType};base64,${content.toString('base64')}`;
return `module.exports = ${JSON.stringify(base64Str)}`;} else {
let fileLoader = require(fallback);
return fileLoader.call(this, content);}
}
//如果你不希望webpack帮你把内容转成字符串的的话需要加上loader.raw=true;,这样的话content就是一个二进制的Buffer
loader.raw = true;
module.exports = loader;
const less = require('less');
function loader(lessSource) {
let cssSource;
// 如果调用了this.async方法,就会把loader的执行从同步变成异步,只有当你手工调用callback的时候才会认为此loader执行结束
// const callback = this.async();
less.render(lessSource, { filename: this.resource }, (err, output) => {
cssSource = output.css;
// callback(null, cssSource);
this.callback(null, cssSource, output.map, output.ast);});
//如果返回值只有一个的话可以用return
//return cssSource;
//return `module.exports=${JSON.stringify(cssSource)}`;
//return `module.exports = "#root{color:red}"`
}
module.exports = loader;
// 在真正的less-loader中返回并不是css文本内容,而也是返回的js
// 一个用来分析css ast的库
const postcss = require('postcss');
const Tokenizer = require('css-selector-tokenizer');
function loader(inputSource) {
let loaderOptions = this.getOptions(this) || {};
let callback = this.async();
// postcss 插件
const cssPlugin = options => {
return root => {
if (loaderOptions.import) {
// 删除所有的 @import语句并且把导入的CSS文件路径添加到options.imports里
root.walkAtRules(/^import$/i, rule => {
rule.remove(); //在css脚本里把 @import删除
options.imports.push(rule.params.slice(1, -1)); // ./index.css
});
}
if (loaderOptions.url) {
// 遍历语法树,找到里面所有的url
root.walkDecls(decl => {
let values = Tokenizer.parseValues(decl.value);
values.nodes.forEach(node => {
node.nodes.forEach(item => {
if (item.type === 'url') {
// stringifyRequest可以把任意路径标准化为相对路径
let url = loaderUtils.stringifyRequest(this, item.url);
item.stringType = "'";
item.url = '`+require(' + url + ')+`';
}
});
});
let value = Tokenizer.stringifyValues(values);
decl.value = value;
});
}
};};
// 将会用它来收集所有的 @import
let options = { imports: [] };
let pipeline = postcss([cssPlugin(options)]);
// 开始编译css
pipeline.process(inputSource).then(result => {
let { importLoaders = 0 } = loaderOptions;
let { loaders, loaderIndex } = this; // 所有的loader数组和当前loader的索引
let loadersRequest = loaders
.slice(loaderIndex, loaderIndex + 1 + importLoaders)
.map(x => x.request)
.join('!'); // request是loader绝对路径
// -! 不要前置和普通 loader
// loader-utils中的stringifyRequest方法,可以将绝对路径转化为相对路径。
// loader.js=> ./src/loader.js
let importCss = options.imports
.map(url => `list.push(...require(` + loaderUtils.stringifyRequest(this, `-!${loadersRequest}!${url}`) + `));`)
.join('\r\n');
let script = `
var list = [];
list.toString = function(){return this.join('')}
${importCss}
list.push(`${result.css}`);
module.exports = list.toString();
`;
callback(null, script);});
}
module.exports = loader;
通常我们在使用style-loader时,都会和css-loader配合使用。通常配置如下
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
context: process.cwd().replace(/\/g, '/'),
output: {
path: path.resolve('dist'),
filename: 'main.js',},
resolveLoader: {
modules: ['my-loaders', 'node_modules'],},
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),],
};
src/index.js
import './index.css'
src/index.css
#root {
width: 200px;
height: 200px;
background-color: red;
}
function loader(cssSource) {
console.log(cssSource)
const script = `
let style = document.createElement("style");
style.innerHTML = ${JSON.stringify(css.replace('\n', ''))};
document.head.appendChild(style);
`;
return script;
}
执行webpack进行编译
npx webpack
然后打开编译好的dist/index.html,但是并没有发现root变红,这是什么原因呢?
样式文件首先会经过css-loader
的处理之后才会交给style-loader
处理。我们在style-loader中打印出css-loader编译后的结果如下(这里使用的是原生的css-loader):
如果使用我们自己实现的css-loader,则打印的结果如下:
其实本质上差不多,cssSource的内容是一个js脚本,我们将js脚本的内容插入到style element中,当然样式是不会生效的。
dist/index.html
的内容
这说明如果我们将style-loader
设计为normal loader
的话,我们需要执行css-loader
返回的js
脚本,并且获得它导出的内容才可以得到对应的样式内容。比如:
function loader(cssSource) {let module = { exports: {} };eval(cssSource);const script = ` const style = document.createElement("style"); style.innerHTML = ${JSON.stringify(module.exports)}; document.head.appendChild(style);`;return script;
}
module.exports = loader;
那么此时我们需要在style-loader
的normal
阶段实现一系列js
的方法才能执行js
并读取到css-loader
返回的样式内容,这无疑是一种非常糟糕的设计模式。
style-loader
设计为pitch loader
function loader(cssSource) {};
loader.pitch = (remainingRequest) => {
const script = `
import style from "!!${remainingRequest}";
let styleEle = document.createElement('style');
styleEle.innerHTML = style;
document.head.appendChild(styleEle);
`;
return script;
}
module.exports = loader;
如果我们在style-loader的pitch函数中直接返回值的话,会发生熔断效果,此时的执行顺序如下:
webpack获取到style-loader pitch的返回值,然后进行解析,webpack发现里面存在import语句,
import style from "!!${remainingRequest}"
然后就再次使用loader去编译引入的文件,此时remainingRequest的值为css-loader!index.css
。为什么我们要加上!!
前缀呢?加上这个前缀代表我们不再找配置中的loader,仅仅使用这里的inline loader,这里也就是css-loader normal。如果不加的话又会去找配置文件中的style-loader,又会重新执行其pitch函数,这样就会发生死循环。
最后使用css-loader的normal去获取到真正要插入到style标签的文本,然后生成一个标签并插入页面。
重新打包并打开页面,看到页面可以正常显示样式了!
如果对webpack整体编译流程感兴趣的话,可以查看这篇webpack核心流程解析与简单实现,对你理解这个例子更加有帮助
通过上述style-loader
的实现过程,我们可以发现如果在loader开发的过程中你需要依赖上一个loader,然后上一个loader的normal函数返回的并不是处理后的资源文件内容而是一串js脚本,那么此时相较于normal loader将你的loader编写为一个pitch loader应该是更好的方式。