Webpack makes it more flexible through the Plugin mechanism to adapt to various application scenarios. A number of events are broadcast during the life cycle of a Webpack run, and the Plugin can listen for these events and change the output when appropriate through the API provided by Webpack.

A basic Plugin code looks like this:

class BasicPlugin{
  // Get the configuration passed in by the user to the plug-in in the constructor
  constructor(options){
  }
  
  // Webpack calls the Apply method of the BasicPlugin instance to pass in the Compiler object to the plug-in instance
  apply(compiler){
    compiler.plugin('compilation'.function(compilation) {})}}/ / export Plugin
module.exports = BasicPlugin;
Copy the code

When using this Plugin, the configuration code is as follows:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins: [new BasicPlugin(options),
  ]
}
Copy the code

After Webpack starts, new BasicPlugin(options) is executed to initialize a BasicPlugin to get an example during the configuration reading process. After initializing the Compiler object, basicPlugin.apply(Compiler) is called to pass the compiler object to the plug-in instance. Once the compiler object is acquired, the plug-in instance can listen for events broadcast by Webpack through compiler.plugin(event name, callback function). The Compiler object can also be used to manipulate Webpack.

With the simplest Plugin above, you probably get a sense of how the Plugin works, but there are a lot of details to pay attention to in actual development, which are described below.

The Compiler and Compilation

The two most commonly used objects for Plugin development are Compiler and Compilation, which bridge the gap between Plugin and Webpack. Compiler and Compilation have the following meanings:

  • The Compiler object contains all the configuration information of the Webpack environment, including options, loaders and plugins. This object is instantiated when Webpack starts. It is globally unique and can be simply understood as an instance of Webpack.
  • The Compilation object contains the current module resources, compile-generated resources, changing files, and so on. When Webpack is running in development mode, a new Compilation is created each time a file change is detected. The Compilation object also provides many event callbacks for plug-ins to extend. Compiler objects can also be read from the Compilation.

The difference between Compiler and Compilation is that Compiler represents the entire Webpack lifecycle from startup to shutdown, while Compilation simply represents a new Compilation.

Flow of events

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.

Webpack’s event flow mechanism applies the observer pattern, much like EventEmitter in Node.js. Compiler and Compilation inherit from Tapable and can broadcast and listen for events directly on Compiler and Compilation objects as follows:

/** * broadcast event * event-name is the name of the event. Do not use the same name as the existing event * params as the attached parameter */
compiler.apply('event-name',params);

/** * listen for events with the name event-name. When event-name occurs, the function will be executed. * The params argument in the function is the argument attached to the broadcast event. * /
compiler.plugin('event-name'.function(params) {});Copy the code

Similarly, compilation.apply and compilation.plugin are used in the same way as above.

When you’re developing a plug-in, you might not know where to start because you don’t know which events to listen for to get the job done.

There are also two points to keep in mind when developing plug-ins:

  • As long as the Compiler or Compilation object is available, new events can be broadcast, so a newly developed plug-in can broadcast events for other plug-ins to listen on.
  • The Compiler and Compilation object passed to each plug-in is the same reference. This means that changing the properties of the Compiler or Compilation object in one plug-in can affect subsequent plug-ins.
  • Some events are asynchronous, and these asynchronous events take two parameters. The second parameter is a callback function that needs to be called to notify Webpack when the plug-in has finished processing the task before moving on to the next process. Such as:
    compiler.plugin('emit'.function(compilation, callback) {
      // Support processing logic
    
      // Execute callback to notify Webpack
      // If callback is not executed, the running process will remain stuck at this point
      callback();
    });
    Copy the code

Commonly used API

Plug-ins can be used to modify output files, add output files, even improve Webpack performance, and so on, but plug-ins can do a lot of things by calling the APIS provided by Webpack. Because Webpack provides a large number of apis, there are many that are rarely used, and space is limited, so here are some commonly used apis.

Read output resources, code blocks, modules and their dependencies

Some plug-ins may need to read the results of Webpack processing, such as output resources, code blocks, modules and their dependencies, for further processing.

At the time of the EMIT event, the conversion and assembly of the source file has been completed and the final output resources, code blocks, modules and their dependencies can be read, and the contents of the output resources can be modified. The plug-in code is as follows:

class Plugin {
  apply(compiler) {
    compiler.plugin('emit'.function (compilation, callback) {
      // compilation.chunks is an array that stores all code blocks
      compilation.chunks.forEach(function (chunk) {
        // chunk represents a code block
        // A code block consists of several modules, each of which can be read by chunk.forEachModule
        chunk.forEachModule(function (module) {
          // module represents a module
          // module.fileDependencies An array of dependencies for the current module
          module.fileDependencies.forEach(function (filepath) {}); });// Webpack will generate output file resources according to Chunk, each Chunk corresponds to one or more output files
        // For example, if the Chunk contains CSS modules and uses ExtractTextPlugin,
        // The Chunk generates.js and.css files
        chunk.files.forEach(function (filename) {
          // compilation. Assets store all the current output resources
          // Call the source() method of an output resource to get the contents of the output resource
          let source = compilation.assets[filename].source();
        });
      });

      // This is an asynchronous event, remember to call Webpack callback to notify Webpack that the event listening process is finished.
      // If you forget to call callback, Webpack will stay stuck and never execute later.callback(); }}})Copy the code

Listening for file changes

As described in Use Auto-refresh 4-5, Webpack starts from the configured entry module and finds all dependencies in turn. When the entry module or its dependencies change, a new Compilation is triggered.

When developing plug-ins, you often need to know which file changes caused the new Compilation, using code like this:

// The watch-run event is triggered when the dependent file changes
compiler.plugin('watch-run', (watching, callback) => {
	// Get the list of files that have changed
	const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
	// changedFiles format is key-value pair, the key is the changed file path.
	if(changedFiles[filePath] ! = =undefined) {
	  // The file corresponding to filePath has changed
	}
	callback();
});
Copy the code

By default, Webpack only monitors whether the entry and its dependent modules have changed, and in some cases the project may need to introduce new files, such as an HTML file. Because JavaScript files do not import HTML files, Webpack does not listen for changes to the HTML files and does not trigger a new Compilation when the HTML files are edited. To listen for changes to the HTML file, we need to add the HTML file to the dependency list, using the following code:

compiler.plugin('after-compile', (compilation, callback) => {
  // Add the HTML file to the file dependency list so that Webpack can listen for the HTML module file and restart the compilation if the HTML template file changes
	compilation.fileDependencies.push(filePath);
	callback();
});
Copy the code

Modifying output Resources

In some cases, the plugin needs to modify, add, or delete the output resources. To do this, you need to listen for the EMIT event. When the emit event occurs, all module transformations and code block corresponding files have been generated, and the resources that need to be exported are about to be exported. The EMIT event is therefore the last chance to modify the Webpack output resource.

Compilation. assets is a key-value pair. The key is the name of the file to be exported, and the value is the corresponding content of the file.

Set up the compilation.assets code as follows:

compiler.plugin('emit', (compilation, callback) => {
  // Set the output resource named fileName
  compilation.assets[fileName] = {
    // Return the file contents
    source: (a)= > {
      // fileContent can be either a string representing a text file or a Buffer representing a binary file
      return fileContent;
  	},
    // Returns the file size
  	size: (a)= > {
      return Buffer.byteLength(fileContent, 'utf8'); }}; callback(); });Copy the code

Read the compilation.assets code as follows:

compiler.plugin('emit', (compilation, callback) => {
  // Read the output resource named fileName
  const asset = compilation.assets[fileName];
  // Get the contents of the output resource
  asset.source();
  // Get the file size of the output resource
  asset.size();
  callback();
});
Copy the code

Determine which plug-ins Webpack uses

When developing a plug-in, you may need to make the next decision based on whether the current configuration uses another plug-in, so you need to read Webpack’s current plug-in configuration. To determine whether the ExtractTextPlugin is currently used as an example, you can use the following code:

// The ExtractTextPlugin is used in the current configuration.
// Compiler parameters are those passed in by Webpack in Apply
function hasExtractTextPlugin(compiler) {
  // List of all plug-ins used in the current configuration
  const plugins = compiler.options.plugins;
  // Go to plugins to find instances of the ExtractTextPlugin
  return plugins.find(plugin= >plugin.__proto__.constructor === ExtractTextPlugin) ! =null;
}
Copy the code

In actual combat

Let’s take you through a step-by-step implementation of a plug-in with a practical example.

The name of this plugin is EndWebpackPlugin, and it is used to add some additional actions when Webpack is about to exit, such as publishing the output file to the server after Webpack has successfully compiled and exported the file. The plug-in can also distinguish between successful Webpack builds. Use the plug-in as follows:

module.exports = {
  plugins: [// The EndWebpackPlugin is initialized with two parameters: a callback function on success and a callback function on failure;
    new EndWebpackPlugin((a)= > {
      // The Webpack is successfully built and the file is exported. This is where you can publish the file
    }, (err) => {
      // The Webpack build failed. Err is the cause of the error
      console.error(err); }})]Copy the code

To implement the plug-in, you need two events:

  • Done: Occurs when Webpack is about to exit after a successful build and the files have been exported;
  • Failed: Occurs when the build fails due to an exception and Webpack is about to exit.

Implementing the plug-in is very simple and the complete code is as follows:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // Save the callback function passed in the constructor
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

  apply(compiler) {
    compiler.plugin('done', (stats) => {
        // Call doneCallback in the done event
        this.doneCallback(stats);
    });
    compiler.plugin('failed', (err) => {
        // Callback failCallback in failed event
        this.failCallback(err); }); }}// Export the plug-in
module.exports = EndWebpackPlugin;
Copy the code

As you can see from the development of this plug-in, finding the right event point to complete the functionality is very important when developing plug-ins. Webpack broadcasts common events as it runs, as detailed in the 5-1 Overview of how it works, so you can find the ones you need.

This example provides the complete code for the project

Easy to Understand Webpack online reading link

Read the original