Webpack’s event flow and plug-in mechanism

We have talked about the operation principle of Webpack and loader. In this article, we will talk about the mechanism of Webpack event flow and plug-in. This series is divided into three parts

  1. Webpack5 packaging analysis
  2. Webpack loader implementation
  3. Webpack’s event flow and plugin principles

Webpack event flow

What is the event flow of Webpack, here to introduce the simple webpack inside a paragraph.

Webpack is like a production line that goes through a series of processes to convert source files into output. Each process on this production line has a single responsibility. There are dependencies between multiple processes. Only after the current process is completed can the next process be handled. A plug-in is like a function that plugs into a production line and works with resources on the production line at a specific time. Webpack organizes this complex production line through Tapable. Webpack broadcasts events as it runs, and the plug-in only needs to listen for the events it cares about to join the production line and change how the production line works. Webpack’s event flow mechanism ensures the orderliness of plug-ins and makes the whole system very extensible. — Wu Haolin, “Simple Webpack”

How WebPack works

In the first article, we implemented a simple Webpack by ourselves. We use a picture to understand the overall operation mechanism of WebPack from the start to the packaging of resources.There are several key steps

  1. When we run Webpack, it will read your incoming configuration file (webpack.config.js) and run itcompile.runTo initialize the intrinsic build parameters (entry files, etc.), and then start analyzing the module (Environment hook function)
  2. Next comes the entryOption phase, where WebPack starts reading configured entry entries and recursively iterates through all entry files (entryOption hooks)
  3. After WebPack enters the entry file, it starts analyzing the dependencies, and proceedscompilationProcess. (compilation Hook function)
    • Compile the file contents using the configured Loader (buildModule), we can get the module’s resource(resource path) from the parameter module of the incoming event callback (buildModule Hook function)
    compiler.hooks.compilation.tap('compilation'.(compilation , compilationParams) = > {
      compilation.hooks.buildModule.tap('sourceMap'.(module) = > {
        console.log(module.resource)  // Resource path})})Copy the code
    • The files processed by loader will be parsed through Acorn to generate ast syntax tree (normalModuleLoader) to analyze the dependencies of files, and then form the dependency array to repeat the above module analysis process (Webpackage 5 has been abandonednormalModuleLoaderThis hook)
  4. Emit phase: All files have been compiled and converted, including the final output of the resource. We can retrieve the required data from the compilation.assets of the incoming event callback, including the output of the resource, Chunk of code, and so on.

The diagram below is a comparison of the overall operation mechanism and the main plug-in diagramThere is also a more detailed module-based running flow diagram based on WebPack (dotted lines represent repeated executions)

Plug-in development for Webpack

We mainly use 2 plugins

  1. Use MD mode to list the file packaging resources
  2. Analyze the components that the file depends on through plug-ins

First we need to know how the plug-in for WebPack needs to be developed. It requires two conditions

  1. Must be a class
  2. To expose an Apply method, WebPack executes the method at initialization and passes in onecompiler, the source code is like this
if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else{ plugin.apply(compiler); }}}Copy the code

List package resource plug-ins

As we can see from the above figure, we can fetch the packaged resource after it is packaged and then add a new file.

  1. By using the emit hook function, we can use the emit hook function in the callback functioncompilationGet the parameters inassetsThe resource list
  2. Then add a resource to process the process through WebPack
class FileListPlugin {
    constructor({ filename = 'index.md' }) {
        this.filename = filename;
    }

    apply(compiler) {
        compiler.hooks.emit.tapAsync('FileListPlugin'.(compilation, cb) = > {
            let assets = compilation.assets;
            let content = '## 文件    资源大小\r\n';
            Object.entries(assets).forEach(([key, value]) = > {
                content += `${key}    ${value.size()}\r\n`;
            });
            assets[this.filename] = {
                size() {
                    return content.length;
                },
                source() {
                    returncontent; }}; cb(); }); }}module.exports = FileListPlugin;
Copy the code

The list of components used

In today’s component mentality, a component can be used by multiple pages, sometimes we modify a component without knowing whether it will affect other pages, so we developed a plug-in to count the pages used by the component. The usage is different in webpack4 and webpack5, and we are now analyzing based on webpack5.

  1. As you can see from the flowchart above, each asset has a module phaseNormalModuleFactory, all we can do after the module is processed by loaderafterResolveTo analyze the module
  2. After analyzing module dependencies, you can passdoneHooks and so on output the parsed content
class NormalModuleFactory {
  constructor() {
    this.dependencies = {}
    this.entry = ' '
    this.workDictory = ""
  }

  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap('NormalModuleFactory'.(nmf) = > {
      nmf.hooks.afterResolve.tapAsync('nmf'.(result, callback) = > {
        let { request, contextInfo, context } = result
        if(! request.includes('node_modules')) {
          if(! contextInfo.issuer) {console.log('entry', path.normalize(request))
            this.entry = request
            this.workDictory = context
          } else if(! ['react'.'react-dom'].includes(request)){
            // There is a dependency contextInfo which is the context in which the resource is requested
            const requestPath = path.relative(this.workDictory, contextInfo.issuer)
            // './component/Hello requestPath: src/index.js
            if(! requestPath.includes("node_modules")) {
              const resourcePath = path.join(context, request)
              console.log(requestPath, request)
              request = path.relative(this.workDictory, resourcePath)
              if(!this.dependencies[request]) {
                this.dependencies[request] = [requestPath]
              } else {
                this.dependencies[request].push(requestPath)
              }
            }
          }
        }
        callback()
      })
    })
    compiler.hooks.done.tap('NormalModuleFactory'.() = > {
      console.log(this.dependencies)
    })
  }
}

module.exports = NormalModuleFactory
Copy the code

After executing NPX webpack, we can get this content, key is the path of the component being used, value is the page or component using this component, and we can analyze the specific style and component dependency based on the path matching according to the project content.

{
  'src\\base\\a.js': [ 'src\\index.js' ],
  'src\\component\\Hcc': [ 'src\\index.js' ],
  'src\\component\\Hello': [ 'src\\index.js', 'src\\component\\Hcc.jsx' ],
  'src\\index.less': [ 'src\\index.js' ],
  'src\\base\\b': [ 'src\\base\\a.js' ],
  'src\\pic\\hcc.jpg': [ 'src\\index.less' ]
}
Copy the code
Refer to the article
  1. Explore the webpack plugin mechanism
  2. Uncover the WebPack plug-in workflow and principles