The target

  • Understand the functions and principles of Webpack Loader
  • Independently develop the Loader required

define

A loader is just a JavaScript module that exports a function.

Loader is simply a JavaScript module exported as a function.

Loader code structure:

The simplest loader code structure is:

module.exports = function(source) {
  return source;
};
Copy the code
  • The first loader passes in only one argument: the contents of the resource file.
  • An optional SourceMap result (formatted as a JSON object) can also be passed.

loader runner

Before getting started with the Loader workflow, let’s take a look at what a Loader Runner is. Loader Runner acts as a dependency on WebPack and is used in Webpack to execute the loader. In addition, loader-Runner allows loaders to run without installing webpack.

Loader-runner usage:

import { runLoaders } from 'loader-runner';

runLoaders({
  resource: '/abs/path/to/file.txt? query'.// String: The absolute path to the resource (you can add a query String)
  loaders: ['/abs/path/to/loader.js? query'].// String[]: absolute path of loader (can add query String)
  context: { minimize: true }, // Additional Loader context in addition to the base context
  readResource: fs.readFile.bind(fs) // A function that reads resources
}, function(err, result) {
  // err: Error?
  // result.result: Buffer | String
})
Copy the code

Note that both resource and loaders are absolute paths

To use loader Runner, write a simple loader to read the contents of demo.txt:

Raw – loader. Js file:

module.exports = function(source) {
  const json = JSON.stringify(source)
  return json;
};
Copy the code

The demo. TXT file:

I am just a demo.
Copy the code

Index. Js file:

const fs = require('fs');
const path = require('path');
const { runLoaders } = require('loader-runner');

runLoaders({
  resource: path.resolve(__dirname, 'demo.txt'),
  loaders: [path.resolve(__dirname, 'raw-loader.js')].readResource: fs.readFile.bind(fs),
}, (err, result) = > (
  err ? console.error(err) : console.log(result)
));
Copy the code

Finally, execute the program using index.js:

Loader overall working process

The Loader Runner calls the loader(the exported function) and passes in the results or resource files generated by the previous loader. And so on, after layer upon layer of processing, transmission. The last loader needs to pass the resulting processing results to compiler. The result should be a String or Buffer (converted to a String) that represents the JavaScript source code for the module.

Execution sequence of multiple Loaders

If there are multiple loaders configured, what is the order in which loaders are executed? Conclusion: Multiple loaders are executed sequentially, from back to front. Verify the conclusion with a specific example:

Configure in webpack.config.js:

{
  test: /\.js$/,
  use: [
    path.resolve('./src/loader/a-loader.js'),
    path.resolve('./src/loader/b-loader.js')]}Copy the code

B – loader. Js file:

module.exports = function(source) {
  console.log('Loader b is excuted! ');
  return source;
}
Copy the code

A – loader. Js file:

module.exports = function(source) {
  console.log('Loader a is excuted! ');
  return source;
}
Copy the code

Finally, execute webpack compilation:

It can be clearly seen that b-loader is executed before A-Loader, which verifies the conclusion that multiple Loaders are executed in serial order from back to front. Multiple Loaders always comply with the following rules:

  • The loader last in the configuration order is called first, and the arguments passed to it are the contents of the source;
  • Loaders in the middle are called by chain, passing them the return value of the previous loader, providing input for the next loader;
  • The loader at the beginning of the configuration sequence is called last, passing the resulting processing results to compiler.

The above process is similar to:

  • In the Unix pipline
  • In the FPcompose = (f, g) => (... args) => f(g(... args));(WebPack’s most similar process)

Obtain loader parameters

There are many apis for configuring loaders. If you need to obtain the parameters of these Loader configurations, you also need to use the Loader-utils that webPack relies on. Here’s an example of how it can be used.

webpack.config.js

{
  test: /\.js$/,
  use: [
    path.resolve('./src/loader/a-loader.js'),
    {
      loader: path.resolve('./src/loader/b-loader.js'),
      options: {
        name: 'loader b'}}}]Copy the code

A-loader. js remains the same as the example above. B-loader. js needs to be adjusted as follows:

module.exports = function(source) {
  const { name } = loaderUtils.getOptions(this);
  console.log(name);
  return source;
};
Copy the code

Execute the webpack compilation and you can see that the name configured in option has been printed:

Synchronous/asynchronous Loader

Loaders can be classified into synchronous and asynchronous. The loaders described above are synchronous loaders because their processing flows are synchronous and the results are returned after conversion. What if a process is asynchronous, or has to be asynchronous because synchronous processing would block the current process? Here’s an example:

Webpack.config. js and a-loader.js are the same as the above example.

Modify the code for b-loader.js:

const loaderUtils = require('loader-utils');

module.exports = function (source) {
  const options = loaderUtils.getOptions(this);
  const callback = this.async();

  setTimeout(() = > {
    const result = source.replace('world', options.name);
    callback(null, result);
  }, 1000);
}
Copy the code

The index.js file to compile is as follows:

const a = 'hello world';
Copy the code

Hello world is converted to Hello loader b:

Loader asynchronous processing need to use this. Async, it returns is an asynchronous function, the first parameter is the Error, the second parameter is the result of processing.

Loader Exception Handling

  • Throw directly from the loader
  • Pass an error through this.callback

This. Callback can be used not only to pass errors, but also to return multiple results. Its full API is as follows:

this.callback(
  err: Error | null.content: string | Buffer, sourceMap? : SourceMap, meta? : any );Copy the code
  1. The first argument must be Error or NULL
  2. The second argument is a string or Buffer
  3. Optional: The third parameter must be a source map that can be parsed by the module
  4. Optional: The fourth option, which is ignored by Webpack, can be anything (such as some metadata)

Loader processes binary data

By default, webpack passes the original content to loader in UTF-8 format. However, in some scenarios, loaders process binary files, such as file-loader.

module.exports = function(source) {
    // At exports.raw === true, the source that Webpack passes to Loader is of type Buffer
    source instanceof Buffer === true;
    // The type returned by the Loader can also be a Buffer
    / / in exports raw! If == true, the Loader can also return the result of type Buffer
    return source;
};

// Tells Webpack whether the Loader needs binary data via the exports.raw property
module.exports.raw = true;
Copy the code

Module.exports. raw = true; To enable loader to retrieve binary data, otherwise it must be a string

Loader file Output

To write a file to this. EmitFile, print a file:

const loaderUtils = require("loader-utils");

module.exports = function(source) {
  const url = loaderUtils.interpolateName(this."[hash].[ext]", {
    source,
  });
  this.emitFile(url, source);
  const path = `__webpack_public_path__ + The ${JSON.stringify(url)}; `;
  return `export default ${path}`;
};
Copy the code

Use caching in the Loader

If the transformation is repeated with each build, the build becomes very inefficient. To do this, WebPack caches the results of all loaders by default (dependent loaders cannot use caching). Only when the processed file or its dependent file changes, the corresponding Loader will be called again to perform the conversion operation. You can manually turn off the cache using this.cacheable(false).

Write loader tips

1. The modular

Since loader is an exported module, it should follow the specifications and design principles of the written module.

2. Single responsibility

The SRP principle is that an object (method) does only one thing. In addition, modules should be independent of each other.

3. Chain combination

Under the premise of a single responsibility, use chain combination to achieve the goal of writing loader.

4. Path problems

Do not write absolute paths in the Loader module, because when the project root path changes, these paths will interfere with Webpack’s hash (converting the module path to the Module reference ID). Loader-utils has a stringifyRequest method that converts absolute paths to relative paths.

Write application scenarios of loader

  • Uniformly for functiontry... catch..Processing;error-captured-loader
  • Internationalization, specific content conversion, etc.
  • more…