A brief discussion on the implementation principle of Webpack4 plugins

A brief discussion on the implementation principle of Webpack4 plugins

Preface

In wabpack, the core function is plugins, besides loader. It broadcasts a series of events during the execution of webpack. Plugins listen to these events and process the output files through webpack API. For example, hmlt-webpack-plugin copies the template index.html to the dist directory.

know

Let's first understand the basic structure of plugins through the source code
https://github.com/webpack/webpack/blob/webpack-4/lib/Compiler.js line 551

// Create a compiler createChildCompiler(
  compilation,
  compilerName,
  compilerIndex,
  outputOptions,
  plugins // contains plugins) {

   // new a compiler const childCompiler = new Compiler(this.context);
  // Find all existing plugins if (Array.isArray(plugins)) {
    for (const plugin of plugins) {
       // If it exists, call plugin's apply method plugin.apply(childCompiler);
    }
  }
  
  // Traverse and find the hooks corresponding to the plugin
  for (const name in this.hooks) {
    if (
      ![
        "make",
        "compile",
        "emit",
        "afterEmit",
        "invalid",
        "done",
        "thisCompilation"
      ].includes(name)
    ) {
    
      // Find the corresponding hooks and call them, 
      if (childCompiler.hooks[name]) {
        childCompiler.hooks[name].taps = this.hooks[name].taps.slice();
      }
    }
  }
 
 // .... omitted....

  return childCompiler;
}

From the above source code, we can see that plugin is essentially a class. First, a compiler class is created, the current context is passed in, and then it is determined whether it exists. If it exists, the apply method of the corresponding plugin is directly called, and then the hooks event stream called by the corresponding plugin is found and emitted to the corresponding hooks event.
Where do hooks come from?

https://github.com/webpack/webpack/blob/webpack-4/lib/Compiler.js line 42

// The Compiler class above inherits from the Tapable class, and Tapable defines these hooks event flows class Compiler extends Tapable {
 constructor(context) {
            super();
            this.hooks = {
                    /** @type {SyncBailHook<Compilation>} */
                    shouldEmit: new SyncBailHook(["compilation"]),
                    /** @type {AsyncSeriesHook<Stats>} */
                    done: new AsyncSeriesHook(["stats"]),
                    /** @type {AsyncSeriesHook<>} */
                    additionalPass: new AsyncSeriesHook([]),
                    /** @type {AsyncSeriesHook<Compiler>} */
                    beforeRun: new AsyncSeriesHook(["compiler"]),
                    /** @type {AsyncSeriesHook<Compiler>} */
                    run: new AsyncSeriesHook(["compiler"]),
                    /** @type {AsyncSeriesHook<Compilation>} */
                    emit: new AsyncSeriesHook(["compilation"]),
                    /** @type {AsyncSeriesHook<string, Buffer>} */
                    assetEmitted: new AsyncSeriesHook(["file", "content"]),
                    /** @type {AsyncSeriesHook<Compilation>} */
                    afterEmit: new AsyncSeriesHook(["compilation"]),

                    /** @type {SyncHook<Compilation, CompilationParams>} */
                    thisCompilation: new SyncHook(["compilation", "params"]),
                    /** @type {SyncHook<Compilation, CompilationParams>} */
                    compilation: new SyncHook(["compilation", "params"]),
                    /** @type {SyncHook<NormalModuleFactory>} */
                    normalModuleFactory: new SyncHook(["normalModuleFactory"]),
                    /** @type {SyncHook<ContextModuleFactory>} */
                    contextModuleFactory: new SyncHook(["contextModulefactory"]),

                    /** @type {AsyncSeriesHook<CompilationParams>} */
                    beforeCompile: new AsyncSeriesHook(["params"]),
                    /** @type {SyncHook<CompilationParams>} */
                    compile: new SyncHook(["params"]),
                    /** @type {AsyncParallelHook<Compilation>} */
                    make: new AsyncParallelHook(["compilation"]),
                    /** @type {AsyncSeriesHook<Compilation>} */
                    afterCompile: new AsyncSeriesHook(["compilation"]),

                    /** @type {AsyncSeriesHook<Compiler>} */
                    watchRun: new AsyncSeriesHook(["compiler"]),
                    /** @type {SyncHook<Error>} */
                    failed: new SyncHook(["error"]),
                    /** @type {SyncHook<string, string>} */
                    invalid: new SyncHook(["filename", "changeTime"]),
                    /** @type {SyncHook} */
                    watchClose: new SyncHook([]),

                    /** @type {SyncBailHook<string, string, any[]>} */
                    infrastructureLog: new SyncBailHook(["origin", "type", "args"]),

                    // TODO the following hooks are weirdly located here
                    // TODO move them for webpack 5
                    /** @type {SyncHook} */
                    environment: new SyncHook([]),
                    /** @type {SyncHook} */
                    afterEnvironment: new SyncHook([]),
                    /** @type {SyncHook<Compiler>} */
                    afterPlugins: new SyncHook(["compiler"]),
                    /** @type {SyncHook<Compiler>} */
                    afterResolvers: new SyncHook(["compiler"]),
                    /** @type {SyncBailHook<string, Entry>} */
                    entryOption: new SyncBailHook(["context", "entry"])
            };
            
            // TODO webpack 5 remove this
            this.hooks.infrastructurelog = this.hooks.infrastructureLog;
               
            // Call the corresponding compiler through tab and pass in a callback function this._pluginCompat.tap("Compiler", options => {
                    switch (options.name) {
                            case "additional-pass":
                            case "before-run":
                            case "run":
                            case "emit":
                            case "after-emit":
                            case "before-compile":
                            case "make":
                            case "after-compile":
                            case "watch-run":
                                    options.async = true;
                                    break;
                    }
            });
            // Omitted below......
  }

Well, after understanding the basic structure, you can infer the basic structure and usage of the plugin, which is as follows

// Define a plugins class class MyPlugins {
    // As mentioned above, a new compiler instance will be created, and the instance's apply method will be executed, passing in the corresponding compiler instance apply (compiler) {
        // Call the hooks event flow under the new compiler instance, trigger it through tab, and receive a callback function compiler.hooks.done.tap('usually the plugin nickname', (default receiving parameters) => {
            console.log('Enter execution body');
        })
    }
}
// Exportmodule.exports = MyPlugins

OK, the above is a simple template. Let's try the internal hook function to see if it will be called and triggered as expected.

Configure webpack

let path = require('path')
let DonePlugin = require('./plugins/DonePlugins')
let AsyncPlugins = require('./plugins/AsyncPlugins')

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'build.js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new DonePlugin(), // Internal synchronization hooks
        new AsyncPlugins() // Internal asynchronous hooks
    ]
}

Synchronous plugin plugin simulation call

class DonePlugins {
    apply (compiler) {
        compiler.hooks.done.tap('DonePlugin', (stats) => {
            console.log('Execution: Compilation completed');
        })
    }
}

module.exports = DonePlugins

Asynchronous plugin plugin simulation call

class AsyncPlugins {
    apply (compiler) {
        compiler.hooks.emit.tapAsync('AsyncPlugin', (complete, callback) => {
            setTimeout(() => {
                console.log('Execution: file emitted');
                callback()
            }, 1000)
        })
    }
}

module.exports = AsyncPlugins

Finally, compile webpack and you can see the compilation console, which prints and executes: compilation completed, execution: the file is emitted, indicating that the hooks event flow can be called and triggered.

Practice makes perfect

Now that we have understood the basic structure and how to use it, let's write a plugin. Well, let's write a file description plugin. In our daily packaging, we can package a xxx.md file into the dist directory and make a packaging description to achieve such a small function.

File Description Plugin

class FileListPlugin {
    // Initialization, get the file name constructor ({filename}) {
        this.filename = filename
    }
    // Same template format, define the apply method apply (compiler) {
        compiler.hooks.emit.tap('FileListPlugin', (compilation) => {
            // assets static resources, can print out compilation parameters, and there are many methods and properties let assets = compilation.assets;
            
            // Define the output document structure let content = `## file name resource size\r\n`
            
            // Traverse static resources and dynamically combine output content Object.entries(assets).forEach(([filename, stateObj]) => {
                content += `- ${filename} ${stateObj.size()}\r\n`
            })
            
            // Output resource object assets[this.filename] = {
                source () {
                    return content;
                },
                size () {
                    return content.length
                }
            }
            
        })
    }
}
// Export module.exports = FileListPlugin

webpack configuration

let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
// plugins directory is at the same level as node_modules, custom plugins, similar to loader let FileListPlugin = require('./plugins/FileListPlugin')

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'build.js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'index.html'
        }),
        new FileListPlugin({
            filename: 'list.md'
        })
    ]
}

OK, through the above configuration, we can see that when we package again, a xxx.md file will appear in the dist directory each time it is packaged, and the content of this file is the content above.

This is the end of this article about the implementation principle of Webpack4 plugins. For more related Webpack4 plugins content, please search for previous articles on 123WORDPRESS.COM or continue to browse the following related articles. I hope everyone will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • Web development js string concatenation placeholder and conlose object API detailed explanation
  • Web project development JS function anti-shake and throttling sample code
  • Multiple solutions for cross-domain reasons in web development
  • js to realize web message board function
  • JavaScript article will show you how to play with web forms
  • JavaScript web page entry-level development detailed explanation

<<:  Detailed explanation of the use of MySQL comparison operator regular expression matching REGEXP

>>:  Summary of common commands for Ubuntu servers

Recommend

Analysis of the difference between Mysql InnoDB and MyISAM

MySQL supports many types of tables (i.e. storage...

Detailed introduction to logs in Linux system

Table of contents 1. Log related services 2. Comm...

Linux type version memory disk query command introduction

1. First, let’s have a general introduction to th...

How to use nginx as a proxy cache

The purpose of using cache is to reduce the press...

MySQL 8.0 upgrade experience

Table of contents Preface 1. First completely uni...

The most commonly used HTML tags to create web pages

1. Optimization of commonly used HTML tags HTML s...

Introduction to Vue life cycle and detailed explanation of hook functions

Table of contents Vue life cycle introduction and...

When backing up files in Centos7, add the backup date to the backup file

Linux uses files as the basis to manage the devic...

Analyzing ab performance test results under Apache

I have always used Loadrunner to do performance t...

Solution to forgetting the MYSQL database password under MAC

Quick solution for forgetting MYSQL database pass...

HTML framework_Powernode Java Academy

1. Framework A browser document window can only d...

How to capture exceptions gracefully in React

Table of contents Preface ErrorBoundary Beyond Er...

HTML Tutorial: DOCTYPE Abbreviation

When writing HTML code, the first line should be ...

Solution to the problem that the image name is none after Docker load

Recently, I found that after using the docker loa...