Webpack Loader Configuration and Matching Rules – Detailed Explanation
The article explains Webpack loader configuration by detailing rule definitions, inline loader syntax, the symbols that control loader inclusion, the internal parsing and matching process that groups pre, normal, and post loaders, and practical tips for ordering and debugging complex builds.
This article continues the Webpack series by focusing on loader configuration, matching rules, and the execution flow of loaders. It is divided into three parts: (1) basic configuration and matching rules, (2) detailed execution of loader parsing, and (3) practical examples.
1. Loader Basic Configuration and Matching Rules
Webpack allows two ways to specify loaders: through the module.rules array in webpack.config.js or by using inline loaders directly in the import statement.
// webpack.config.js
module.exports = {
...
module: {
rules: [{
test: /.vue$/,
loader: 'vue-loader'
}, {
test: /.scss$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
data: '$color: red;'
}
}
]
}]
}
...
}When using inline loaders, the syntax looks like:
import 'style-loader!css-loader!stylus-loader?a=b!../../common.styl';The symbols ! , -! , and !! control which loaders from the configuration are applied:
! – ignore normal loaders defined in the config.
-! – ignore pre‑loaders and normal loaders.
!! – ignore pre‑, normal‑, and post‑loaders.
2. Loader Parsing Execution Details
During module creation, NormalModuleFactory creates a RuleSet instance based on the rules defined in the configuration. The factory then resolves which loaders apply to a given request.
class NormalModuleFactory {
... // internal resolver hooks
this.hooks.factory.tap('NormalModuleFactory', (result, callback) => { ... })
this.hooks.resolver.tap('NormalModuleFactory', () => (result, callback) => { ... })
...
}Inline loader handling starts by stripping the special prefixes and splitting the request:
// NormalModuleFactory.js
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
let elements = requestWithoutMatchResource
.replace(/^-?!+/, "")
.replace(/!!+/g, "!")
.split("!");
let resource = elements.pop(); // resource path
elements = elements.map(identToLoaderRequest); // loader + optionsAfter the inline loaders are parsed, the RuleSet.exec method matches the remaining configuration rules:
const result = this.ruleSet.exec({
resource: resourcePath,
realResource: matchResource !== undefined ? resource.replace(/\?.*/, "") : resourcePath,
resourceQuery,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler
});The matched rules are then grouped by their enforce type (pre, normal, post) and merged into the final loader list:
for (const r of result) {
if (r.type === "use") {
if (r.enforce === "post" && !noPrePostAutoLoaders) {
useLoadersPost.push(r.value);
} else if (r.enforce === "pre" && !noPreAutoLoaders && !noPrePostAutoLoaders) {
useLoadersPre.push(r.value);
} else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
} else if (typeof r.value === "object" && r.value !== null && typeof settings[r.type] === "object" && settings[r.type] !== null) {
settings[r.type] = cachedMerge(settings[r.type], r.value);
} else {
settings[r.type] = r.value;
}
}Finally, the factory resolves each group of loaders in parallel and concatenates them in the order post → inline → normal → pre (the actual execution order of pitch methods is post → inline → normal → pre ):
asyncLib.parallel([
[
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, loaderResolver),
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, loaderResolver),
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, loaderResolver)
],
(err, results) => {
// results[0] -> postLoader
// results[1] -> normalLoader
// results[2] -> preLoader
loaders = results[0].concat(loaders, results[1], results[2]);
process.nextTick(() => { /* create module */ });
}
]);3. Practical Usage
The article concludes that understanding both configuration forms and the matching process is essential for correctly ordering loaders, customizing inline loaders for special modules, and debugging complex build pipelines.
Didi Tech
Official Didi technology account
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.