Little boy (Hujiang Online School Web Front-end Engineer)

This article is originally reproduced, please indicate the author and source

Those of you who have been on the Webpack website will look familiar. As advertised, WebPack can package the various types of files on the left (webPack calls them “modules”) into files on the right that are supported by a universal browser. A Webpack is like a magician’s hat. You put a silk scarf in it and it turns out to be a dove. So how does this “magic” work? Today we look to one of webPack’s core concepts – loader – to find out and start implementing this magic trick. After reading this article, you can:

  • Know how webPack Loader works.
  • Develop a loader that meets business requirements.

What is Loader?

Before we can masturbate a loader, we need to know what it is. Loader is essentially a Node module, which is consistent with webpack’s “everything is a module” mentality. Since it’s a Node module, it’s bound to export something. In webpack’s definition, loader exports a function that loader calls when it converts a resource module. Inside this function, we can use them by passing the this context to the Loader API. The modules on the left of the header are the so-called source modules that loader converts to the generic files on the right, so we can also summarize what Loader does: convert source modules into generic modules.

How to use Loader?

Now that we know how powerful it is, how do we use Loader?

1. Configure the Webpack config file

Since Loader is a Webpack module, we can’t make it work without configuration. I’ve collected three configuration methods for you to choose from.

Configuration of a single Loader

Add a rule object to the config.module.rules array.

let webpackConfig = {
    / /...
    module: {
        rules: [{
            test: /\.js$/.use: [{
                // Here write the loader path
                loader: path.resolve(__dirname, 'loaders/a-loader.js'), 
                options: {/ *... * /}}}}Copy the code

Multiple Loaders are configured

Add rule objects in the config.module.rules array and config.resolVeloader.

let webpackConfig = {
    / /...
    module: {
        rules: [{
            test: /\.js$/.use: [{
                // Write the loader name here
                loader: 'a-loader'.options: {/ *... * /}}, {loader: 'b-loader'.options: {/ *... * /}}}}]],resolveLoader: {
        // Tell Webpack where to look for the Loader module
        modules: ['node_modules', path.resolve(__dirname, 'loaders')]}}Copy the code

Other configuration

You can also use the NPM Link to connect to your project, which is similar to the node CLI tool development, and is not exclusively for loader modules, which will not be discussed in this article.

2. Get started

When the configuration is complete, when you introduce modules into the Webpack project, matching the rule (e.g. /\.js$/ above) will enable the corresponding loader (e.g. A-loader and B-loader above). In this case, assuming we are the developer of a-Loader, a-Loader will export a function that takes a string containing the contents of the source file as its only argument. Let’s call it “source.”

We then process the transformation of the source in the function and return the processed value. Of course, the number of return values and the return mode depend on the requirements of A-Loader. In general, you can return a value by returning the converted value. Call this.callback(err, values…) if you need to return more than one argument. To return. In an asynchronous Loader you can handle exceptions by throwing errors. Webpack suggests that we return one or two parameters, the first of which is the converted source, which can be either a string or a buffer. The second parameter is an optional object used as SourceMap.

3. Advanced use

Usually when we are dealing with a class of source files, a single Loader is not sufficient (loader design principles are discussed later). Generally, we use multiple loaders in series, similar to a factory assembly line, where workers (or machines) in one location do only one type of work. Webpack specifies that loaders in the use array are executed from the last to the first. They comply with the following rules:

  • The last loader is called first and takes the contents of the source as an argument
  • The first loader is called last. Webpack expects it to return JS code, and the source map is optional as mentioned earlier.
  • The intermediate loaders are called in chains. They take the return value of the previous loader and provide input to the next loader.

Here’s an example:

webpack.config.js

    {
        test: /\.js/.use: [
            'bar-loader'.'mid-loader'.'foo-loader']}Copy the code

In the above configuration:

  • Loaders are called in the order foo-loader -> mid-loader -> bar-loader.
  • Foo-loader gets the source, processes it, and passes the JS code to Mid. Mid gets the “source” processed by Foo and then processes it to bar. After bar processes it, it gives it to Webpack.
  • Bar-loader eventually passes the return value and the source map to WebPack.

Develop Loader with proper posture

After understanding the basic pattern, we are not in a rush to develop. The so-called sharpening knife does not mistakenly cut wood workers, let’s first look at the development of a loader need to pay attention to what, so that can be less detours, improve the quality of development. Here are a few guidelines from WebPack, in order of importance, noting that some of them apply only to certain situations.

1. Single responsibility

A Loader can only do one thing, which not only makes loader maintenance easy, but also allows loader to combine different combinations to meet the requirements of the scenario.

2. Chain combination

This is an extension of the first point. Make good use of the link combination of Loader special type, you can harvest unexpected results. Specifically, instead of writing a loader that can do five things at a time, break it down into five single-thing Loaders, perhaps several of which can be used in other scenarios you haven’t thought of yet. Let’s take an example.

Suppose we now want to implement the ability to render the template through the loader configuration and query parameter. We implement this in an “apply-loader” that compiles the source template and ultimately outputs a module that exports the HTML strings. According to the rules of chained composition, we can combine the other two open source loaders:

  • jade-loaderConvert a template source file into a module that exports a function.
  • apply-loaderPass loader options to the above function and execute it, returning HTML text.
  • html-loaderReceive HTMl text files and convert them into referenced JS modules.

In fact, the loader in the concatenation does not have to return JS code. An upstream loader can return modules of any type as long as the downstream loader can efficiently process the output from the upstream loader.

3. The modular

Make sure the Loader is modular. Loader generated modules need to follow the same design principles as normal modules.

4. A stateless

We should not preserve state in the Loader between module transitions. Each loader should be run independently of other compiled modules, and also independently of previous loaders’ compilation of the same module.

5. Use the Loader utility

Take advantage of the Loader-utils package, which provides a number of useful tools. One of the most common is to retrieve the options passed to the loader. In addition to The Loader-utils package, there is also the Schema-utils package. We can use the schema-utils tool to obtain the JSON Schema constant that is used to verify the options of loader. The following example briefly combines the two toolkits mentioned above:

import { getOptions } from 'loader-utils';
import { validateOptions } from 'schema-utils';

const schema = {
  type: object,
  properties: {
    test: {
      type: string
    }
  }
}

export default function(source) {
    const options = getOptions(this);

    validateOptions(schema, options, 'Example Loader');

    // Write the source logic here...
    return `export default The ${JSON.stringify(source) }`;
};

Copy the code

The dependence of the loader

If we use external resources (that is, resources read from the file system) in the loader, we must declare information about these external resources. This information is used to verify the cacheable LOder in Watch mode and to recompile. The following example briefly shows how to do this using the addDependency method. Loader. Js:

import path from 'path';

export default function(source) {
    var callback = this.async();
    var headerPath = path.resolve('header.js');

    this.addDependency(headerPath);

    fs.readFile(headerPath, 'utf-8'.function(err, header) {
        if(err) return callback(err);
        // The callback is equivalent to the asynchronous version of the return
        callback(null, header + "\n" + source);
    });
};
Copy the code

Module is dependent on

Different modules specify dependencies in different ways. For example, in CSS we use @import and URL (…) Declaration, and we should let the module system resolve these dependencies.

How do you get the module system to resolve the dependencies of different declarations? There are two ways to do this:

  • Convert the different dependency declarations intorequireThe statement.
  • throughthis.resolveFunction to resolve the path.

A good example of the first approach is csS-loader. It turns the @import declaration into a require stylesheet file, and turns the URL (…) The declaration is converted to require referenced files.

For the second method, refer to less-loader. Since we need to keep track of variables and mixins in less, we need to compile all.less files at once, so we can’t turn every @import into require. Thus, less-Loader extends the LESS compiler with custom path-resolution logic. This approach uses the second approach we mentioned earlier — this.resolve — to resolve dependencies through WebPack.

If a language only supports relative paths (such as url(file) pointing to./file). You can use ~ to point the relative path to an already installed directory (such as node_modules), so for example, a URL will look like this: URL (~some-library/image.jpg).

Code to the public

To avoid initializing the same code across multiple Loaders, extract the common code into a runtime file and import it into each loader via require.

An absolute path

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

Company relies on

If you are developing a loader that simply packs another package, you should set the package as peerDependency in package.json. This lets the application developer know which specific version to specify. For example, the following sass-Loader specifies Nod-sass as a peer dependency:

"PeerDependencies ": {"node-sass": "^4.0.0"}Copy the code

Talk is cheep

Above, we have sharpened the knife for cutting wood. Next, we start to develop a loader.

Compression of HTML is a very common requirement if we are referring to template files in project development. To decompose the above requirements, we can split the template parsing and compression to two loaders to do (single responsibility), the former is more complex, we will introduce the open source package HTmL-Loader, and the latter, we will use to practice. First, let’s give it a resounding name — HTml-minify-loader.

Next, follow the steps described earlier. First, we should configure webpack.config.js so that WebPack can recognize our loader. Of course, to start, we will create the loader file — SRC /loaders/html-minify-loader.js.

So, we do this in the configuration file: webpack.config.js

module: {
    rules: [{
        test: /\.html$/.use: ['html-loader'.'html-minify-loader'] // Processing sequence html-minify-loader => html-loader => webpack}},resolveLoader: {
    // Since html-Loader is an open source NPM package, add the 'node_modules' directory here
    modules: [path.join(__dirname, './src/loaders'), 'node_modules']}Copy the code

Next, we provide sample HTML and JS to test the Loader:

SRC/example. HTML:


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    
</body>
</html>
Copy the code

SRC/app. Js:

var html = require('./expamle.html');
console.log(html);
Copy the code

Ok, now let’s work with SRC /loaders/html-minify-loader.js. As mentioned earlier, loader is also a node module. It exports a function that takes the source module of require, processes the source module, and passes the return value to the next loader. So its “template” should look like this:

module.exports = function (source) {
    // Handle source...
    return handledSource;
}
Copy the code

or

module.exports = function (source) {
    // Handle source...
    this.callback(null, handledSource)
    return handledSource;
}
Copy the code

Note: In other words, it must be an executable JS script (stored as strings). More specifically, it must be a NODE module JS script. Let’s take a look at the following example.

// The loader with the last processing order
module.exports = function (source) {
    // The function of this loader is to convert the source module into a string for the caller of require
    return 'module.exports = ' + JSON.stringify(source);
}
Copy the code

The whole process is equivalent to this loader putting source files

Here is the source moduleCopy the code

into

// example.js
module.exports = 'Here's the source module';
Copy the code

Then pass it to the require caller:

// applySomeModule.js
var source = require('example.js'); 

console.log(source); // Here is the source module
Copy the code

However, in our concatenated two loaders, the task of parsing HTML and converting it into JS execution script has been handed over to HTmL-Loader, and we will deal with THE PROBLEM of HTML compression.

Loaders that act as normal Node modules can easily reference third-party libraries. We use the minimize library for core compression:

// src/loaders/html-minify-loader.js

var Minimize = require('minimize');

module.exports = function(source) {
    var minimize = new Minimize();
    return minimize.parse(source);
};
Copy the code

Of course, the minimize library supports a series of compression parameters, such as the comments parameter that specifies whether or not comments need to be retained. We certainly can’t write these configurations into the Loader. Loader-utils comes into play:

// src/loaders/html-minify-loader.js
var loaderUtils = require('loader-utils');
var Minimize = require('minimize');

module.exports = function(source) {
    var options = loaderUtils.getOptions(this) | | {};// Get the loader configuration for webpack.config.js here
    var minimize = new Minimize(options);
    return minimize.parse(source);
};
Copy the code

In this way, we can set whether we want to keep comments after compression in webpack.config.js:

    module: {
        rules: [{
            test: /\.html$/.use: ['html-loader', {
                loader: 'html-minify-loader'.options: {
                    comments: false}}}}]],resolveLoader: {
        // Since html-Loader is an open source NPM package, add the 'node_modules' directory here
        modules: [path.join(__dirname, './src/loaders'), 'node_modules']}Copy the code

Of course, you can also write our loader asynchronously so that it does not block the rest of the compilation:

var Minimize = require('minimize');
var loaderUtils = require('loader-utils');

module.exports = function(source) {
    var callback = this.async();
    if (this.cacheable) {
        this.cacheable();
    }
    var opts = loaderUtils.getOptions(this) | | {};var minimize = new Minimize(opts);
    minimize.parse(source, callback);
};

Copy the code

You can view the related code in this repository, and NPM Start can go to http://localhost:9000 to open the console to see what is processed by the loader.

conclusion

By this point, I’m sure you have your answer to “How to develop a Loader”. To sum up, a loader needs to go through the following steps to work in our project:

  • Create the loader directory and module file
  • In Webpack, configure the rule and the resolution path of the loader, and pay attention to the order of the loaderrequireWhen specifying a type file, we can make the processing flow pass through specifying laoder.
  • Follow the principles to design and develop the loader.

Finally, Talk is Cheep, get your hands dirty

reference

Writing a loader

Recommended: Translation Project Master’s Self-statement:

1. Everyone is a Master of translation projects

2. IKcamp wechat mini program teaching consists of 5 chapters and 16 sections (including video)

3. Began to free serial ~ 2 more per week, a total of 11 iKcamp lesson | based on Koa2 structures, the Node. Js combat (video) | project teaching syllabus is introduced


In 2019, iKcamp’s original book “Koa and Node.js Development Practice” has been sold on JD.com, Tmall, Amazon and Dangdang!