directory
- What is Tabable?
- Tabable usage
- Advanced the
- Other methods of Tabable
- Webpack process
- conclusion
- Actual combat! Write a plug-in
Webpack can be understood as an event-based programming paradigm, a collection of plug-ins.
Running these plug-ins on the Webapck event stream is the base class Tapable written by WebPack itself.
Tapable exposes the way to mount the plugin, allowing us to control the plugin to run on the Webapack event stream (figure below). As we will see later, the core object Compiler, Compilation, and so on are inherited from the Tabable class. (As shown below)
What is Tabable?
The Tapable library exposes a number of Hook classes that provide hooks for plug-ins to mount.
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable"); Copy the codeCopy the code
Tabable usage
- 1. Create a new Hook
- Tapable exposes all the class methods, new a class method to get the hook we need.
- Class takes an array parameter, options, which is not mandatory. Class methods take the same number of arguments, depending on the arguments passed.
const hook1 = new SyncHook(["arg1"."arg2"."arg3"]); Copy the codeCopy the code
- 2. Bind the hook using tap/tapAsync/tapPromise
Tabpack provides methods for binding hooks synchronously and asynchronously, and both have methods for binding events and executing events.
Async* | Sync* |
---|---|
Binding: tapAsync/tapPromise/tap | Binding: tap |
Execution: callAsync/promise | Implementation: the call |
- 3. Call /callAsync executes the binding event
const hook1 = new SyncHook(["arg1"."arg2"."arg3"]); // Bind events to the webapck event stream hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3 // execute the bound event hook1.call(1,2,3) copy the codeCopy the code
- Take a chestnut
- Define a Car method to create a new hook on internal hooks. , respectively,
Synchronous hooks
Accelerate, break (accelerate accepts one parameter),Asynchronous hooks
calculateRoutes - Use the hook corresponding
Bind and execute methods
- CalculateRoutes use
tapPromise
You can return apromise
Object.
- Define a Car method to create a new hook on internal hooks. , respectively,
// tapable const {SyncHook, AsyncParallelHook} = require('tapable'); // create class Car {constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source"."target"."routesList"])}; } } const myCar = new Car(); // Bind sync hook mycar.links.break. Tap ("WarningLampPlugin", () => console.log('WarningLampPlugin')); / / hooks binding synchronization And mass participation myCar. Hooks. Accelerate. Tap ("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); / / bind an asynchronous myCar. Promise hook hooks. CalculateRoutes. TapPromise ("calculateRoutes tapPromise", (source, target, routesList, callback) => {
// return a promise
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(`tapPromise to ${source}${target}${routesList}`) resolve(); }, 1000)})}); // Execute synchronous hook mycar.links.break.call (); myCar.hooks.accelerate.call('hello');
console.time('cost'); / / execute asynchronous hook myCar. Hooks. CalculateRoutes. Promise ('i'.'love'.'tapable').then(() => {
console.timeEnd('cost');
}, err => {
console.error(err);
console.timeEnd('cost'); }) copy the codeCopy the code
The results
WarningLampPlugin Accelerating to hello tapPromise to ilovetapable cost: 1003.898ms Copy codeCopy the code
CalculateRoutes can also use tapAsync binding hooks, note that the asynchronous callback is terminated with callback.
myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
// return a promise
setTimeout(() => {
console.log(`tapAsync to ${source}${target}${routesList}`) callback(); }, 2000)}); myCar.hooks.calculateRoutes.callAsync('i'.'like'.'tapable', err => {
console.timeEnd('cost');
if(err) console.log(err)}) Copies the codeCopy the code
The results
WarningLampPlugin Accelerating to Hello tapAsync to iliketapable Cost: 2007.850ms Copying codeCopy the code
Step it up
You’ve probably learned to use Tapable by now, but how does it relate to the Webapck/Webpack plug-in?
We changed the code slightly and split it into two files: Compiler.js and myplugin.js
Compiler.js
- Change the Class Car Class name to the core of Webpack
Compiler
- Accept the plugins passed in options
- Pass the Compiler as a parameter to the plugin
- Execute the run function, which triggers the execution of the corresponding hook function at each stage of compilation.
const {
SyncHook,
AsyncParallelHook
} = require('tapable');
class Compiler {
constructor(options) {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source"."target"."routesList"])};let plugins = options.plugins;
if(plugins && plugins.length > 0) { plugins.forEach(plugin => plugin.apply(this)); }}run(){
console.time('cost');
this.accelerate('hello')
this.break()
this.calculateRoutes('i'.'like'.'tapable')
}
accelerate(param){
this.hooks.accelerate.call(param);
}
break(){
this.hooks.break.call();
}
calculateRoutes(){ const args = Array.from(arguments) this.hooks.calculateRoutes.callAsync(... args, err => { console.timeEnd('cost');
if(err) console.log(err) }); Module.exports = Compiler Copies codeCopy the code
MyPlugin.js
- The introduction of the Compiler
- Define your own plug-in.
- The apply method accepts the Compiler parameter.
The Webpack plug-in is a JavaScript object with the Apply method. The Apply attribute is invoked by the Webpack Compiler, and the Compiler object is accessible throughout the compile life cycle.
- Bind methods to the hooks on the Compiler.
- Following the Webpack rules,
Pass a new instance to the plugins property
.
const Compiler = require('./Compiler')
class MyPlugin{
constructor() {} apply(conpiler){// Accept the compiler argument conpiler.hooks.break.tap()"WarningLampPlugin", () => console.log('WarningLampPlugin'));
conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
setTimeout(() => {
console.log(`tapAsync to ${source}${target}${routesList}`) callback(); }, 2000)}); } // Pass a new instance to the plugins property const myPlugin = new myPlugin (); // Pass a new instance to the plugins property const myPlugin = new myPlugin (); const options = { plugins: [myPlugin] }letCompiler = new Compiler(options) Compiler.run () copies the codeCopy the code
The results
Accelerating to Hello WarningLampPlugin tapAsync to iliketapable Cost: 2015.866ms Copying codeCopy the code
After transformation, it runs normally, and the logic of the plug-in is gradually straightened out in accordance with the thought of Compiler and Webpack plug-in.
Other methods of Tabable
type | function |
---|---|
Hook | Suffix for all hooks |
Waterfall | Synchronous method, but it passes the value to the next function |
Bail | Fusing: When the function has any return value, the current execution of the function stops |
Loop | Listeners return true to continue the loop and undefine to end the loop |
Sync | Synchronized methods |
AsyncSeries | Asynchronous serial hook |
AsyncParallel | Asynchronous parallel execution hooks |
We can choose the appropriate synchronous/asynchronous hooks based on our development needs.
Webpack process
From the above reading, we know how to mount hooks on the Webapck event stream.
Suppose we now want to customize a plug-in that changes the contents of the final output resource, on which hook should we add the event? Which step can get the webPack compiled resources to modify?
So the next task is to understand the webpack process.
Post a classic Webpack flow chart shared by Taobao team, and then slowly analyze ~
1. Webpack entry (webpack.config.js+shell options)
Read and merge parameters from configuration file package.json and Shell statements to get the final parameters;
Every time you type webpack on the command line, the operating system invokes the./node_modules/. Bin /webpack shell script. This script will go call. / node_modules/webpack/bin/webpack. Js and additional input parameters, such as – p – w.
2. Yargs parameter parsing (optimist)
Yargs.parse (process.argv.slice(2), (err, argv, output) => {}) copies the codeCopy the code
The source address
3. Webpack initialization
(1) Build compiler objects
letCompiler = new Webpack(options) copies the codeCopy the code
The source address
(2) Register NOdeEnvironmentPlugin
new NodeEnvironmentPlugin().apply(compiler); Copy the codeCopy the code
The source address
(3) Basic plug-in hanging in options, call the WebpackOptionsApply library to initialize the basic plug-in.
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.apply(compiler);
} else{ plugin.apply(compiler); } } } compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); compiler.options = new WebpackOptionsApply().process(options, compiler); Copy the codeCopy the code
The source address
4. Run Starts compiling
if (firstOptions.watch || options.watch) {
const watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
if (watchOptions.stdin) {
process.stdin.on("end".function(_) {
process.exit(); // eslint-disable-line
});
process.stdin.resume();
}
compiler.watch(watchOptions, compilerCallback);
if(outputOptions.infoVerbosity ! = ="none") console.log("\nwebpack is watching the files... \n");
} elsecompiler.run(compilerCallback); Copy the codeCopy the code
There are two cases:
1) Watching: Monitor file changes
2) Run: executes compilation
The source address
5. To trigger the compile
BeforeRun ->run->beforeCompile->compile->make->seal beforeRun->run->beforeCompile->compile->make->seal
(2) Built key Compilation objects
In the run() method, this.compile () is executed.
This.compile () created the compilation
this.hooks.beforeRun.callAsync(this, err => { ... this.hooks.run.callAsync(this, err => { ... this.readRecords(err => { ... this.compile(onCompiled); }); }); }); . compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { ... this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { ... compilation.finish(); compilation.seal(err => { ... this.hooks.afterCompile.callAsync(compilation, err ...returncallback(null, compilation); }); }); }); }); } Duplicate codeCopy the code
The source address
const compilation = this.newCompilation(params); Copy the codeCopy the code
The Compilation process is responsible for the entire Compilation process and includes methods for each part of the build. A reference to compiler is retained inside the object.
When Webpack is running in development mode, a new Compilation is created whenever a file change is detected.
Compilation is important. It compiles production resource transformation files.
6. AddEntry () make Analyze entry files to create module objects
The make event is triggered in Compile and addEntry is called
In the Make hook of Webpack, tapAsync registers a DllEntryPlugin that calls the compilation of entry modules.
This registration is executed in the compiler.compile () method.
The addEntry method adds all entry modules to the build build queue, starting the build process.
DllEntryPlugin.js
compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
compilation.addEntry(
this.context,
new DllEntryDependency(
this.entries.map((e, idx) => {
const dep = new SingleEntryDependency(e);
dep.loc = {
name: this.name,
index: idx
};
returndep; }), this.name ), this.name, callback ); }); Copy the codeCopy the code
The source address
I was surprised to see how the process ended up in dllentryplugin.js after compiling in compiler.js.
Said it will be before WebpackOptionsApply. The process () to initialize the plug-in, execute the compiler. The hooks. EntryOption. Call (options. The context, the options. Entry);
WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply { process(options, compiler) { ... compiler.hooks.entryOption.call(options.context, options.entry); }} Copy the codeCopy the code
process
entryOption
DllPlugin.js
compiler.hooks.entryOption.tap("DllPlugin", (context, entry) => {
const itemToPlugin = (item, name) => {
if (Array.isArray(item)) {
return new DllEntryPlugin(context, item, name);
}
throw new Error("DllPlugin: supply an Array as entry");
};
if (typeof entry === "object" && !Array.isArray(entry)) {
Object.keys(entry).forEach(name => {
itemToPlugin(entry[name], name).apply(compiler);
});
} else {
itemToPlugin(entry, "main").apply(compiler);
}
return true; }); Copy the codeCopy the code
DllPlugin
Actually addEntry method, there are a lot of entrance, SingleEntryPlugin also registered a compiler. The hooks. Make. TapAsync hook. Main again WebpackOptionsApply here. The process () process (233).
There are a lot of entrance, interested can debug the sequence ~
7. Building blocks
Executing the _addModuleChain() method in compilation.addentry does two main things. One is to obtain the corresponding module factory according to the type of the module and create the module, the other is to build the module.
Create a module (NormalModule, MultiModule, ContextModule, DelegatedModule, etc.) using the * moduleFactory. create method to load the loader used by the module. Acorn is called to parse the source file processed by loader to generate the abstract syntax tree AST. Iterate through the AST to build the modules that the module depends on
addEntry(context, entry, name, callback) {
const slot = {
name: name,
request: entry.request,
module: null
};
this._preparedEntrypoints.push(slot);
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
if (err) {
return callback(err);
}
if (module) {
slot.module = module;
} else {
const idx = this._preparedEntrypoints.indexOf(slot);
this._preparedEntrypoints.splice(idx, 1);
}
returncallback(null, module); }); } Duplicate codeCopy the code
AddEntry addModuleChain() source address
8. Encapsulate build results (SEAL)
Webpack will listen to seal event to call each plug-in to encapsulate the built result. It will sort out each module and chunk successively, generate compiled source code, merge, split, and generate hash. It is also a key part of our code optimization and feature addition during development.
Copy the code template. GetRenderMainfest. Render ()Copy the code
Chunk is formatted for _webpack_requie() using MainTemplate, ChunkTemplate.
9. Output resources (emit)
Output Assets to the path of Output.
conclusion
Webpack is a collection of plug-ins that tapable controls to run on the WebPack event stream. The compilation module and encapsulation of compilation are mainly relied on.
In fact, the entry file of Webpack instantiates Compiler and invokes the run method to open the compilation. The main compilation of Webpack is executed in the following sequence of hook calls.
- Compiler:beforeRun clears the cache
- Compiler: Run registers the cache data hooks
- Compiler:beforeCompile
- Compiler:compile Starts to compile
- Compiler: Make analyzes dependencies and indirectly dependent modules from the entry and creates module objects
- Compilation:buildModule building
- Compiler: normalModuleFactory build
- Compilation:seal Build result encapsulation, cannot be changed
- Compiler:afterCompile completes the build and caches the data
- Compiler: Emit output to the dist directory
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.
The important part of the Compilation is assets. If we want to generate files for you with webpack, we have to add the corresponding file information to assets.
Compilation.getstats () gets the production files and some information about chunkhash. , etc.
Actual combat! Write a plug-in
This time I’ll try to write a simple plug-in that will help us remove unwanted comments from the bundle.js generated by the WebPack package
How to write a plug-in?
Refer to the official Webpack tutorial Writing a Plugin
A WebPack Plugin consists of the following steps:
- A JavaScript class function.
- Define an injection in the function prototype
compiler
The object’sapply
Methods. apply
The compiler inserts the specified event hooks into the compiler function and retrieves the compilation object in the hook callback- Modify webapack internal instance data using the Compilation manipulation.
- Asynchronous plug-in that uses callback after data processing
Complete the initial plug-in architecture
Before Tapable, I wrote a MyPlugin class function that satisfies the first two points of the Webpack plugin structure (a JavaScript class function with an injection compiler defined in the function prototype).
Now we need Myplugin to satisfy the last three points. First, use the event hooks specified by the Compiler.
class MyPlugin{
constructor() {
}
apply(conpiler){
conpiler.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
setTimeout(() => {
console.log(`tapAsync to ${source}${target}${routesList}`) callback(); }, 2000)}); }} Copy the codeCopy the code
Common objects for plug-ins
object | hook |
---|---|
Compiler | run,compile,compilation,make,emit,done |
Compilation | buildModule,normalModuleLoader,succeedModule,finishModules,seal,optimize,after-seal |
Module Factory | beforeResolver,afterResolver,module,parser |
Module | |
Parser | program,statement,call,expression |
Template | hash,bootstrap,localVars,render |
Writing a plug-in
class MyPlugin { constructor(options) { this.options = options this.externalModules = {} } apply(compiler) { var reg = / ("([^ \ \ \"] * (\ \.) ?). *") | ('[[^ \ \ \'] * (\ \.) ?). *') | (\ / {2}. *? (\r|\n))|(\/\*(\n|.) *? \*\/)|(\/\*\*\*\*\*\*\/)/g compiler.hooks.emit.tap('CodeBeautify', (compilation)=> { Object.keys(compilation.assets).forEach((data)=> { let content = compilation.assets[data].source() // To handle the text content = content. the replace (reg, function (word) {/ / remove annotation text after return / ^ \ {2} / test (word) | | / ^ \ \ *! /.test(word) || /^\/\*{3,}\//.test(word) ? "" : word; }); compilation.assets[data] = { source(){ return content }, Size (){return content.length}})})}} module.exports = MyPluginCopy the code
The first step is to use the Compiler’s EMIT hook
The emit event is emitted by firing the compiled code into the specified stream. When this hook executes, we can retrieve the compiled stream from the compilation object returned by the callback function.
compiler.hooks.emit.tap('xxx',(compilation)=>{}Copy the code
The second step is to access the Compilation object. We use the binding to provide the emit hook function that compiles the Compilation reference. Each compilation will fetch a new compilation object. These compilation objects provide hook functions that hook into many steps of the build process.
The compilation returns a number of internal objects, as shown in partial screenshots below:
One of the things we need is compilation. Assets
assetsCompilation {
assets:
{ 'js/index/main.js':
CachedSource {
_source: [Object],
_cachedSource: undefined,
_cachedSize: undefined,
_cachedMaps: {} } },
errors: [],
warnings: [],
children: [],
dependencyFactories:
ArrayMap {
keys:
[ [Object],
[Function: MultiEntryDependency],
[Function: SingleEntryDependency],
[Function: LoaderDependency],
[Object],
[Function: ContextElementDependency],
values:
[ NullFactory {},
[Object],
NullFactory {} ] },
dependencyTemplates:
ArrayMap {
keys:
[ [Object],
[Object],
[Object] ],
values:
[ ConstDependencyTemplate {},
RequireIncludeDependencyTemplate {},
NullDependencyTemplate {},
RequireEnsureDependencyTemplate {},
ModuleDependencyTemplateAsRequireId {},
AMDRequireDependencyTemplate {},
ModuleDependencyTemplateAsRequireId {},
AMDRequireArrayDependencyTemplate {},
ContextDependencyTemplateAsRequireCall {},
AMDRequireDependencyTemplate {},
LocalModuleDependencyTemplate {},
ModuleDependencyTemplateAsId {},
ContextDependencyTemplateAsRequireCall {},
ModuleDependencyTemplateAsId {},
ContextDependencyTemplateAsId {},
RequireResolveHeaderDependencyTemplate {},
RequireHeaderDependencyTemplate {} ] },
fileTimestamps: {},
contextTimestamps: {},
name: undefined,
_currentPluginApply: undefined,
fullHash: 'f4030c2aeb811dd6c345ea11a92f4f57'.hash: 'f4030c2aeb811dd6c345',
fileDependencies: [ '/Users/mac/web/src/js/index/main.js'], contextDependencies: [], missingDependencies: []Copy the code
Optimize all chunk resources (assets). Assets are stored as key-values in compilation.assets.
Step 3, walk through assets.
1) The key in assets array is the name of the resource. In Myplugin, we get object.key ()
Main.css bundle.js index.html copies the codeCopy the code
2) Call object.source () to get the contents of the resource
Compilation. Assets [data].source(Copy the code
3) Use re to remove comments
Object.keys(compilation.assets).forEach((data)=> {
let content = compilation.assets[data].source()
content = content.replace(reg, function (word) {
return/^\/{2,}/.test(word) || /^\/\*! /.test(word) || /^\/\*{3,}\//.test(word) ?"": word; })}); Copy the codeCopy the code
Step 4, update the compilation.assets[data] object
compilation.assets[data] = {
source() {return content
},
size() {returnContent.length}} Copies the codeCopy the code
Step 5 Reference the plug-in in the Webpack
webpack.config.js
const path = require('path')
const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
entry:'./src/index.js',
output:{
path:path.resolve('dist'),
filename:'bundle.js'}, plugins:[... new MyPlugin()]} copy codeCopy the code
Plug-in address
The resources
- Taobaofed.org/blog/2016/0…
- Github.com/webpack/tap…
- Zoumiaojiang.com/article/wha…
- Webpack.js.org/api/plugins…
- Webpack.js.org/contribute/…
- Webpack.docschina.org/api/plugins…