Frontend Development 9 min read

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.

Didi Tech
Didi Tech
Didi Tech
Webpack Loader Configuration and Matching Rules – Detailed Explanation

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 + options

After 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.

frontendJavaScriptConfigurationWebpackBuild ToolsloaderRule Matching
Didi Tech
Written by

Didi Tech

Official Didi technology account

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.