plugin
Loader finished, let’s talk about plugin. In fact, loader is only the basic tool of Webpack, in Webpack is really powerful plugin.
Introduction to the
Plugin is much more powerful than loader and permeates Webpack. Almost anything a Loader can’t do can be done with a plugin. Everything in Webpack is a plugin, 80% of the code in Webpack is a plugin.
Plugins are a key piece of the webpack ecosystem and provide the community with a powerful way to tap into webpack’s compilation process. A plugin is able to hook into key events that are fired throughout each compilation. Every step of the way, the plugin will have full access to the
compiler
and, when applicable, the currentcompilation
.
While loaders are used to transform certain types of modules, plugins can be leveraged to perform a wider range of tasks like bundle optimization, asset management and injection of environment variables.
Plugins are the backbone of webpack. webpack itself is built on the same plugin system that you use in your webpack configuration!
They also serve the purpose of doing anything else that a loader cannot do.
Plugins grant unlimited opportunity to perform customizations within the webpack build system. This allows you to create custom asset types, perform unique build modifications, or even enhance the webpack runtime while using middleware. The following are some features of webpack that become useful while writing plugins.
It’s interesting to know that 80% of the webpack is made up of its own plugin system. Webpack itself is an event-driven architecture. Plugins are a key piece of the webpack ecosystem and provide the community with a powerful way to tap into Webpack’s compilation process. A plugin is able to hook into key events that are fired throughout each compilation webpack-behind-the-scenes
Anyway, with plugin you have the whole Webpack.
The plugin’s writing
Let’s try to write a WebPack plugin.
A webpack plugin is a JavaScript object that has an
apply
method. Thisapply
method is called by the webpack compiler, giving access to the entire compilation lifecycle.
The above paragraph gives a good overview of plugin writing and functionality.
A plugin is an object that has a method called Apply, whose parameter is Compiler. These methods are mounted via hooks of the Compiler, which have compilation objects. Then there are hooks on the Compilation object that can mount various methods. To get all the relevant information about the entire Webpack and packaging.
Take a look at the simplest plugin:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; class ConsoleLogOnBuildWebpackPlugin { apply(compiler) { compiler.hooks.run.tap(pluginName, compilation => { console.log('The webpack build process is starting!!! '); }); } } module.exports = ConsoleLogOnBuildWebpackPlugin;Copy the code
There are several concepts involved: Compiler, hooks, run, tap, compilation. Let’s look at compiler and compilation one by one.
Compiler is equivalent to the instance of Webpack, and is also the top-level or core content of the whole system, which controls the compilation process of the whole Webpack.
It is the top level, it’s central dispatch.
— compiler-compilation
Compiler is responsible for listening to files and starting compilation. The Compiler instance contains the complete Webpack configuration, and there is only one Compiler instance globally. — Webpack in a nutshell
If you don’t pass the
webpack
runner function a callback, it will return a webpackCompiler
instance. This instance can be used to manually trigger the webpack runner or have it build and watch for changes, much like the CLI. TheCompiler
instance provides the following methods:
.run(callback)
.watch(watchOptions, handler)
Typically, only one master Compiler instance is created, although child compilers can be created in order to delegate specific tasks. The Compiler is ultimately just a function which performs bare minimum functionality to keep a lifecycle running. It delegates all the loading, bundling, and writing work to registered plugins. The hooks property on a Compiler instance is used to register a plugin to any hook event in the Compiler’s lifecycle. The WebpackOptionsDefaulter and WebpackOptionsApply utilities are used by webpack to configure its Compiler instance with all the built-in plugins. The run method is then used to kickstart all compilation work. Upon completion, the given callback function is executed. The final logging of stats and errors should be done in this callback function. — webpack.js.org/api/node/#c…
Let’s look at where plugin is called in the Webpack.
// node_modules/webpack/lib/webpack.js 46 if (options.plugins && Array.isArray(options.plugins)) { 47 for (const plugin of options.plugins) { 48 if (typeof plugin === "function") { 49 plugin.call(compiler, compiler); 50 } else { 51 plugin.apply(compiler); 52} 53} 54}Copy the code
It is called almost immediately after the config is initialized, because plugins are registered with the entire process, so it is necessary to register these processes before compilation so that each plugin can be applied during compilation.
Now that it is registered, there is a time to fire, and the time to fire is all the events for Compiler. hooks, which are all the hooks exposed during compilation. Run is actually one of those hooks, which is when you start compiling. While emit is the final generated file, all hooks can be found in the official website, including the parameters passed in the hook callback function. That in fact the official website document is just a translation of the source code, we look at the official website document to see the source code will find that all the content in the source code has been shown.
// node_modules/webpack/lib/Compiler.js 45 this.hooks = { 46 /** @type {SyncBailHook<Compilation>} */ 47 shouldEmit: new SyncBailHook(["compilation"]), 48 /** @type {AsyncSeriesHook<Stats>} */ 49 done: new AsyncSeriesHook(["stats"]), 50 /** @type {AsyncSeriesHook<>} */ 51 additionalPass: new AsyncSeriesHook([]), 52 /** @type {AsyncSeriesHook<Compiler>} */ 53 beforeRun: new AsyncSeriesHook(["compiler"]), 54 /** @type {AsyncSeriesHook<Compiler>} */ 55 run: new AsyncSeriesHook(["compiler"]), 56 /** @type {AsyncSeriesHook<Compilation>} */ 57 emit: new AsyncSeriesHook(["compilation"]), 58 /** @type {AsyncSeriesHook<string, Buffer>} */ 59 assetEmitted: new AsyncSeriesHook(["file", "content"]), 60 /** @type {AsyncSeriesHook<Compilation>} */ 61 afterEmit: new AsyncSeriesHook(["compilation"]), 62 63 /** @type {SyncHook<Compilation, CompilationParams>} */ 64 thisCompilation: new SyncHook(["compilation", "params"]), 65 /** @type {SyncHook<Compilation, CompilationParams>} */ 66 compilation: new SyncHook(["compilation", "params"]), 67 /** @type {SyncHook<NormalModuleFactory>} */ 68 normalModuleFactory: new SyncHook(["normalModuleFactory"]), 69 /** @type {SyncHook<ContextModuleFactory>} */ 70 contextModuleFactory: new SyncHook(["contextModulefactory"]), 71 72 /** @type {AsyncSeriesHook<CompilationParams>} */ 73 beforeCompile: new AsyncSeriesHook(["params"]), 74 /** @type {SyncHook<CompilationParams>} */ 75 compile: new SyncHook(["params"]), 76 /** @type {AsyncParallelHook<Compilation>} */ 77 make: new AsyncParallelHook(["compilation"]), 78 /** @type {AsyncSeriesHook<Compilation>} */ 79 afterCompile: new AsyncSeriesHook(["compilation"]), 80 81 /** @type {AsyncSeriesHook<Compiler>} */ 82 watchRun: new AsyncSeriesHook(["compiler"]), 83 /** @type {SyncHook<Error>} */ 84 failed: new SyncHook(["error"]), 85 /** @type {SyncHook<string, string>} */ 86 invalid: new SyncHook(["filename", "changeTime"]), 87 /** @type {SyncHook} */ 88 watchClose: new SyncHook([]), 89 90 /** @type {SyncBailHook<string, string, any[]>} */ 91 infrastructureLog: new SyncBailHook(["origin", "type", "args"]), 92 93 // TODO the following hooks are weirdly located here 94 // TODO move them for webpack 5 95 /** @type {SyncHook} */ 96 environment: new SyncHook([]), 97 /** @type {SyncHook} */ 98 afterEnvironment: new SyncHook([]), 99 /** @type {SyncHook<Compiler>} */ 100 afterPlugins: new SyncHook(["compiler"]), 101 /** @type {SyncHook<Compiler>} */ 102 afterResolvers: new SyncHook(["compiler"]), 103 /** @type {SyncBailHook<string, Entry>} */ 104 entryOption: new SyncBailHook(["context", "entry"]) 105 };Copy the code
Let’s take a look at a few common hooks and when they trigger.
**run
**AsyncSeriesHook
Hook into the compiler before it begins readingrecords
.
Callback Parameters:compiler
**compilation
**SyncHook
Runs a plugin after a compilation has been created.
Callback Parameters:compilation
,compilationParams
**shouldEmit
**SyncBailHook
Called before emitting assets. Should return a boolean telling whether to emit.
Callback Parameters:compilation
emi****t
AsyncSeriesHook
Executed right before emitting assets to output dir.
Callback Parameters:compilation
We’ve seen these hooks, but we can look at when these hooks actually execute. Every hooks execution is when the hook calls call. For example, let’s look at when run is actually executed, beforeRun and compile beforeRun.
/ / node_modules/webpack/lib/Compiler. Js / / line 264-310 many hooks onCompiled code can also see the implementation of the timing and order 312 Enclosing hooks. BeforeRun. CallAsync (this, err = > {/ / beforeRun calls the timing of the if (err) 313 return finalCallback (err); CallAsync (this, err => {// beforeRun run 316 if (err) return finalCallback(err); 317 318 this.readRecords(err => { 319 if (err) return finalCallback(err); 320 321 this.compile(onCompiled); / / 322}); 323}); 324}); . 660 compile(callback) { 661 const params = this.newCompilationParams(); 662 this.hooks.beforeCompile.callAsync(params, err => { // beforeCompile 663 if (err) return callback(err); 664 665 this.hooks.compile.call(params); // compile 666 667 const compilation = this.newCompilation(params); 668 669 this.hooks.make.callAsync(compilation, err => { // make 670 if (err) return callback(err); 671 672 compilation.finish(err => { 673 if (err) return callback(err); 674 675 compilation.seal(err => { 676 if (err) return callback(err); 677 678 this.hooks.afterCompile.callAsync(compilation, err => { // afterCompile 679 if (err) return callback(err); 680 681 return callback(null, compilation); 682}); 683}); 684}); 685}); 686}); 687}Copy the code
The more hooks, the finer the control degree of the whole process is. For example, including emit events, there are shouldEmit, emit, afterEmit, and assetemrepeatable events before and after it. For EMIT, the author has provided many opportunities for third party developers to get involved in the WebPack compilation process. This level of refinement is high, which is why the Plugin is really what makes up the core of WebPack. The Compiler for Webpack and compilation have over 100 hooks, which means that you have more than 100 opportunities to be involved in the compilation process.
The following article is also an official document
Webpack.js.org/contribute/…
Hooks on the Compiler object look at this document
The official documentation basically shows the hooks on all the Compiler
Let’s go on to Compilation.
When Webpack is running in development mode, a new Compilation is created whenever a file change is detected. A Compilation object contains the current module resources, compile-build resources, changing files, and so on. The Compilation object also provides many event callbacks for plug-ins to extend. — Webpack in a nutshell
Documentation is also provided regarding methods and properties mounted on the Compilation object.
The compilation object contains the following hooks:
250 this.hooks = { 251 /** @type {SyncHook<Module>} */ 252 buildModule: new SyncHook(["module"]), 253 /** @type {SyncHook<Module>} */ 254 rebuildModule: new SyncHook(["module"]), ... . 430 /** @type {SyncBailHook<Chunk[]>} */ 431 optimizeExtractedChunks: new SyncBailHook(["chunks"]), 432 /** @type {SyncBailHook<Chunk[]>} */ 433 optimizeExtractedChunksAdvanced: new SyncBailHook(["chunks"]), 434 /** @type {SyncHook<Chunk[]>} */ 435 afterOptimizeExtractedChunks: new SyncHook(["chunks"]) 436 };Copy the code
Compiler/compilation/hooks/run let’s look at tap, which is similar to EventEmitter’s on method in NodeJS. Tap is triggered by the Call async method, similar to the emit method in EventEmitter.
So what is tap? How did it come about? We have to talk about Tapable. Let’s talk about tapbale.
Tapable is explained in detail in the documentation of the Writing-a-plugin. But I’m going to separate tapable out. Write in detail about the use and meaning of tapable.
Webpack.js.org/contribute/…
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// Explore each chunk (build output):
compilation.chunks.forEach(chunk => {
// Explore each module within the chunk (built inputs):
chunk.getModules().forEach(module => {
// Explore each source file path that was included into the module:
module.buildInfo && module.buildInfo.fileDependencies && module.buildInfo.fileDependencies.forEach(filepath => {
// we've learned a lot about the source structure now...
});
});
// Explore each asset filename generated by the chunk:
chunk.files.forEach(filename => {
// Get the asset source for each file generated by the chunk:
var source = compilation.assets[filename].source();
});
});
callback();
});
}
}
module.exports = MyPlugin;
Copy the code
In addition to compiler and compilation, webPack’s Parser is also an extension to Tapable, which has hooks. Detailed in the document or source (node_modules/webpack/lib/Parser. Js).
Analyze several official plugins and try to write one yourself.
One of the official plugins: html-webpack-plugin
Here is the code that generates the HTML
279 // Add the evaluated html code to the webpack assets
280 compilation.assets[finalOutputName] = {
281 source: () => html,
282 size: () => html.length
283 };
Copy the code
Webpack default ProgressPlugin, see the compiler. The hooks, emit, intercept, the intercept is a special kind of method tapable.
Node_modules/webpack/lib/optimize/RemoveEmptyChunksPlugin js this simple
Tapable
This small library is a core utility in webpack but can also be used elsewhere to provide a similar plugin interface. Many objects in webpack extend the
Tapable
class. The class exposestap
,tapAsync
, andtapPromise
methods which plugins can use to inject custom build steps that will be fired throughout a compilation.
In other words, Tapable was not created for Webpack, but for plugin. Tapable is more of a process mechanism than a compilation mechanism. It’s actually important to understand that I originally wanted to see Tapable and always wanted to learn about tapable from a compile point of view, so I couldn’t watch it at all because all the code had nothing to do with compilation. Your mental modal is not associated with tapable at all, so of course you can’t watch it. At the heart of tapable are exposed hooks that fire different types of events in the process using different types of hooks.
Core of Tapable: hooks
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
Copy the code
Which are the hooks of tapable. SyncHook and asyncHook. From the perspective of stream processing mechanism, there are hook, bailHook, waterfallHook, loopHook, parallelHook, seriesHook. Hooks of different types have different processing logic, but basically cover all process processing mechanisms.
The different Hooks are described in the Webpack documentation.
Webpack.js.org/contribute/…
-
SyncHook’s normal synchronous serial process processing mechanism processes the same content in sequence according to the order of plugin access.
-
bailHook In these type of hooks, each of the plugin callbacks will be invoked one after the other with the specific args. If any value is returned except undefined by any plugin, then that value is returned by hook and no further plugin callback is invoked. Many useful events like optimizeChunks, optimizeChunkModules are SyncBailHooks. The synchronous serial circuit breaker process the same content in the sequence of plugins. When any plugin returns a value, it returns immediately, and the subsequent plugin does not execute again.
-
waterfallHook Here each of the plugins are called one after the other with the arguments from the return value of the previous plugin. The plugin must take the order of its execution into account. It must accept arguments from the previous plugin that was executed. The value for the first plugin is init. Hence at least 1 param must be supplied for waterfall hooks. This pattern is used in the Tapable instances which are related to the webpack templates like ModuleTemplate, ChunkTemplate etc. The synchronous pipelining mechanism processes content in sequence according to the order of plugins. The return value of the previous plugin is the input value of the next plugin, so the order of plugins should be considered.
-
asyncSeriesHook The plugin handler functions are called with all arguments and a callback function with the signature (err? : Error) -> void. The handler functions are called in order of registration. callback is called after all the handlers are called. This is also a commonly used pattern for events like emit, run. An asynchronous serial mechanism, in which all plugins receive the same parameters in sequence.
-
asyncWaterfallHook The plugin handler functions are called with the current value and a callback function with the signature (err: Error, nextValue: any) -> void. When called nextValue is the current value for the next handler. The current value for the first handler is init. After all handlers are applied, callback is called with the last value. If any handler passes a value for err, the callback is called with this error and no more handlers are called. This plugin pattern is expected for events like before-resolve and after-resolve. Asynchronous pipelining, in which the return value of the last plugin is the input value of the next plugin, executes in sequence.
The following article explains all the hook type principles and processes very well.
www.programmersought.com/article/145…
Draw a few diagrams to illustrate all the hook types.
Each hook: about tapable webpack.js.org/contribute/…
After reading these hooks in Tapable I am wondering: what the hell does this have to do with build and package? Yes, Tapable has nothing to do with compilation. Tapable’s appearance in Webpack is entirely for plugin service, which is to facilitate the addition of various plugins in the packaging process and facilitate the integration of various plugins into the whole packaging process.
Therefore, Tapable is the backbone of Webpack, but not the core. It is only the core of architecture, not the core of packaging. The core of packaging is to analyze the dependency relationships of each module, form a dependency graph (network between modules), and then package all files into one or more chunks/bundles. Then each module can call each other normally during execution, which is the core work of WebPack.
So let’s look at WebPack from two more perspectives. One is from a tapable perspective, looking back at how our plugin works and how it fits into webpack. The second is completely from the perspective of packaging and compilation. Since Tapable has nothing to do with packaging and compilation, we will peel off Webpack and Tapable and write a pure webpack to see how the working principle of Webpack works.
Tapable looks like an array of advanced functions. Such as some, every, forEach, reduce, map and so on. In fact, tapable is the integration of plugin list. Plugin list is an array, so what tapable does is execute all the plugins in the plugin list. The process is naturally similar to some/every/forEach/reduce, but with asynchronous operations.
Webpack principle
There are three links to webPack principles at webpack.js.org/concepts/
Handwritten webpack
1. Create the simply-webpack configuration file simplepack.config.js
2. Create the contents of the SRC directory: SRC /index.js, SRC /greeting.js, SRC /name.js
3. Create contents in lib directory:
Lib /index.js: Compiler is introduced to execute the run method.
Lib/parser. js: Parse the syntax tree to get dependencies and convert es6 syntax to ES5 syntax
Lib /Compiler.js: constructor takes the options argument and mounts the entry and output to the instance. There are three methods: run for entry, buildModule for build modules, and emitFiles for output files.
4. packages: @babel/core, @babel/parser, @babel/preset-env, @babel/traverse
See minipack in github repository