The introduction

The most central mechanism around the Webpack packaging process is the so-called Plugin mechanism.

Plug-ins are a key part of the WebPack ecosystem and provide a powerful way for the community to directly touch the WebPack compilation process.

Today, we’ll talk about the essential core Plugin mechanism in Webpack

For past Webpack highlights you can check out the column on how to play Webpack from scratch.

Plugin

In essence, different hooks are initialized for each compiled object during Webpack compilation. Developers can listen to these hooks in their own Plugin, and trigger the corresponding Hook to inject specific logic at a specific time of packaging to achieve their own behavior.

The hooks in Plugin are implemented entirely based on tapable, if you are interested. You can check out this Tapable post and just read one.

Common objects in the Plugin

Let’s first look at which objects in Webpack can register hooks:

  • compiler Hook

  • compilation Hook

  • ContextModuleFactory Hook

  • JavascriptParser Hooks

  • NormalModuleFactory Hooks

Don’t worry, these 5 objects may feel strange to you right now, and I’ll take you through them step by step.

Basic components of a plug-in

Let’s start with one of the simplest plug-ins that executes the done output when the compilation is complete:

class DonePlugin {
  apply(compiler) {
    // Call Compiler Hook to register additional logic
    compiler.hooks.done.tap('Plugin Done'.() = > {
      console.log('compilation done'); }); }}module.exports = DonePlugin;
Copy the code

At this point, the compilation terminal will print a line compilation Done when the compilation is complete.

We can see that a Webpack Plugin mainly consists of the following aspects:

  • First, a Plugin should be a class, and of course a function.

  • Secondly, there should be an Apply method on the prototype object of the Plugin. When WebPack creates the Compiler object, the Apply method on each plug-in instance is called and the Compiler object is passed in as a parameter.

  • You also need to specify a Hook bound to the Compiler object, such as compiler.hooks. Done. Tap listens for done events on the incoming Compiler object.

  • Handle the logic of the plug-in itself in the Hook callback, and here we simply do console.log.

  • Depending on the type of Hook, notify Webpack to continue after the logic is complete.

The build object for the plug-in

We mentioned above which corresponding objects in Webpack Plugin can be Hook registered, and I will take you through these five objects.

Understanding them is the key to understanding and applying the Webpack Plugin.

The compiler object

class DonePlugin {
  apply(compiler) {
    // Call Compiler Hook to register additional logic
    compiler.hooks.done.tapAsync('Plugin Done'.(stats, callback) = > {
      console.log(compiler, 'the compiler object'); }); }}module.exports = DonePlugin;
Copy the code

The complete Webpack environment configuration is stored in the Compiler object, which creates a Compilation instance through all the options passed through the CLI or the Node API.

This object is created when Webpack is first started, and we can use the Compiler object to query the main Webapck environment configuration, such as loader, plugin, and so on.

Compiler you can think of it as a singleton, a unique object that is created only once each time the WebPack build is started.

Compiler objects have the following main properties:

  • Options gives you access to the complete configuration information for webPack during compilation.

The compiler.options object stores all configuration files when starting webpack this time, including but not limited to loaders, entry, output, plugin and other complete configuration information.

  • Compiler. InputFileSystem (get file API objects) and outputFileSystem (output file API objects) can help us to implement file operations. You can think of it simply as an extension of the FS module in the Node Api.

If you want some of the input and output behavior of your custom plug-in to be as synchronized as possible with WebPack, these two variables are best used in compiler.

The extra note is that when compiler objects are run in Watch mode, usually devServer, the outputFileSystem is rewritten as an in-memory output object. In other words, webpack builds in Watch mode do not generate actual files but are stored in memory.

If your plugin for file operations exist corresponding logic, then next. Please use the compiler inputFileSystem/outputFileSystem replace code of fs.

  • In addition, compiler.hooks save extensions to different hooks from Tapable, listening on these hooks to implement different logic in the Compiler lifecycle.

The compiler object properties can be viewed in webpack/lib/ compiler.js.

Compilation object

class DonePlugin {
  apply(compiler) {
    compiler.hooks.afterEmit.tapAsync(
      'Plugin Done'.(compilation, callback) = > {
        console.log(compilation, 'compilation objects'); }); }}module.exports = DonePlugin;
Copy the code

A compilation object represents a build of a resource, and a compilation instance has access to all modules and their dependencies.

A compilation object will compile all the modules in the build dependency diagram. During compilation, modules are loaded, sealed, optimized, chunk, hashed, and restored.

The compilation object allows you to retrieve/manipulate the module resources being compiled, the generated resources, the files that are being changed, and the tracked state information. The compilation also allows for Hook callbacks at different times based on tapable.

To put it simply, in devServer every change in code is recompiled, which you can interpret as creating a new compilation object for every build.

There are several main properties about the Compilation object:

  • modules

Its value is of a Set type, modules. Simply put, you can think of a file as a module, whether you write your file using ESM or Commonjs. Each file can be understood as a separate module.

  • chunks

The so-called chunk is a code block composed of multiple modules. When Webapck is packaged, it will first analyze the corresponding dependency relationship according to the entry file of the project, and combine the modules dependent on the entry into a large object, which can be called chunk.

Chunks are, of course, a Set of chunks.

  • assets

The assets object records the result of generating all files for this packaging.

  • hooks

A series of hooks are also provided in the Compilation object based on Tapable for logical addition and modification during the compilation module stage.

Since Webpack 5, a set of compilation apis have been provided instead of manipulating properties such as Moduels /chunks/assets directly, allowing developers to manipulate the corresponding apis to affect packaging results.

You can see it here, for example, for common output file work, using the Compiler. emitAsset API instead of working directly with compiler. assets objects.

ContextModuleFactory Hook

class DonePlugin {
  apply(compiler) {
    compiler.hooks.contextModuleFactory.tap(
      'Plugin'.(contextModuleFactory) = > {
        // Call this Hook before require.context parses the requested directory
        // The argument is the Context directory object to parse
        contextModuleFactory.hooks.beforeResolve.tapAsync(
          'Plugin'.(data, callback) = > {
            console.log(data, 'data'); callback(); }); }); }}module.exports = DonePlugin;
Copy the code

The Compiler.hooks object also has a contextModuleFactory, which is also a tapable derived list of hooks.

ContextModuleFactory provides a list of hooks that, as its name implies, are primarily used when parsing file directories using Webpack’s exclusive API require.context.

ContextModuleFactory hooks are not commonly used, so you can read require.context here.

And when to click here to view the individual hooks in ContextModuleFactory.

NormalModuleFactory Hook

class DonePlugin {
  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap(
      'MyPlugin'.(NormalModuleFactory) = > {
        NormalModuleFactory.hooks.beforeResolve.tap(
          'MyPlugin'.(resolveData) = > {
            console.log(resolveData, 'resolveData');
            // Just parse the directory as./ SRC /index.js and ignore other imported modules
            return resolveData.request === './src/index.js'; }); }); }}module.exports = DonePlugin;
Copy the code

The Webpack Compiler object generates various modules from the NormalModuleFactory module.

In other words, starting with the entry file, NormalModuleFactory breaks down each module request, parses the file contents to find further requests, and then crawls the entire file by breaking down all requests and parsing new files. In the final phase, each dependency becomes an instance of the module.

NormalModuleFactory Hook can be used to inject Plugin logic to control the handling of default module references in Webpack, such as before and after the introduction of ESM and CJS modules.

NormalModuleFactory Hooks can be used to inject specific logic into Webpack parsing modules in the Plugin to affect module import content when packaging. You can see the specific types of hooks here.

JavascriptParser Hook

const t = require('@babel/types');
const g = require('@babel/generator').default;
const ConstDependency = require('webpack/lib/dependencies/ConstDependency');

class DonePlugin {
  apply(compiler) {
    // enter when parsing the module
    compiler.hooks.normalModuleFactory.tap('pluginA'.(factory) = > {
      // This hook is called when the module is processed using javascript/auto
      const hook = factory.hooks.parser.for('javascript/auto');

      / / register
      hook.tap('pluginA'.(parser) = > {
        parser.hooks.statementIf.tap('pluginA'.(statementNode) = > {
          const { code } = g(t.booleanLiteral(false));
          const dep = new ConstDependency(code, statementNode.test.range);
          dep.loc = statementNode.loc;
          parser.state.current.addDependency(dep);
          returnstatementNode; }); }); }); }}module.exports = DonePlugin;
Copy the code

We mentioned above. The compiler normalModuleFactory hooks for Webpack for parsing module when the trigger, and JavascriptParser Hook was based on module resolution generated AST node injection of hooks.

Webpack parses each module using Parser, and we can register JavascriptParser hooks in Plugin to add additional logic when WebPack parses modules to generate AST nodes.

The DonePlugin above modifies the judgment expression for all statementIf nodes in the module to be false.

More information about JavascriptParser Hook can be found here.

At the end

The core mechanism of the Webpack Plugin is the publishing subscriber pattern based on tapable, which triggers different hooks at different times to influence the final packaging result.

At first glance, there are many concepts in many articles, and there are many things about the Webpack document that are not fully supplemented, but let’s go back and comb it out.

It is only the API listed in this article that is unfamiliar to you. The purpose of this article is not to give you a detailed understanding of the various development methods of Webpack Plugin in just a few thousand words, but to give you a brief understanding and concept of the Plugin mechanism and development usage.

Later in my column, I will add some plugins to show you how open source plug-in projects can be pieced together into enterprise applications that are truly engaged in the business.

Interested friends can pay attention to my column [from principle to play Webpack].