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 hooksAccelerate, break (accelerate accepts one parameter),Asynchronous hookscalculateRoutes
    • Use the hook correspondingBind and execute methods
    • CalculateRoutes usetapPromiseYou can return apromiseObject.
// 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 WebpackCompiler
  • 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:

  1. A JavaScript class function.
  2. Define an injection in the function prototypecompilerThe object’sapplyMethods.
  3. applyThe compiler inserts the specified event hooks into the compiler function and retrieves the compilation object in the hook callback
  4. Modify webapack internal instance data using the Compilation manipulation.
  5. 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…