In Webpack, there are two mechanisms, one is loader mechanism, the other is plug-in mechanism. Although there are many open source Webpack Loaders in the community, we still have to develop some loaders in the development process under the requirements of some specific scenarios. In this article, we’ll look at how to implement a custom Webpack Loader.

Let’s take an example of how to convert the contents of a Markdown file to HTML. My relevant system environment is as follows:

  • System: Windows 10
  • Webpack: 5.6.0
  • Webpack – cli: 4.2.0
  • Node. Js: v14.15.0
  • NPM: 6.14.9

In addition, the NPX package was installed globally via npm-i-g NPX.

Episode: Issues with NPM and Node.js versions

The system environment is listed because I encountered the following error when executing the NPM run build without changing to the above environment:

> SyntaxError: Invalid regular expression: /(\p{Uppercase_Letter}+|\p{Low

After a search, it was found that the problem was due to the NPM version, so the NPM i-g NPM (see this article) was executed to upgrade NPM (6.14.9). But this brings up a more serious problem — you can’t execute the NPM command on the console. It was found that the node.js version was too low (it was supposed to be 9.x at the time), so I reinstalled the latest version of Node.js (V14.15.0). Then the problem was solved.

The content returned by the user-defined Loader does not meet the requirements

Before starting to customize loaders, we need to clarify the principle of single responsibility, which is the principle that a Loader does only one thing. This not only makes loader maintenance easy, but also enables Loaders to combine different combinations to meet the needs of various scenarios. Otherwise, multiple functions are stacked together. The scenarios to which this Loader can be applied are greatly reduced.

Next, we create some simple files for loader development. The overall directory structure is organized as follows:

.dev - Webpack-loader ├─ Loader.js ├─ Package.js ├─package.js ├─ SRC ├─index.js └ ─ markdown └ test. The mdCopy the code

The code for each file is as follows:

  • ./src/markdown/test.md
# test markdown

Hello, this is a test markdown!
Copy the code
  • ./src/index.js
import md from './markdown/test.md';

console.log(md);
Copy the code
  • . / markdown – loader. Js file:
const marked = require('marked');
module.exports = (source) = > {
  console.log('* * * * * * * * * * * * * * * * * * * * * * * * * * * * *');
  console.log(source);
  console.log('* * * * * * * * * * * * * * * * * * * * * * * * * * * * *');
  return source;
};
Copy the code
  • . / package. Json file:
{
  "name": "dev-webpack-loader"."version": "0.1.0 from"."description": ""."main": "index.js"."scripts": {
    "build": "npx webpack --config ./webpack.config.js"
  },
  "author": ""."license": "MIT"."devDependencies": {
    "webpack": "^ 5.6.0"."webpack-cli": "^ 4.2.0"
  },
  "dependencies": {
    "marked": "^ 1.2.5." "}}Copy the code
  • ./webpack.config.js
/ * * *@type {import('webpack').Configuration}* /
const path = require('path');

module.exports = {
  mode: 'none'.entry: {
    index: './src/index.js',},output: {
    filename: '[name].[hash:8].js',},module: {
    rules: [{test: /\.md$/,
        use: [
          './markdown-loader.js']},}Copy the code

Because the contents of each file are relatively simple and unexplained. But when we run the NPM run build, we get the following:

&gt; [email protected] build D: \ dev - webpack - loader & gt; npx webpack --config ./webpack.config.js ***************************** # test markdown Hello, this is a test markdown! ***************************** (node:12252) [DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_HASH] DeprecationWarning: [hash] is now [fullhash] (also consider using [chunkhash] or [contenthash], see documentation for details) (Use `node --trace-deprecation ... 'To show where the Warning was created) [Webpack-CLI] Compilation Finished Assets by Status 3.03 KiB [cached] 1 Asset runtime modules 657 bytes 3 modules cacheable modules 211 bytes ./src/index.js 73 bytes [built] [code generated] ./src/markdown/test.md 138 bytes [built] [code generated] [1 error] ERROR in ./src/markdown/test.md 1:15 Module parse failed: Unexpected token (1:15) File was processed with these loaders: * ./markdown-loader.js You may need an additional loader to handle the result of these loaders. export default = "<p>// ./src/markdown/test.md</p>\n<h1>test markdown</h1>\n<p>Hello, this is a test markdown! </p>\n" export default = "<p>// ./src/markdown/test.md</p>\n<h1>test markdown</h1>\n<p>Hello, this is a test markdown! </p>\n" @. / SRC /index.js 2:0-36 4:12-14 Webpack 5.6.0 compiled with 1 error in 165 ms NPM ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! [email protected] build: 'NPX webpack -- config. /webpack.config.js' NPM ERR! Exit status 1 npm ERR! npm ERR! Failed at the [email protected] build script. NPM ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! C:\Users\xxx\AppData\Roaming\npm-cache\_logs\2020-11-21T13_43_36_862Z-debug.logCopy the code

As you can see, the contents of the source parameter are the contents of the./ SRC /markdown/test.md file, which is expected. However, an error message appeared, and the most valuable reminder was this:

> You may need an additional loader to handle the result of these loaders.

It is strange that other loaders are needed to handle such simple content. ! Return source in markdown-loader; Webpack requires the last loader to return a string whose literal content is standard JavaScript code.

We put the

return source;
Copy the code

That sentence changed to:

return `module.exports = ${JSON.stringify(source)}; `Copy the code

NPM run build

Misuse of hashes

However, there is one detail worth noting in the above error:

> DeprecationWarning: [hash] is now [fullhash] (also consider using [chunkhash] or [contenthash]

Hash is no longer recommended and is changed to fullhash, which is actually clearer than the original hash.

Here’s the difference between the three hashes:

  • hash:

If any of the files packaged in the project are modified, the hash changes.

  • Chunkhash:

If a file packaged in the current chunk changes, the chunkhash changes.

  • Contenthash:

If the contents of the text file change, the Contenthash changes. Since styles are also introduced in Webpack as JS, the chunkhash changes when the JS file changes. CSS is in the same chunk as the JS file that introduced it, so the chunkhash of the exported CSS file changes every time the JS file changes but the CSS file does not change. Therefore, for the name of the EXPORTED CSS file, use Contenthash.

ExtractTextPlugin('[name].[chunkhash:8].css');
Copy the code

Back to the point, here

output: {
  filename: '[name].[hash:8].js',
},
Copy the code

It’s not appropriate to use hash for filename, it’s still not appropriate to change it to a new fullhash, it should be changed to chunkhash:

output: {
  filename: '[name].[chunkhash:8].js',
},
Copy the code

Four, plug-in use error

Now, we generate a new JS file in the./dist directory after each NPM run build, which is not what we expected. Therefore, we should empty the./dist directory before each rebuild. The detailed operations are as follows.

First install the dependencies:

npm i -D clean-webpack-plugin

/webpack.config.js add plugin configuration. After configuration, the file content is as follows:

/ * * *@type {import('webpack').Configuration}* /
var CleanWebpackPlugin = require('clean-webpack-plugin').CleanWebpackPlugin;

module.exports = {
  mode: 'none'.entry: {
    index: './src/index.js',},output: {
    filename: '[name].[chunkhash:8].js',},module: {
    rules: [{test: /\.md$/,
        use: [
          './markdown-loader.js']]}},plugins: [
    new CleanWebpackPlugin(),
  ]
}
Copy the code

Var CleanWebpackPlugin = require(‘clean-webpack-plugin’).CleanWebpackPlugin; The first letter of CleanWebpackPlugin in CleanWebpackPlugin needs to be capitulated, otherwise the following error will be reported:

> TypeError: cleanWebpackPlugin is not a constructor

I ran into this problem when I accidentally wrote it in lowercase.

Five,outputThe configuration item is not configuredpathThe clean- Webpack-plugin is invalid because of the configuration item

At this point, it looks like the clean-webpack-plugin is in effect, but when we modify the contents of./ SRC /index.js and run the NPM run build again, we will see that multiple index.xxxxxxx.js files are generated in./dist. This indicates that the clean-Webpack-Plugin does not actually work. Through the investigation, it is found that the output configuration item is not configured with path configuration item, so we add path to the output:

output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash:8].js',},Copy the code

That’s when the clean-Webpack-plugin really works.

Let’s implement the actual markDown to HTML logic. Here we use the marked package to do the transformation:

. / markdown – loader. Js file:

// ./markdown-loader.js
const marked = require('marked');
module.exports = (source) = > {
  console.log('* * * * * * * * * * * * * * * * * * * * * * * * * * * * *');
  console.log(source);
  console.log('* * * * * * * * * * * * * * * * * * * * * * * * * * * * *');
  // return `module.exports = ${JSON.stringify(source)}; `
  const result = marked(source);
  console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --');
  console.log(result);
  console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --');
  const code = `module.exports = The ${JSON.stringify(result)}`;
  return code;
};
Copy the code

After you run NPM run build, you get the following console output:

&gt; [email protected] build D: \ dev - webpack - loader & gt; npx webpack --config ./webpack.config.js ***************************** # test markdown Hello, this is a test markdown! ***************************** ----------------------------- <h1>test markdown</h1> <p>Hello, this is a test markdown! < / p > -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- (webpack - cli) Compilation finished asset index. 302 aeaa6. Js 2.79 KiB [emitted] (name: index) runtime modules 657 bytes 3 modules cacheable modules 178 bytes ./src/index.js 74 bytes [built] [code generated] / SRC /markdown/test.md 104 bytes [built] [code generated] Webpack 5.6.0 compiled successfully in 354 msCopy the code

I marked it

# test markdown

Hello, this is a test markdown!
Copy the code

Into the

<h1>test markdown</h1> <p>Hello, this is a test markdown! </p>Copy the code

How to remove interference when analyzing the package result file

From time to time during development, it is inevitable to analyze the packaged code to make sure that the output is delivered as expected when packaged. However, if you set mode to ‘development’ or ‘production’ in the Webpack configuration, you get a lot of clutter. In ‘production’ mode, the code is compressed and very distracting to read. You might not even know where to start. You can see it in the following two pictures.

If mode is set to ‘development’

Set mode to ‘production’;

Here are some practical lessons:

First, to make the packaging easier to read, mode is set to ‘None’ in this article to avoid the eval function caused by ‘development’ and automatic compression when set to ‘production’.

Second, we strip out comments such as /****/ and format them before deleting any extra blank lines.

After that, the code becomes as follows:

(() = > { // webpackBootstrap
  var __webpack_modules__ = ([
    /* 0 */
    ((__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony import */
      var _markdown_test_md__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
      /* harmony import */
      var _markdown_test_md__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/ __webpack_require__.n(_markdown_test_md__WEBPACK_IMPORTED_MODULE_0__);
      // ./src/index.js
      console.log((_markdown_test_md__WEBPACK_IMPORTED_MODULE_0___default()));
    }),
    / * 1 * /
    ((module) = > {
      module.exports = "

test markdown

\n

Hello, this is a test markdown!

\n"
})]);// The module cache var __webpack_module_cache__ = {}; // The require function function __webpack_require__(moduleId) { // Check if module is in cache if (__webpack_module_cache__[moduleId]) { return __webpack_module_cache__[moduleId].exports; } // Create a new module (and put it into the cache) var module = __webpack_module_cache__[moduleId] = { // no module.id needed // no module.loaded needed exports: {}};// Execute the module function __webpack_modules__[moduleId](module.module.exports, __webpack_require__); // Return the exports of the module return module.exports; } /* webpack/runtime/compat get default export */ (() = > { // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = (module) = > { var getter = module && module.__esModule ? () = > module['default'] : () = > module; __webpack_require__.d(getter, { a: getter }); returngetter; }; }) ();/* webpack/runtime/define property getters */ (() = > { // define getter functions for harmony exports __webpack_require__.d = (exports, definition) = > { for (var key in definition) { if(__webpack_require__.o(definition, key) && ! __webpack_require__.o(exports, key)) { Object.defineProperty(exports, key, { enumerable: true.get: definition[key] }); }}}; }) ();/* webpack/runtime/hasOwnProperty shorthand */ (() = > { __webpack_require__.o = (obj, prop) = > Object.prototype.hasOwnProperty.call(obj, prop) })(); /* webpack/runtime/make namespace object */ (() = > { // define __esModule on exports __webpack_require__.r = (exports) = > { if (typeof Symbol! = ='undefined' && Symbol.toStringTag) { Object.defineProperty(exports.Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports.'__esModule', { value: true}); }; }) ();// startup // Load entry module __webpack_require__(0); // This entry module used 'exports' so it can't be inlined}) ();Copy the code

Third, we make full use of the IDE’s ability to fold and expand code to improve readability.

By folding the code, you can see that the final packaging result is an IIFE function:

After expanding part of the content, look at the following figure:

__webpack_modules__ is defined to store the packed modules, and __webpack_module_cache__ is defined to store the cache of loaded modules. Then define the require function — __webpack_require__, then define n, d, o, and r methods to attach to __webpack_require__, and finally by executing __webpack_require__(0); Load the module with the entry module (that is, moduleId 0).

Then we expand the contents of __webpack_modules__ :

var __webpack_modules__ = ([
  /* 0 */
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */
    var _markdown_test_md__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
    /* harmony import */
    var _markdown_test_md__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/ __webpack_require__.n(_markdown_test_md__WEBPACK_IMPORTED_MODULE_0__);
    // ./src/index.js
    console.log((_markdown_test_md__WEBPACK_IMPORTED_MODULE_0___default()));
  }),
  / * 1 * /
  ((module) = > {
    module.exports = "

test markdown

\n

Hello, this is a test markdown!

\n"
})]);Copy the code

Each item in the array is a function, and its contents are the contents of the packaged module files./ SRC /index.js and./ SRC /markdown/test.md.

Expand __webpack_require__ again:

// The require function
function __webpack_require__(moduleId) {
  // Check if module is in cache
  if (__webpack_module_cache__[moduleId]) {
    return __webpack_module_cache__[moduleId].exports;
  }
  // Create a new module (and put it into the cache)
  var module = __webpack_module_cache__[moduleId] = {
    // no module.id needed
    // no module.loaded needed
    exports: {}};// Execute the module function
  __webpack_modules__[moduleId](module.module.exports, __webpack_require__);
  // Return the exports of the module
  return module.exports;
}
Copy the code

This method is the method that loads the module, passing in the module Id. If __webpack_module_cache__[moduleId] is true, it indicates that the module has already been loaded and is thus taken from the cache. Otherwise, it creates a module and caches it, then executes the module function and returns what the module needs to export.

Then let’s look at the four tool methods: __webpack_require__.n, __webpack_require__.d, __webpack_require__.o, and __webpack_require__.r. The code is as follows:

/* webpack/runtime/compat get default export */
(() = > {
  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = (module) = > {
    var getter = module && module.__esModule ?
      () = > module['default'] :
      () = > module;
    __webpack_require__.d(getter, {
      a: getter
    });
    returngetter; }; }) ();/* webpack/runtime/define property getters */
(() = > {
  // define getter functions for harmony exports
  __webpack_require__.d = (exports, definition) = > {
    for (var key in definition) {
      if(__webpack_require__.o(definition, key) && ! __webpack_require__.o(exports, key)) {
        Object.defineProperty(exports, key, {
          enumerable: true.get: definition[key] }); }}}; }) ();/* webpack/runtime/hasOwnProperty shorthand */
(() = > {
  __webpack_require__.o = (obj, prop) = > Object.prototype.hasOwnProperty.call(obj, prop)
})();

/* webpack/runtime/make namespace object */
(() = > {
  // define __esModule on exports
  __webpack_require__.r = (exports) = > {
    if (typeof Symbol! = ='undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports.Symbol.toStringTag, {
        value: 'Module'
      });
    }
    Object.defineProperty(exports.'__esModule', {
      value: true}); }; }) ();Copy the code

By observing, we can see that the Object. DefineProperty method is used in all of them. Object.defineproperty (obj, prop, descriptor); object.defineProperty (obj, Prop, Descriptor); object.defineProperty (obj, Prop, Descriptor); It takes three arguments:

  • obj

The object for which properties are to be defined.

  • prop

The name or Symbol of the property to define or modify.

  • descriptor

Attribute descriptors to define or modify. There are two main types of attribute descriptors that exist in objects: data descriptors and access descriptors. A data descriptor is an attribute that has a value, which can be writable or unwritable. Access descriptors are properties described by getter and setter functions. A descriptor can only be one of these two; It can’t be both. The keys they can use are as follows:

The data descriptor can use a different key, enumerable, value, or writable, and the access descriptor can use a different key, enumerable, get, or set.

With that in mind, let’s move on to the packaged code. This is because __webpack_require__.d is used in __webpack_require__.n and __webpack_require__.o is used in __webpack_require__.d.

So let’s first read the __webpack_require__.o code. __webpack_require__. O = (obj, prop) = > Object. The prototype. The hasOwnProperty. Call (obj, prop), So it is just for the Object. The prototype. A hasOwnProperty packaging, if there is a prop attributes used for judgment on the Object obj.

Next, let’s look at the code for __webpack_require__.r:

/* webpack/runtime/make namespace object */
(() = > {
  // define __esModule on exports
  __webpack_require__.r = (exports) = > {
    if (typeof Symbol! = ='undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports.Symbol.toStringTag, {
        value: 'Module'
      });
    }
    Object.defineProperty(exports.'__esModule', {
      value: true}); }; }) ();Copy the code

ToStringTag is a built-in Symbol. It is usually used as the key of an object’s property. The value of the property should be a string that represents the custom type tag of the object. Usually only built-in Object. The prototype. The toString () method to read the label and put it in his own return values. Let’s use an example to illustrate:

const obj = {};
Object.defineProperty(obj, Symbol.toStringTag, { value: 'MyCustomObject' });
Copy the code

This definition, then use the Object. The prototype. ToString. Call get (obj) to obtain the types “[Object MyCustomObject]”.

__webpack_require__.r does just two things:

First, let the Object. The prototype. ToString. Call (exports) return values for “[Object Module]”.

Second, set the __esModule attribute of exports to true.

In summary, the function is to label the module with the ES6 module identifier.

Let’s look at the code for __webpack_require__.d:

/* webpack/runtime/define property getters */
(() = > {
  // define getter functions for harmony exports
  __webpack_require__.d = (exports, definition) = > {
    for (var key in definition) {
      if(__webpack_require__.o(definition, key) && ! __webpack_require__.o(exports, key)) {
        Object.defineProperty(exports, key, {
          enumerable: true.get: definition[key] }); }}}; }) ();Copy the code

Exports did not have a key value, and exports did not have a key value. In this case, exports did not have a key value. In this case, exports did not have a key value.

Finally, look at the __webpack_require__.n code:

/* webpack/runtime/compat get default export */
(() = > {
  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = (module) = > {
    var getter = module && module.__esModule ?
      () = > module['default'] :
      () = > module;
    __webpack_require__.d(getter, {
      a: getter
    });
    returngetter; }; }) ();Copy the code

Its function is to return different modul functions depending on whether moDUL is an ES6 module (with or without an __esModule attribute) and to add getters to the return values.

At this point, the entire packaged code is analyzed. Overall, it’s relatively simple, mainly mastering the aforementioned methods of working with Webpack (mode set to ‘None’), removing intrusive comments, and using the editor’s code folding and unwrapping.