preface

Webpack is an indispensable topic and tool in front end nowadays. I believe that many students have felt the fear of being dominated by Webpack, but to have a deep understanding of it may be the first step to get rid of the suffering sea.

In the system of Webpack, Loader and Plugin are undoubtedly the most core components. This paper will introduce the difference between Loader and Plugin combined with the operation mechanism of Webpack, and conduct an in-depth analysis of Loader.

The difference between Loader and Plugin

When it comes to Loader, there is definitely a question: what is the difference between Loader and Plugin?

In a nutshell:

Loader is a module converter that converts non-JS modules into JS modules.

Plugins are events mounted at various stages of the WebPack run life cycle that are triggered at specified time points (equivalent to subscription/publish mode) and can change build results, split, optimize bundles, and so on.

The configuration of Loader and Plugin can be seen from the way that loader is configured. Loader is configured to use the specified Loader to compile when declaring test for a certain file type, while plugin is instantiated (new) to mount the plug-in to the specified life cycle node.

Loader and plugin in the Webpack running mechanism

Take a look at the webPack running process diagram:

As you can see, the Loader is only used to compile static resources and is called during Compilation cycles.

So where’s the plugin?

Webpack broadcasts events in each lifecycle and triggers the corresponding plug-in, as shown by the blue dotted arrow below:

Loader,

What is the Loader

Loader is a converter with a single responsibility.

There are two key words here, converter and single responsibility.

Let’s look at the converter. As we know, everything in Webpack is JS module, and loader’s function is to convert non-JS modules into JS modules for Webpack processing.

The js modules, namely the style file (CSS,. Less,. SCSS, etc.), non-standard js file (. Ts,. JSX,. Vue), and other types of files (SVG, PNG | JPG | jpeg, etc.).

Single responsibility, literally, means that a loader is responsible for only one transformation. The single responsibility is a constraint on the Loader definition by the Webpack community. If a source file needs to go through a multi-step conversion to be used, it should be converted through multiple Loaders.

Of course, you can write only one loader to implement all tasks, but it is not recommended that you do so.

Loader writing

Module. exports is a function js module:

// my-loader.js
module.exports = function (content, map, meta) {... };Copy the code

Webpack will inject three parameters into the loader:

  • content: The contents of the resource file. For the start loader, this parameter is the only one
  • map: Source map generated by previous loaders can be shared by previous loaders
  • meta: Other information to be shared by the rear Loader can be customized

Is Loader a pure function?

By definition and responsibility, loader is implemented much like a pure function. It inputs a file and outputs the converted content to the next loader or to WebPack. Loader is not a pure function for two main reasons:

A loader has an execution context, that is, this accesses built-in properties and methods to implement specific functions.

The loader return statement may not return.

The kinds of Loader

Loaders fall into four categories:

  • Front (Pre)
  • Normal
  • Post
  • Inline

You can specify the loader type by using the rule-enforce attribute in the configuration file. The default value is empty, indicating normal. The value can be Pre or Post.

Types also affect the execution order of the Loader, as described below.

module: {
  rules: [{test: /\.js$/,
      use: ['pre-loader'].enforce: 'pre'}, {test: /\.js$/,
      use: ['normal-loader'],}, {test: /\.js$/,
      use: ['post-loader'].enforce: 'post',]}}Copy the code

There are also special inline loaders that call the Loader directly from the import/require statement.

/ / use! Separate the loaders in the resource
import Styles from 'style-loader! css-loader? modules! ./styles.css';

// Use Inline loader! And!!!!! , -! Prefix to disable part of the Loader in the configuration file
import Styles from '! style-loader! css-loader? modules! ./styles.css';
import Styles from '!!!!! style-loader! css-loader? modules! ./styles.css';
import Styles from '-! style-loader! css-loader? modules! ./styles.css';
Copy the code
The prefix role
! Disable the configured Normal Loader
!!!!! Disable all Loaders (pre, Normal, post) in the configuration.
-! The Pre loader and Normal Loader are disabled

The Inline loader can also pass parameters to the Loader in URL query or JSON mode

import Styles from 'style-loader? key=value&foo=bar! css-loader? modules! ./styles.css';
/ / or
import Styles from 'style-loader? {"key": "value", "foo": "bar"}! css-loader? modules! ./styles.css';
Copy the code

Equivalent to a configuration file:

[{loader: 'style-loader'.options: { key: 'value'.foo: 'bar'}}, {loader: 'css-loader'.options: { modules: true}},]Copy the code

Tip:

It is recommended to use module.rules instead of inline loaders as much as possible to reduce the amount of boilerplate code in source code and reduce system maintenance costs. Inline-loader is used only for modules that require special processing.

Input and output

By default, resource files are converted to UTF-8 strings and passed to the Loader. The loader can receive the raw Buffer by setting raw to true.

module.exports = function (content) {
  return someSyncOperation(content);
};

module.exports.raw = true;
Copy the code

The output of the loader must be a String or Buffer.

Synchronous and asynchronous

The Loader can be synchronous or asynchronous.

Synchronous writing:

Call this.callback() or return directly; The benefit of this.callback is that more content arguments can be passed.

module.exports = function (content, map, meta) {
  const output = someSyncOperation(content);

  return output;
  // or
  this.callback(null, output, map, meta);
  return;
};
Copy the code

Asynchronous writing:

Get the callback method with this.async().

module.exports = function (content, map, meta) {
  const callback = this.async();

  someAsyncOperation(content, function (err, result, sourceMaps, meta) {
    if (err) return callback(err);

    callback(null, result, sourceMaps, meta);
  });
};
Copy the code

Official tip:

Loader was originally designed to run in charge both synchronously (e.g. Node.js) and asynchronously (e.g. Webpack). However, synchronous computation is too time-consuming to do in a single-threaded environment such as Node.js, and we recommend making your Loader as asynchronous as possible. However, if the computation is small, the synchronous loader can also be used.

The cache

By default, webpack will cache the output results of the loader input and related dependence (by enclosing addDependency or enclosing addContextDependency add) did not change, will return the same result.

You can disable caching by executing this.cacheable(false) in the Loader.

cacheable(flag = true: boolean)
Copy the code

Execution order

Loader.js (); loader.js (); loader.js ();

// webpack.config.js

rules: [{test: '/\.js$/'.use: ['a-loader'.'b-loader'.'c-loader'],},],Copy the code

C-loader => B-loader => A-loader (from right to left)

The answer is both right and wrong. This is true because most of the time the loader does execute in this order; This is not true because the information given in the above example is not complete enough to infer the actual order of execution.

Pitch and Normal

Loader execution consists of two phases, pitch phase and Normal phase.

The Normal stage is the stage when the loader translates the source files.

The Pitch phase precedes the normal phase. If the Loader defines a Pitch method, it will be executed in Pitch phase. If the pitch method of the Loader returns the content, the pitch and normal phases of the rear loader are skipped.

The actual execution sequence of the loader is as follows:

If loader-b’s pitch method returns, the execution sequence will be:

Loader-a (pitch)=> Loader-b (pitch)=> Loader-A (normal)

The Pitch method is written as follows:

module.exports = function (content) {
  return someOperation(content);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    returnsomeContent; }};Copy the code

Webpack passes the pitch method three arguments:

  • remainingRequest: Request string of the loader following itself in the loader chain
  • precedingRequest: Request string of the loader that precedes it in the loader chain
  • data: data object, which can be obtained by this.data during the Normal phase and can be used to pass shared information

Request string: Loader and the absolute path of the target resource file start with “!” A concatenated string similar to the require path of an inline loader. Such as:

// remainingRequest
// In the Loader chain, there are also CSS-loader and less-loader after the current Loader, and the target file to be translated is index.less
'/src/project/node_modules/css-loader/index.js! /src/project/node_modules/less-loader/dist/cjs.js! /src/project/src/styles/index.less'
Copy the code

Use of the data parameter:

module.exports = function (content) {
  console.log(this.data.value) / / 42
  return someOperation(content);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.value = 42;
};
Copy the code

Description of pitch in official documentation:

In some cases, the loader only cares about the metadata following the request and ignores the results of the previous loader

Pitch and normal stages can be understood as capture and bubble stages of DOM event mechanism in JS. Pitch method provides loader with the ability to skip certain execution stages.

Real order

The actual execution order of Loader depends on the type of Loader, pitch method, and inline-loader prefix.

The execution priorities of loader types are as follows: Pre Loader > Inline Loader > Normal Loader > Post Loader.

For example, there is the following WebPack configuration:

module: {
    rules: [{test: /\.js$/,
            use: ['pre-loader'].enforce: 'pre'}, {test: /\.js$/,
            use: ['normal-loader-a'.'normal-loader-b'],}, {enforce: 'post'.test: /\.js$/,
            use: ['post-loader'],},],}Copy the code

And inline-loader is called in the js file

// demo.js

const someModule = import('inline-loader-a! inline-loader-b! ./someModule.js');
Copy the code

For somemodule.js, the call chain of the loader that processes it is: [‘pre-loader’, ‘inline-loader-a’, ‘inline-loader-b’, ‘normal-loader-a’, ‘normal-loader-b’, ‘post-loader’]

If a call to inline-loader in a js file is prefixed:

// demo.js

const someModule = import('-! inline-loader-a! inline-loader-b! ./someModule.js'); / / use -! Prefix disables pre loader and Normal Loader in the configuration
Copy the code

Then the order of execution will be:

If the pitch method of inline-loader-b returns a value, the order of execution will change to:

loader-runner

The implementation of Loader is essentially a method. After inputting a module, loader-Runner is responsible for organizing and calling Loader inside Webpack. The workflow is as follows:

Loader context

Loader has a runtime context that allows you to access some properties and methods through this. Here are some common ones (as of Webpack V5.24.2)

this.getOptions

Gets the options passed to the loader in the configuration file.

this.callback

this.callback(
  err: Error | null.content: string | Buffer, sourceMap? : SourceMap, meta? : any );Copy the code
  • SourceMap: Returns the Source map generated during this transformation.
  • Meta: Additional information generated in this conversion, customizable. For example, if an AST is generated during source file conversion, you can send the AST to a subsequent Loader to avoid the performance degradation caused by repeated generation of the AST loader.

this.async

Tell loader-runner that the loader will call back asynchronously. Return this. The callback

this.request

A parsed request string, similar to an Inline loader call, as in:

“/abc/loader1.js? xyz! /abc/node_modules/loader2/index.js! /abc/resource.js? rrr”

this.loaders

Loader call chain array

this.addDependency

Add a file as a dependency of loader. If loader has cache enabled, the change of this file will invalidate the cache and invoke Loader again.

For example, sas-loader and less-loader use this method and recompile when it finds that the imported CSS file has changed.

this.addContextDependency

Add a directory as a loader dependency.

this.sourceMap

You can use this.sourcemap () to check whether the source map is required in the configuration.

this.emitFile

emitFile(name: string, content: Buffer|string, sourceMap: {... })Copy the code

Output a file.

See the official Webpack documentation for more context content.

Local development

By default, Webpack only looks for loaders in node_modules. You can make WebPack look for local loaders by modifying resolveloader. modules in the configuration file.

For example, if the loader path in development is./path-to-your-loader/first-loader, the following Settings are required:

// webpack.config.js

module.exports = {
  resolveLoader: {modules: ['node_modules'.'path-to-your-loader'].// Specify which directory webpack should search for loader (in order)}}Copy the code

Custom loaders can then be used in module.rules:

// webpack.config.js

{
  module: {
    rules: [{test: /\.css$/,
        use: [ 'first-loader'],}]}}Copy the code

Loader Development Principles

Finally, the ten Loader development principles recommended by Webpack are attached:

Keep them simple

Loaders should only do a single task. Not only does this make each loader easy to maintain, but it can also be chain-called in more scenarios.

Use Utilize chaining

Take advantage of the loader’s ability to chain calls. Write five simple Loaders for five tasks instead of one loader for five tasks. Feature isolation not only makes loaders simpler, it may also allow them to be used for functionality you didn’t think of before.

Emit Modular Output

Ensure that the output is modular. The modules generated by Loader comply with the same design principles as common modules.

Make sure they’re stateless

Ensure that Loader does not save state between module transitions. Each run should be independent of other compiled modules and previous compilations of the same module.

Use Loader Utilities (Employ Loader Utilities)

Take advantage of the Loader-utils package, which provides many useful tools.

Mark Loader Dependencies

If a Loader uses an external resource (such as reading from a file system), it must be declared. This information is used to invalidate the cache loaders and to recompile in Watch mode.

Resolve Module dependencies

Depending on the module type, there may be different pattern-specified dependencies that should be resolved by the module system.

There are two ways to parse a module:

  • By converting them torequirestatements
  • usethis.resolveFunction parsing path

Css-loader is an example of the first approach. It replaces the @import statement with require to reference other style files, changing the URL (…) Replace this with require to reference the file, thereby converting the dependency to the require declaration.

Extract Common Code

Avoid general-purpose code generation in every module handled by the Loader. Instead, you should create a runtime file in the Loader and generate require statements to reference the shared module.

Avoid absolute paths

Do not insert absolute paths in module code, because when the project root path changes, so does the file absolute path. The stringifyRequest method in loader-utils converts an absolute path to a relative path.

Peer Dependencies (Use Peer Dependencies)

If your loader relies on another package, you should introduce this package as a peerDependency, so that developers using your package can better manage the dependencies.