Write webpack loader and plug-in

Time:2021-7-27

Introduction to webpack

Tabable (super housekeeper of webpack controlling event flow)

The core function of tapable is to execute the registered events in sequence when triggered according to different hooks.
Write webpack loader and plug-in
BailWaterfallLoopAnd other keywords, specifying the registered event callbackhandlerSequence of triggering.

  • Basic hook: execute in sequence according to the event registration sequencehandlerhandlerNo interference between;
  • Bail hook: execute in sequence according to the event registration sequencehandler, if any of themhandlerReturn value is notundefined, then the remaininghandlerWill not be implemented;
  • Waterfall hook: execute in sequence according to the event registration sequencehandler, previoushandlerThe return value of will be the nexthandlerInput parameters of;
  • Loop hook: execute in sequence according to the event registration sequencehandler, if anyhandlerThe return value of is notundefined, the event chain againFrom the beginningStart execution until allhandlerAll returnundefined

Basic concepts

  • Entry: entry, the first step of webpack execution construction will start with entry, which can be abstracted into input.
  • Module: module. In webpack, everything is a module. A module corresponds to a file. Webpack will recursively find all dependent modules from the configured entry.
  • ChunkCode block: a chunk is composed of multiple modules, which is used for code merging and segmentation.
  • Loader: module converter, which is used to convert the original content of the module into new content as required.
  • Plugin: extension, which broadcasts corresponding events at specific opportunities in the webpack construction process. The plug-in can listen to the occurrence of these events and do corresponding things at specific times

Workflow

The running process of webpack is a serial process. The following processes will be executed from start to end:

  1. Initialization parameters: read and merge parameters from configuration files and shell statements to get the final parameters;
  2. Start compilation: initialize the compiler object with the parameters obtained in the previous step, load all configured plug-ins, and execute the run method of the object to start compilation;
  3. Determine entry: find all entry files according to the entry in the configuration;
  4. Compiling module: starting from the entry file, call all configured loaders to translate the module, find out the module that the module depends on, and then recurse this step until all the entry dependent files have been processed in this step;
  5. Complete module compilation: after translating all modules with loader in step 4, the final translated content of each module and the dependencies between them are obtained;
  6. Output resources: according to the dependency between entries and modules, assemble chunks containing multiple modules, and then convert each chunk into a separate file to be added to the output list. This step is the last chance to modify the output content;
  7. Output completion: after determining the output content, determine the output path and file name according to the configuration, and write the file content to the file system.

In the above process, webpack will broadcast specific events at specific time points. After listening to the events of interest, the plug-in will execute specific logic, and the plug-in can call the API provided by webpack to change the running results of webpack.
Write webpack loader and plug-in

Introduction to compiler and compilation

The difference between compiler and compilation is that compiler represents the entire webpack life cycle from Startup to shutdown, while compilation only represents a new compilation.

compiler

The compiler module of webpack is its core. It contains all the options passed by the webpack configuration file, including information such as loader and plugins.

We can look at some of the core methods defined in the compiler class.

//It inherits from the tapable class, enabling it to publish and subscribe
class Compiler extends Tapable {
  //Constructor, the actual incoming value of context is process. CWD (), representing the current working directory
  constructor(context) {
    super();
    //A series of event hooks are defined, which are triggered at different times
    this.hooks = {
      shouldEmit: new SyncBailHook(["compilation"]),
      done: new AsyncSeriesHook(["stats"]),
      //... more hooks
    };
    this.running = true;
    //Some other variable declarations
  }

  //After calling this method, it will listen for file changes. Once the changes are made, the compilation will be performed again
  watch(watchOptions, handler) {
    this.running = true;
    return new Watching(this, watchOptions, handler)
  }
  
  //Used to trigger all work at compile time
  run(callback) {
    //After compilation, some code is omitted
    const onCompiled = (err, compilation) => {
      this.emitAssets(compilation, err => {...})
    }
  }

  //Be responsible for writing the compiled output file to the local
  emitAssets(compilation, callback) {}

  //Create a compilation object and pass the compiler itself as a parameter
  createCompilation() {
    return new Compilation(this);
  }

  //Trigger compilation, create a compilation instance internally and perform corresponding operations
  compile() {}


  //Many of the above core methods will trigger the specified event through this.hooks.somehooks.call
  
}

It can be seen that the compiler sets a series of event hooks and various configuration parameters, and defines a series of core methods of webpack, such as starting compilation, observing file changes, writing compilation result files locally and so on. In the corresponding work performed by the plugin, we will certainly need to get all kinds of webpack information through the compiler.

compilation

If compiler is regarded as the main console, compilation focuses on compiling.

After the watch mode is enabled, the webpack will monitor whether the file changes. Whenever a file change is detected, a new compilation will be executed, and a new compilation resource and a new compilation object will be generated at the same time.
The compilation object contains module resources, compilation generated resources, changed files and tracked dependent state information for plug-in work. If we need to complete a custom compilation process in the plug-in, this object is bound to be used.

Write webpack loader and plug-in

Write loader

Responsibilities: the responsibilities of a loader are single, and only one transformation needs to be completed.

initialization

module.exports = function(source) {  
    //Source is the original content of a file passed by the compiler to the loader  
    //After performing some operations on the source, return to the next loader
    return source;
};
  • Get loader’soptions

    const loaderUtils = require('loaderutils');
    module.exports = function(source) {  
        //Get the options passed in by the user to the current loader 
        const options = loaderUtils.getOptions(this);
        //Perform different operations according to different options
        return source;
    };

Return other results

For examplebabel-loaderTaking the conversion of ES6 code as an example, it also needs to output the source map corresponding to the converted Es5 code to facilitate the debugging of the source code. In order to return the source map to webpack along with the Es5 code

module.exports = function(source) { 
    this.callback(null, source, sourceMaps); 
    //Tell webpack the returned result through this.callback
    //When using this.callback to return content, the loader must return undefined to let webpack know that the result returned by the loader is in this.callback, not return   
    return;
};

Among themthis.callbackIt is an API injected by webpack into loader to facilitate communication between loader and webpack.this.callbackThe detailed usage of the is as follows:

this.callback(    
    //When the original content cannot be converted, an error is returned to webpack   
    err: Error | null,    
    //Content after original content conversion    
    content: string | Buffer,    
    //It is used to get the source map of the original content from the converted content, which is convenient for debugging
    sourceMap?: SourceMap,    
    //If the ast syntax tree is generated from the conversion to the original content, the ast can be returned to facilitate the reuse of the ast by the loader of the ast, so as to avoid repeated generation of AST and improve performance    
    abstractSyntaxTree?: AST
);

Synchronous and asynchronous

However, in some scenarios, the conversion steps can only be completed asynchronously. For example, you need to obtain the results through network requests. If you use synchronous methods, the network requests will block the whole construction, resulting in very slow construction.

module.exports = function(source) {    
    //Tell webpack that this conversion is asynchronous, and the loader will call back the result in the callback    
    var callback = this.async();    
    someAsyncOperation(
    source, 
    function(err, result, sourceMaps, ast) {  
    //The result after asynchronous execution is returned through callback
    callback(err, result, sourceMaps, ast);   
    });
};

Processing binary data

By default, the original content passed from webpack to loader is a string encoded in UTF-8 format. However, in some scenarios, the loader does not process text files, but binary files, such asfile-loader, you need webpack to pass data in binary format to the loader.

module.exports = function(source) {    
    //When exports. Raw = = = true, the source passed by webpack to loader is of buffer type    
    source instanceof Buffer === true;    
    //The type returned by loader can also be buffer type    
    //At exports.raw== When true, loader can also return buffer type results    
    return source;
    };
    //Tell webpack whether the loader needs binary data through the exports.raw attribute 
    module.exports.raw = true;

Other loader APIs(Loader API address)

  • this.context: the directory of the file currently processed. If the file currently processed by the loader is/src/main.js, thenthis.contextIs equal to/src
  • this.resource: the full request path of the currently processed file, including:querystringFor example/src/main.js?name=1
  • this.resourcePath: the path to the currently processed file, for example:/src/main.js
  • this.resourceQuery: of the file currently being processedquerystring
  • this.target: equals the target in the webpack configuration.
  • this.loadModule: however, when the loader processes a file, if it depends on the processing results of other files to obtain the results of the current file, it canthis.loadModule(request:string,callback:function(err,source,sourceMap,module))To getrequestProcessing results of corresponding files.
  • this.resolve: likerequireStatement to obtain the full path of the specified file. The method isresolve(context:string,request:string,callback:function(err,result:string))
  • this.addDependency: add the dependent file to the current processing file so that when the dependent file changes, the loader will be called again to process the file. The method of use isaddDependency(file:string)
  • this.addContextDependency: andaddDependencySimilar, butaddContextDependencyIs to add the entire directory to the dependency of the file currently being processed. The method of use isaddContextDependency(directory:string)
  • this.clearDependencies: clear all dependencies of the file currently being processed byclearDependencies()
  • this.emitFile: output a file usingemitFile(name:string,content:Buffer|string,sourceMap:{...})

Writing plug-ins

Webpack plug-in composition

Before customizing the plug-in, we need to know what constitutes a webpack plug-in. The following excerpt is from the document:

  • A named JavaScript function;
  • Defined on its prototype   apply   method;
  • Specify a that touches the webpack itselfevent hook
  • Operate instance specific data inside webpack;
  • After the function is implemented, the callback provided by Webpack is called.

Basic architecture of webpack plug-in

The plug-in is instantiated by a constructor. Constructor definitionapplyMethod, when installing the plug-in,applyMethod will be used by webpackcompilerCall once.applyMethod can receive a webpackcompilerObject so that it can be accessed in the callback functioncompilerObject.

The official document provides a simple plug-in structure:

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Hello World Plugin', (
      Stats / * when the hook is touched, stats will be passed in as a parameter*/
    ) => {
      console.log('Hello World!');
    });
  }
}
module.exports = HelloWorldPlugin;

Using plug-ins:

// webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
  //... here are other configurations
  plugins: [new HelloWorldPlugin({ options: true })]
};

Plug in trigger timing

There are many hooks provided by webpack. For complete details, please refer to the documentation《Compiler Hooks

  • entryOption : In the webpack optionentryAfter the configuration item is processed, execute the plug-in.
  • afterPlugins :  After setting the initial plug-in, execute the plug-in.
  • compilation :  After compilation and creation, execute the plug-in before generating the file..
  • emit :  Generate resources tooutputBefore directory.
  • done :  Compilation is complete.

staycompiler.hooks  Next assignmentEvent hook function, the callback function is executed when the hook is triggered.
Webpack provides three methods to trigger hooks:

  • tap: toSynchronization modeTrigger hook;
  • tapAsync: toAsynchronous modeTrigger hook;
  • tapPromise: toAsynchronous modeTrigger the hook and return promise;

Common API(All APIs)

Plug ins can be used to modify output files, add output files, and even improve webpack performance. In short, plug-ins can accomplish many things by calling the API provided by webpack.

Read output resources, code blocks, modules and their dependencies

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

stayemitWhen the event occurs, it means that the conversion and assembly of the source file have been completed. Here, you can read the resources, code blocks, modules and their dependencies to be output, and modify the contents of the output resources.

class MyPlugin {
  apply(compiler) {
    compiler.plugin('emit', function(compilation, callback) {
      //Compilation.chunks is an array that stores all code blocks. We need to traverse it
      compilation.chunks.forEach(function(chunk) {
        /*
         *Chunk represents a code block, which is composed of multiple modules.
         *We can read each module that makes up the code block through chunk. Foreachmodule
        */
        chunk.forEachModule(function(module) {
          //Module represents a module.
          //Module.filedependencies stores all dependent file paths of the current module. It is an array
          module.fileDependencies.forEach(function(filepath) {
            console.log(filepath);
          });
        });
        /*
         Webpack will generate output file resources according to chunks, and each chunk corresponds to one or more output files.
        */
        chunk.files.forEach(function(filename) {
          //Compilation.assets is used to store all current resources to be output.
          //Calling the source () method of an output resource can get the content of the output resource
          const source = compilation.assets[filename].source();
        });
      });
      /*
       This event is asynchronous, so you need to call callback to notify the end of this weback event listening.
       If we don't call callback(); Then the webpack will always be stuck here and will not be executed later.
      */
      callback();
    })
  }
}

Modify output resource

In some scenarios, plug-ins need to modify, add and delete output resources. To do this, you need to listenemitEvent because it happenedemitThe files corresponding to the conversion of all modules and code blocks have been generated during the event, and the resources to be output will be output soon, soemitEvent is the last time to modify the webpack output resource.

All resources that need to be output will be stored in thecompilation.assetsIn,compilation.assetsIs a key value pair. The key is the name of the file to be output, and the value is the corresponding content of the file.

set upcompilation.assetsThe code of is as follows:

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

readcompilation.assetsThe code of is as follows:

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

Determine which plug-ins webpack uses

When developing a plug-in, you may need to make the next decision according to whether the current configuration uses another plug-in. Therefore, you need to read the current plug-in configuration of webpack. Take the determination of whether extracttextplugin is currently used as an example, you can use the following code:

//Judge that the current configuration uses extracttextplugin, and the compiler parameter is the parameter passed in by webpack in apply (compiler)
function hasExtractTextPlugin(compiler) {  
//List of all plug-ins currently configured  
const plugins = compiler.options.plugins;  
//Go to plugins to find out if there is an instance of extracttextplugin  
return plugins.find(plugin=>plugin.\_\_proto\_\_.constructor === ExtractTextPlugin) != null;}

Listening for file changes

Webpack will start from the configured entry module and find out all dependent modules in turn. When the entry module or its dependent module changes, a new compilation will be triggered.

When developing plug-ins, you often need to know which file has changed, resulting in a new compilation

By default, webpack will only monitor whether the entry and its dependent modules have changed. In some cases, the project may need to introduce a new file, such as an HTML file. Because JavaScript files do not import HTML files, webpack will not listen to the changes of HTML files, and new compilation will not be triggered when editing HTML files. In order to monitor the changes of HTML files, we need to add HTML files to the dependency list. For this purpose, we can use the following code:

//When the dependent file changes, the watch run event will be triggered
class MyPlugin {
  apply(compiler) {
    compiler.plugin('watch-run', (watching, callback) => {
      //Gets the list of files that have been transformed
      const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
      //The changed files format is in the form of key value pairs. When the key is the changed file path
      if (changedFiles[filePath] !== undefined) {
        //The corresponding file has changed
      }
      callback();
    });

    /*
     By default, webpack will only listen to whether the entry file or its dependent module has changed, but in some cases, such as when the HTML file has changed, webpack
     Will listen for changes in HTML files. Therefore, the new compilation will not be triggered again. Therefore, in order to monitor the changes of HTML files, we need to add HTML files to
     In the dependency list. Therefore, we need to add the following code:
    */
    compiler.plugin('after-compile', (compilation, callback) => {
      /*
       The following parameter filepath is the path of the HTML file. We add the HTML file to the file dependency table, and then our webpack will listen to the HTML module file,
       When the HTML template file changes, it will restart and recompile a new compilation
      */
      compilation.fileDependencies.push(filePath);
      callback();
    })
  }
}

Write at the end

Reference articles

(https://cloud.tencent.com/dev…

Recommended reading