Reference:

  • Webpack Book — Extending with Loaders.
  • Webpack Doc — Loader Interface

Loader is one of the most important modules of Webpack. When you need to load resources, you need to set up the corresponding Loader so that its source code can be converted.

Due to the prosperity of Webpack community, most of the resources used by business scenarios have appropriate Loaders. You can refer to available Loaders on the official website. However, due to the uniqueness of business, there may be no applicable loader.

The following examples will show you how to develop your own loader. But before you do that, it’s a good idea to understand how to debug them individually.

usingloader-runnerDebugging Loaders

Loader-runner allows you to run a loader without relying on Webpack. Install it first

mkdir loader-runner-example
npm init
npm install loader-runner --save-dev
Copy the code

Next, create a Demo-loader to test

mkdir loaders
echo "module.exports = input => input + input;" > loaders/demo-loader.js
Copy the code

The loader copies the contents of the imported module once and returns them. Create the imported module

echo "Hello world" > demo.txt
Copy the code

Next, run the loader through loader-runner:

/ / create the run - loader. Js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");

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

When you run node run-loader.js, you will see the log displayed on the terminal

{ result: [ 'Hello world\nHello world\n'].resourceBuffer: <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 0a>,
  cacheable: true,
  fileDependencies: [ './demo.txt' ],
  contextDependencies: [] }
Copy the code

You can see from the output

  • Result: Loader completes the task we gave it, copying the contents of the target module to one side;
  • ResourceBuffer: The contents of the module are converted to buffers.

If you need to output the converted file, you only need to modify the second parameter of runLoaders, as shown in

runLoaders(
  {
    resource: "./demo.txt".loaders: [path.resolve(__dirname, "./loaders/demo-loader")].readResource: fs.readFile.bind(fs),
  },
  (err, result) => {
    if (err) console.error(err)
    
    fs.writeFileSync("./output.txt", result.result)
  }
);
Copy the code

Develop an asynchronous Loader

While you can implement a range of Loaders through the synchronous Interface described above, this format is not suitable for all scenarios, such as packaging third-party packages as loaders that force you to do so.

To make the above example asynchronous, we use the this.async() API provided by WebPack. Calling this function returns an error first (result second) callback that complies with the Node specification.

The above example can be rewritten as:

loaders/demo-loader.js

module.exports = function(input) {
  const callback = this.async();

  // No callback -> return synchronous results
  // if (callback) { ... }

  callback(null, input + input);
};
Copy the code

Webpack is injected through this, so you can’t use () => {}.

A subsequent run of Node run-loader.js will print the same result on the terminal. If you want to handle exceptions generated during loader execution, you can

module.exports = function(input) {
  const callback = this.async();

  callback(new Error("Demo error"));
};
Copy the code

The log printed on the terminal contains errors: Demo Error, and the stack trace shows where the error occurred.

Return output only

Loader can also be used to output code individually, as can be done

module.exports = function() {
  return "foobar";
};
Copy the code

Why do you do that? You can pass the webpack entry file to loader. Instead of pointing to a predefined file, the loader with the corresponding code can be generated dynamically.

If you want to return a Buffer, you can set module.exports.raw = true and change the string to Buffer.

Written to the file

Some loaders, like file-loaders, generate files. Webpack provides a method for this, this.emitfile, but loader-Runner does not support it yet, so it needs to be implemented actively

runLoaders(
  {
    resource: "./demo.txt".loaders: [path.resolve(__dirname, "./loaders/demo-loader")].// Add emitFile method to this
    context: {
      emitFile: (a)= >{},},readResource: fs.readFile.bind(fs),
  },
  (err, result) => (err ? console.error(err) : console.log(result))
);
Copy the code

To implement the basic idea of file-loader, you must do two things: find the file and return its path. You can do this as follows:

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

module.exports = function(content) {
  const url = loaderUtils.interpolateName(this."[hash].[ext]", {
    content,
  });

  this.emitFile(url, content);

  const path = `__webpack_public_path__ + The ${JSON.stringify(url)}; `;

  return `export default ${path}`;
};
Copy the code

Webpack provides two additional EMIT methods:

  • this.emitWarning(<string>)
  • this.emitError(<string>)

These methods are used in place of the console. As with this.emitfile, you must emulate them for loader-Runner to work.

The next problem is how to pass the file name to the Loader.

Pass the configuration to loader

In order to pass the required configuration to the Loader, we need to make some changes

run-loader.js

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

runLoaders(
  {
    resource: "./demo.txt".loaders: [{loader: path.resolve(__dirname, "./loaders/demo-loader"),
        options: {
          name: "demo.[ext]",}},],context: {
      emitFile: (a)= >{},},readResource: fs.readFile.bind(fs),
  },
  (err, result) => (err ? console.error(err) : console.log(result))
);
Copy the code

As you can see, we will remove the loaders from the original

loaders: [path.resolve(__dirname, "./loaders/demo-loader")]
Copy the code

To pass options

loaders: [
      {
        loader: path.resolve(__dirname, "./loaders/demo-loader"),
        options: {
          name: "demo.[ext]",}},]Copy the code

To get the options we passed, we still parse the options using loader-utils.

Don’t forget NPM install loader-utils –save-dev

To connect it to the Loader

loaders/demo-loader.js

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

module.exports = function(content) {
  / / get the options
  const { name } = loaderUtils.getOptions(this);


  const url = loaderUtils.interpolateName(this."[hash].[ext]", {
    content,
  });


  const url = loaderUtils.interpolateName(this, name, { content }); ) ; };Copy the code

Run node run-loader.js and you’ll see it printed on the terminal

{ result:
   [ 'export default __webpack_public_path__ + "f0ef7081e1539ac00ef5b761b4fb01b3.txt"; ' ],
  resourceBuffer: <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 0a>,
  cacheable: true,
  fileDependencies: [ './demo.txt' ],
  contextDependencies: [] }
Copy the code

You can see that the result is the same as what the Loader should return. You can try passing more options to the Loader or using query parameters to see what happens with different combinations.

Connect webPack and custom loader

To use loader in a step, we need to associate it with WebPack. Here, we introduce the custom loader in inline form

// in webpack.config.js
resolveLoader: {
    alias: {
        "demo-loader": path.resolve(
            __dirname,
            "loaders/demo-loader.js"),}},// Specify loader in the file to import
import ! "" demo-loader? name=foo! ./main.css"
Copy the code

Of course you can also handle loaders by rules. Once it’s stable enough, create a webPack-Defaults based project, push the logic to NPM, and start using loader as a package.

Although we use loader-Runner as the development and testing environment for loader. But it’s still slightly different from WebPack, so you’ll need to test it on WebPack.

Pitch Loaders

Webpack is divided into two stages to execute the loader: pitching and evaluating. If you’re familiar with the Web’s event system, it’s very similar to capturing and bubbling events. Webpack allows you to do interception execution in the pitching phase. The order is, pitch left to right, execute right to left.

A pitch loader allows you to modify a request or even terminate it. For example, create

loaders/pitch-loader.js

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

module.exports = function(input) {
  const { text } = loaderUtils.getOptions(this);

  return input + text;
};
module.exports.pitch = function(remainingReq, precedingReq, input) {
  console.log(`
Remaining request: ${remainingReq}
Preceding request: ${precedingReq}
Input: The ${JSON.stringify(input, null.2)}
  `);

  return "pitched";
};
Copy the code

And add it to run-loader.js,

. loaders: [ {loader: path.resolve (__dirname, './loaders/demo-loader'),
        options: {
            name: 'demo.[ext]',
        },
    },
    path.resolve(__dirname, "./loaders/pitch-loader"),],...Copy the code

Perform the node run – loader. Js

Remaining request: ./demo.txt Preceding request: ... /webpack-demo/loaders/demo-loader? {"name":"demo.[ext]"}
Input: {}

{ result: [ 'export default __webpack_public_path__ + "demo.txt"; '].resourceBuffer: null.cacheable: true.fileDependencies: [].contextDependencies: []}Copy the code

You will find that pitch-loader does the information insertion and the interception performed.

conclusion

The WebPack Loader essentially describes how to convert one file format to another. You can find out how to implement specific functionality by studying the API documentation or existing Loaders.

Under review:

  • loader-runnerLoader is a very useful tool for developing and debugging loader.
  • The Webpack Loader generates output from input;
  • Loaders are classified into synchronous and asynchronous modes. The asynchronous mode can passthis.asyncTo write asynchronous loader;
  • You can use a Loader to dynamically generate code for WebPack, in which case the Loader does not have to accept input;
  • useloader-utilsCan compile loader configuration, also can passschema-utilsConduct validation;
  • usingresolveLoader.aliasTo complete the introduction of local custom loader to prevent global impact;
  • The Pitching phase allows you to make modifications to loader inputs or intercept execution sequences.