Webpack Loader API

Loader is an important concept in Webpack that can help us do what we want when loading certain types of files, such as automatically adding try catch to all JavaScript functions.

The so-called loader is just a JavaScript module exported as a function.loader runner The function will be called, and then the result of the previous loader or resource file will be passed in. The’this’ context of the function will be filled by webpack, and loader runner There are some useful methods to change the loader to be called asynchronously, or to obtain query parameters.

The first loader has only one input parameter: the content of the resource file. The compiler needs to get the processing result produced by the last loader. This processing result should be’String ‘or’Buffer’ (converted to a string), representing the JavaScript source code of the module. In addition, an optional SourceMap result (formatted as a JSON object) can be passed.

If it is a single processing result, it can be returned directly in ** Synchronization Mode **. If there are multiple processing results, 'this.callback () ’ must be called. In ** Asynchronous Mode **, 'this.async () ’ must be called to indicate loader runner Waiting for the asynchronous result, it will return’this.callback () ‘callback function, and then the loader must return’undefined’ and call the callback function.

The simplest loader

Synchronization

Either’return ‘or’this.callback’ can return the converted’content 'content synchronously:

sync-loader.js

1
2
3
module.exports = function(content, map, meta) {
return someSyncOperation(content);
};

The this.callback method is more flexible because it allows multiple parameters to be passed, not just content.

The content here is the incoming source code. someSyncOperation is our custom method for processing the source code, and finally returns the processing result through return.

sync-loader-with-multiple-results.js

1
2
3
4
module.exports = function(content, map, meta) {
this.callback(null, someSyncOperation(content), map, meta);
Return;//always returns undefined when calling callback ()
};

The map here is actually scourceMap.

Asynchronous

For asynchronous loaders, use this.async To get the callback function:

async-loader.js

1
2
3
4
5
6
7
module.exports = function(content, map, meta) {
Var callback = this.async (); // tell the loader-runner to wait for the return of the function
someAsyncOperation(content, function(err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};

async-loader-with-multiple-results.js

1
2
3
4
5
6
7
module.exports = function(content, map, meta) {
var callback = this.async();
someAsyncOperation(content, function(err, result, sourceMaps, meta) {
if (err) return callback(err);
callback(null, result, sourceMaps, meta);
});
};

This. aysnc told loader-runner The loader will call back asynchronously. Returns’this.callback '. So its parameters are the same as this.callback.

Loader

Here only the commonly used methods and parameters are listed, explained and added some notes. More configuration can be seen directly文档

The loader context represents some method or property that can be accessed within the loader using’this’.

Suppose we request to load another module like this: In ‘/abc/file.js’:

1
require('./loader1?xyz!loader2!./resource?rrr');

The above import method is a method provided by webpack. This import method specifies which loader is used to load the final file. The loader is specified before the exclamation mark. This method can override the configuration in webpack.config.

You can see the specifics.loader 概念文档

this.query`

  1. If this loader is configured options Object, ‘this.query’ points to this option object.
  2. If there is no’options’ in the loader and it is called with query string as an argument, ‘this.query’ is a string starting with ‘?’.

In the official doc, we are prompted to use the getOptions method provided by loader-utils to get options, because if the value in options is not a simple string, there will occasionally be some problems. The specific usage method is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//webpack config
module.exports = {
rules: [
{
test: ...,
use: [
{
loader: 'loader.js',
options: {
name: 'Ray'
}
}
]
}
]
}
1
2
3
4
5
6
7
8
// loader.js
const loaderUtils = require('loader-utils')

module.exports = function (content) {
const options = loaderUtils.getOptions(this)
console.log(options.name); //Ray
return content;
}

this.callback

A function that returns multiple results and can be called synchronously or asynchronously. The expected parameters are:

1
2
3
4
5
6
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
  1. The first argument must be’Error ‘or’null’
  2. The second parameter is a’string 'or Buffer
    Optional: The third parameter must be one that can be used by这个模块Parsed source map.
  3. Optional: The fourth option, which will be ignored by webpack, can be anything (such as some metadata).

Syntax Tree (abstract)

If this function is called, you should return undefined to avoid ambiguous loader results.

this.async

Tell loader-runner The loader will call back asynchronously. Returns’this.callback '.

this.version

** The version number of the loader API. ** Currently ‘2’. This has some use for backward compatibility. With this version number, you can write different logic for breaking changes between different versions, or downgrade.

this.context

** Directory where the module is located. ** Can be used as a context for resolving paths to other modules.

In our example: this attribute is’/abc ‘because’resource.js’ is in this directory

this.rootContext

Starting from webpack 4, the original’this.options.context ‘has been improved to’this.rootContext’.

this.request

The parsed request string.

In our example:

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

How did this result come about?

First of all, because our resource.js, loader1, loader2 root directory abc directory, so the front will add “abc/”, and loader2 in front of no relative path, so it is considered to be node_modules module, will add a “node_modules/”

Advanced Loader

“Raw”

By default, the resource file will be converted to a UTF-8 string and then passed to the loader. By setting’raw ‘, the loader can receive the original’Buffer’. Each loader can pass its processing results as’String ‘or’Buffer’. Complier will convert them to and from loaders.

raw-loader.js

1
2
3
4
5
6
7
module.exports = function(content) {
assert(content instanceof Buffer);
return someSyncOperation(content);
The return value can also be a'Buffer '.
//Even if it's not a raw loader, it's fine
};
module.exports.raw = true;

Crossed

The loader ** is always ** called from right to left. In some cases, the loader only cares about the ** Metadata (metadata) ** after the request and ignores the result of the previous loader. ** The’pitch 'method on the loader is called from left to right before the actual (right-to-left) execution of the loader **. For the following use Configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
//...
module: {
rules: [
{
//...
use: [
'a-loader',
'b-loader',
'c-loader'
]
}
]
}
};

These steps will occur:

1
2
3
4
5
6
7
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution

So why can loaders take advantage of the “pitching” phase?

First, the’data ‘passed to the’pitch’ method is also exposed under the 'this.data 'during execution and can be used to capture and share previous information during looping.

1
2
3
4
5
6
7
module.exports = function(content) {
return someSyncOperation(content, this.data.value);
};

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

Second, if a loader gives a result in the’pitch ‘method, then the process will turn around and skip the remaining loaders. In our example above, if the’pitch’ method of’b-loader 'returned something:

1
2
3
4
5
6
7
8
9
module.exports = function(content) {
return someSyncOperation(content);
};

module.exports.pitch = function(remainingRequest, precedingRequest, data) {
if (someCondition()) {
return 'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');';
}
};

The above steps will be shortened to:

1
2
3
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution