For Webpack, everything is a module. Therefore, no matter what files, need to be converted into JS recognizable modules. You can understand that files with any suffix are used as JS (even img, PPT, TXT files, etc.). However, it is definitely not possible to use it as A JS module directly. It needs to be converted into a way that can be understood by JS before it can be used as a JS module – this conversion process is handled by the Webpack loader. A Webpack loader is a JS module exported as a function. The Loader runner inside webpack calls this function, passes in the result or resource file from the previous loader, and returns the processed result

How to implement raw-loader, json-loader, url-loader, bundle-loader

Preparations: Install it firstwebpack,webpack-cli,webpack-dev-serverWhat will be used in the following practice

Loader to use

  1. General method: configure rules in webpack.config
module.exports = {
  module: {
    rules: [{test: /\.js$/.// Match rules
        use: ['babel-loader'] // require the loader path array}}}]Copy the code

If a file name ends in.js, it will be processed by all loaders in use

  1. loadername! Let’s say we have a TXT file that we want to passraw-loaderTo get the entire TXT file inside the string content. In addition to using the uniform WebPack Config configuration, we can also import with this syntax:
import txt from "raw-loader! ./1.txt";
// TXT is the entire contents of this file
Copy the code

In fact, after uniformly configuring loader using webpack.config file, loader will be imported in this way eventually. Supports multiple loaders with syntax: loader1! loader2! yourfilename

Query alternative options

Use loadername! Prefix syntax: raw-loader? a=1&b=2! ./1.txt, equivalent to webpack configuration:

      {
        test: /^1\.txt$/.exclude: /node_modules/.use: [{loader: "raw-loader".options: { a: '1'.b: '2'}}},],Copy the code

Loader-utils (webpack) ¶ Loader-utils (webpack) ¶ loader-utils(webpack) ¶

const { getOptions } = require("loader-utils");
module.exports = function(content) {
  const options = getOptions(this) | | {};// If it is configured, return options; If it is loadername! Syntax that returns an object generated from a Query string
 // ...
};
Copy the code

This method will be used to configure the Loader several times for demonstration purposes. If have not used this kind of method, regard as entry study 😊. Start ~

What does a loader look like

A loader is a JS module exported as a function. This function takes three parameters: content, map, and meta

  • Content: indicates the source file string or buffer
  • Map: represents the Sourcemap object
  • Meta: metadata, auxiliary objects

We implement the simplest loader that adds a line of console to the code:

// console.js
module.exports = function(content, map, meta) {
  return `${content}; console.log('loader exec')`;
};
Copy the code

Webpack configuration

  module: {
    rules: [{test: /\.js$/.exclude: /node_modules/.use: [{loader: "./loaders/console" }, // Add your own loader]]}},Copy the code

'loader exec'

The simplest loaders are raw-loader and JSON-loader

These loaders read the contents of the file and can then use import or require to import all the contents of the original file. It is obvious that an export statement is missing when the original file is being used as js, and all loader does is add the export statement.

Let’s say I have a TXT that looks like this

this is a txt file
Copy the code

This is a TXT file. This is a TXT file. This is a TXT file. For normal use, the TXT file needs to be changed to:

export default 'this is a txt file'
Copy the code

The end result is that whatever file (TXT, MD, json, etc.) is used as a JS file, and the content of the original file is equivalent to a string, which is exported:

// Write raw-loader
const { getOptions } = require("loader-utils");
// Get webpack configuration options, write loader's first set of routines

module.exports = function(content, map, meta) {
  const opts = getOptions(this) | | {};const code = JSON.stringify(content);
  const isESM = typeofopts.esModule ! = ="undefined" ? options.esModule : true;
// Return to the original file
  return `${isESM ? "export default" : "module.exports ="} ${code}`;
};
Copy the code

Raw-loader and JSON-loader are almost the same. Their purpose is to export all the contents of the original file as a string. Json-loader has a Json.parse process

Note: take a look at the official Loader source code and they add one more step

JSON.stringify(content)
    .replace(/\u2028/g.'\\u2028')
    .replace(/\u2029/g.'\\u2029');
Copy the code

\u2028 and \u2029 are special characters, similar to \n, \b and the like, except that they are intuitively empty strings when escaped. You can see what makes it special:

Even if you see a strange character in the middle, but you press Enter again, ‘ab’, \u2028 string is visually equivalent to the empty string (the character actually exists, but it has no effect). In addition to 2028 and 2029, such as \u000A \n, there is a newline effect (the character exists and has its effect). Therefore, it is necessary to escape for low probability occurrences of character values 2028 and 2029

Unicode character values Escape sequences meaning category
\u0008 \b Backspace
\u0009 \t Tab blank
\u000A \n Newline character (newline) Line end
\u000B \v Vertical TAB character blank
\u000C \f Change the page blank
\u000D \r enter Line end
\u0022 Double quotation marks (“)
\u0027 \ ‘ Single quotes (‘)
\u005C \ The backslash ()
\u00A0 Uninterrupted space blank
\u2028 Line separators Line end
\u2029 Paragraph separator Line end
\uFEFF Byte order token blank

Raw mode with URL-loader

We have implemented raw-loader, which returns the contents of the original file as a string. But the problem is that some files can’t be solved with a single string, such as pictures, videos and audio. At this point, we need to use the buffer directly from the original file. As it happens, the first argument to the Loader function, content, supports string/buffer

How to enable buffer content?

// Just export raw to true
module.exports.raw = true
Copy the code

The process of url-loader is to read the configuration, whether it can be transferred, and how to transfer. => Read the original file buffer. => Transfer the buffer to base64. Let’s implement a simplified version of urL-loader that only implements the core functionality

const { getOptions } = require("loader-utils");

module.exports = function(content) {
  const options = getOptions(this) | | {};const mimetype = options.mimetype;

  const esModule =
    typeofoptions.esModule ! = ="undefined" ? options.esModule : true;

// Base encoding: data:[mime type]; Base64,[encoded content of file]
  return `${esModule ? "export default" : "module.exports ="} The ${JSON.stringify(
    `data:${mimetype || ""}; base64,${content.toString("base64")}`
  )}`;
};

module.exports.raw = true;
Copy the code

Then, let’s try importing a random image:

// The loader path is modified automatically
// img is a base64 image path that can be used directly with the img tag
import img from ".. /.. /loaders/my-url-loader? mimetype=image! ./1.png";
Copy the code

Read publicPath in configuration => determine the final output path=> Add the file name to the MD5 hash value => Move a file, change the file name to a new name => add the path in front of the new file name => print the final file path

Pitch and bundle – loader

The pitching loader is always called from right to left. In some cases, the loader only cares about the metadata following the request and ignores the results of the previous loader. The pitch method on loader is called from left to right before the loader is actually executed (right to left). Second, if one loader returns a result in the pitch method, the process skips the rest of the loader

Pitch Three parameters of the method:

  • RemainingRequest: loader+ resource path loaderName! The grammar of the
  • PrecedingRequest: Resource path
  • Metadata: The third argument to a normal loader function is an auxiliary object, and the loader uses the same object throughout its execution

Loader executes this process from back to front, which you can view as sequential loading and reverse loading. [‘ A-loader ‘, ‘b-loader’, ‘c-loader’] for example, A file matching rule A will undergo three loaders: [‘ A-loader ‘, ‘b-loader’, ‘c-loader’]

It goes something like this:

  • The implementation of a – loaderpitchmethods
  • B – loaderpitchmethods
  • Executive c – loaderpitchmethods
  • Get the resource content from the import/require path
  • C – loader is carried out
  • B – loader is carried out
  • A – loader is carried out

If there is a pitch method in the B-loader, and the pitch method returns a result, then the above procedure will not stack the C-loader after it has passed the B-loader

// b-loader
module.exports = function(content) {
  return content;
};

// Do nothing but pass import in and export out
module.exports.pitch = function(remainingRequest) {
// remainingRequest path to add -! The prefix
  return `import s from The ${JSON.stringify(
    ` -!${remainingRequest}`
  )}; export default s`;
};
Copy the code

The pitch method of the B-loader returns the result and goes through the following process:

  • The implementation of a – loaderpitchmethods
  • B – loaderpitchMethod (returns result, skip c-loader)
  • Get the resource content from the import/require path
  • B – loader is carried out
  • A – loader is carried out

When should the rest of the loader be skipped? The most common is dynamic loading and cache reading, to skip the later loader calculation. Bundle-loader is a typical example

The bundle-loader implements dynamic loading on demand. We can modify the React final reactdom.render step to dynamically load the React-dom to see the difference

- import ReactDom from "react-dom";
+ import LazyReactDom from "bundle-loader? lazy&name=reactDom! react-dom";

+ LazyReactDom(ReactDom => {
+ console.log(ReactDom, "ReactDom");
ReactDom.render(<S />, document.getElementById("root"));
+});
Copy the code

You can see that the ReactDOM is isolated and introduced dynamically

Check the bundle-loader source code, find that it uses require. Ensure to dynamically introduce, the specific implementation is also very simple, see the bundle-loader source code. The era is changing. Dynamic import should be introduced in the new era. Let’s implement a new bundle-loader based on dynamic import. (Implements only the core features introduced by lazy)

/ / get ChunkName
function getChunkNameFromRemainingRequest(r) {
  const paths = r.split("/");
  let cursor = paths.length - 1;
  if (/^index\./.test(paths[cursor])) {
    cursor--;
  }
  return paths[cursor];
}

// What does the original loader need to do
module.exports = function() {};

module.exports.pitch = function(remainingRequest, r) {
  / / loadername! Dependency path of prefix
  const s = JSON.stringify(` -!${remainingRequest}`);
  // Use the webpackChunkName annotation to define the chunkname syntax
  return `export default function(cb) {
  return cb(import(/* webpackChunkName: "my-lazy-${getChunkNameFromRemainingRequest(
    this.resource
  )}" */${s})); } `;
};

Copy the code

The usage is similar to that of the official bundle-loader, except that dynamic import returns a promise.

import LazyReactDom from ".. /loaders/my-bundle! react-dom";

setTimeout((a)= > {
  LazyReactDom(r= > {
    r.then(({ default: ReactDom }) = > {
      ReactDom.render(<S />, document.getElementById("root"));
    });
  });
}, 1000);
Copy the code

Loader context

This is the context of the loader. Details can be found on the official website.

Let’s practice with one of the context properties: this.loadModule

loadModule(request: string, callback: function(err, source, sourceMap, module))

The loadModule method parses the given request to a module, applies all configured Loaders, and passes in the generated source, sourceMap, and NormalModule instances within webPack in the callback function. You can use this function if you need to obtain the source code of another module to generate the result.

One obvious application of this approach is to inject additional dependencies on existing code

let’s coding

Background: There is an API file called api.js

constapi0 = { log(... args) {console.log("api log>>>". args); }};module.exports = api0;
Copy the code

Hope result: we use the following a.jjs file, can directly use the API, and no error

// a.js
export default function a() {
  return 1;
}
// Other code
// ...

api.log("a"."b");
Copy the code

Therefore, when we need to build, loader will type the API into our code:

/ / addapi loader
module.exports = function(content, map, meta) {
// Async loader involves loading modules
  const callback = this.async();
  this.loadModule(".. /src/api.js", (err, source, sourceMap, module) = > {Module. Exports = require(XXX); // module. Exports = require(XXX)
    callback(
      null.`const api = ${source.split("=") [1]};
${content}; `,
      sourceMap,
      meta
    );
  });
  return;
};
Copy the code

Loadername: loaderName: loaderName: loaderName: loaderName: loaderName: loaderName: loaderName The syntax introduced by A.js (./loaders/ addAPI! ./a.js)

Finally we can see the log that successfully ran api.js

Usually there are some familiar scenes, such and such API, SUCH and such SDK, public utils method, pvUV report of each index page and so on, need to load these JS to complete execution or import. If we can’t be bothered to add import/require statements to each file, we can do this in a flash. The premise of this SAO operation is to ensure that the subsequent colleagues to take over the project difficulty is low, no code pit. Comments, documentation, elegant naming

The last

The function of loader is to convert all files into JS modules that you need and can use to run. Babel and Loader are more powerful together, allowing you to modify code, be lazy, etc. There will also be webpack plugin, Babel related articles, we learn to exchange ~

Pay attention to the public account “Different front-end”, learn front-end from a different perspective, grow fast, play with the latest technology, explore all kinds of black technology