Discussion on the Implementation Principle of Webpack4 plugins

  • 2021-11-23 21:49:56
  • OfStack

Preface to the table of contents Recognize Experience is the mother of wisdom

Preface

In wabpack, the core function except loader should be plugins plug-in. It broadcasts 1 series of events during the execution of webpack, and plugin will listen to these events and process the output files correspondingly through webpack Api. For example, hmlt-webpack-plugin copies the template magic sword index. html to dist directory

Recognize

First, through the source code to understand the basic structure of plugins under 1
https://github. com/webpack/webpack/blob/webpack-4/lib/Compiler. js Line 551


//  Create 1 Compiler 
createChildCompiler(
  compilation,
  compilerName,
  compilerIndex,
  outputOptions,
  plugins //  There are plug-ins inside 
) {

   // new 1 A   Compiler 
  const childCompiler = new Compiler(this.context);
  //  Looking for everything that exists  plugins  Plug-ins 
  if (Array.isArray(plugins)) {
    for (const plugin of plugins) {
       //  If it exists,   Call the  plugin  Adj.  apply  Method 
      plugin.apply(childCompiler);
    }
  }
  
  //  Traversal search  plugin  Corresponding  hooks
  for (const name in this.hooks) {
    if (
      ![
        "make",
        "compile",
        "emit",
        "afterEmit",
        "invalid",
        "done",
        "thisCompilation"
      ].includes(name)
    ) {
    
      //  Find the corresponding  hooks  And call,  
      if (childCompiler.hooks[name]) {
        childCompiler.hooks[name].taps = this.hooks[name].taps.slice();
      }
    }
  }
 
 // ....  Omission  ....

  return childCompiler;
}

Through the above source code, we can see that plugin is essentially a class. First, new is a class of compiler, passing in the current context, then judging whether it exists, if it exists, directly calling the apply method corresponding to plugin, and then finding the hooks event stream corresponding to plugin call, and transmitting it to the corresponding hooks event
Where did hooks come from?

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


//  Above  Compiler  Class inherits from  Tapable  Class, and  Tapable  That defines these  hooks  Event flow 
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;
               
            //  Pass  tab  Call the corresponding  comiler  Compiler, and pass in 1 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 deduce the basic structure and usage of plugin, which is as follows


//  Definition 1 A  plugins  Class    
class MyPlugins {
    //  It says above  new 1 Compiler instances that execute the  apply  Method, passing in the corresponding  comiler  Instances 
    apply (compiler) {
        //  Call  new  Come out  compiler  Instance under the  hooks  Event flow, through  tab  Trigger and receive 1 Callback function 
        compiler.hooks.done.tap('1 It is a nickname for plug-ins ', ( Default receive parameter ) => {
            console.log(' Enter the actuator ');
        })
    }
}
//  Export 
module.exports = MyPlugins

ok, the above is a simple template, let's try the internal hook function, whether it will be called and triggered as desired

Configuring 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
    ]
}

Synchronize plugin plug-in simulation call


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

module.exports = DonePlugins

Asynchronous plugin plug-in simulation call


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

module.exports = AsyncPlugins

Finally compile webpack can see the compilation console, print and execute respectively: compile completed, execute: file emitted, indicating that this can be called to hooks event flow, and can be triggered.

Experience is the mother of wisdom

Understand the basic structure and the way to use, now to hand-write an plugin plug-in, well, to a file to explain the plug-in, we daily package, can package an xxx. md file to dist directory, to do a package description, to achieve such a small function

File description plug-in


class FileListPlugin {
    //  Initialize, get the name of the file 
    constructor ({filename}) {
        this.filename = filename
    }
    //  The same template form, defining the  apply  Method 
    apply (compiler) {
        compiler.hooks.emit.tap('FileListPlugin', (compilation) => {
            // assets  Static resources, which can be printed out   compilation  Parameters, as well as many methods and properties 
            let assets = compilation.assets;
            
            //  Define the output document structure 
            let content = `##  Filename    Resource size \r\n`
            
            //  Traversing static resources and dynamically combining 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 and node_modules  At the same level,   Customize  plugins ,  And  loader  Similar 
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, when we re-package, we can see that each package in the dist directory will appear an xxx. md file, and the content of this file is the content above us


Related articles: