This article is written by Golu, a member of the team. We have authorized the exclusive use of Doodle Big Front, including but not limited to editing, original annotation and other rights.

A brief analysis of Webpack key objects

Know Tapable

Tapable is similar to the Events library reused in Node and is essentially a publish-subscribe model.

var EventEmitter = require('events')

var ee = new EventEmitter()
ee.on('message'.function (text) {
  console.log(text)
})
ee.emit('message'.'hello world')
Copy the code

Some differences from Events

1. The interface names for subscribing and publishing are different

Instantiate based on Tapbale:

const { SyncHook } = require("tapable");

// 1. Create hook instance
const sleep = new SyncHook();

// 2. Invoke the subscription interface to register the callback
sleep.tap("test".() = > {
  console.log("callback A");
});

// 3. Invoke the publish interface to trigger the callback
sleep.call();

// 4. Subscriptions to the Webpack Compiler object are generally mounted in webpack under the hooks namespace
compiler.hooks.someHook.tap('MyPlugin'.(params) = > {
  / *... * /
});
Copy the code

Instantiation based on Node EventEmitter

const EventEmitter = require('events');

// 1. Create hook instance
const sleep = new EventEmitter();

// 2. Invoke the subscription interface to register the callback
sleep.on('test'.() = > {
  console.log("callback A");
});

// 3. Invoke the publish interface to trigger the callback
sleep.emit('test');
Copy the code

2. The array passed in for instantiation represents the semantics of the parameters

// 1. When creating the hook instance
class Compiler {
  constructor() {
    this.hooks = {
      compilation: new SyncHook(["compilation"."params"]),}; }/ *... * /
}

// 2. Invoke the publish interface to trigger the callback
newCompilation(params) {
  const compilation = this.createCompilation();
  compilation.name = this.name;
  compilation.records = this.records;
  this.hooks.thisCompilation.call(compilation, params);
  this.hooks.compilation.call(compilation, params);
  return compilation;
}
Copy the code

3. Extended asynchronous hooks

// Asynchronous hooks
compiler.hooks.beforeCompile.tapAsync(
  'MyPlugin'.(params, callback) = > {
    console.log('Asynchronously tapping the run hook.'); callback(); });// Asynchronous promise hooks
compiler.hooks.beforeCompile.tapPromise('MyPlugin'.(params) = > {
  return new Promise((resolve) = > setTimeout(resolve, 1000)).then(() = > {
    console.log('Asynchronously tapping the run hook with a delay.');
  });
});
Copy the code

In addition, the Tapable classes in Webpack are strongly coupled, as shown in the demo below. In the beforeCompile phase, the parameters can be modified. This hook can be used to add/modify the compilation parameters

Webpack is based on this strong coupling, with the following Compiler and Compilation instances attaching sufficient context information to trigger hooks at certain times. Enables the Plugin to subscribe and, based on the current context information and business logic, have side effects (modifying the context state) that affect the subsequent compilation process.

// Strongly coupled Compiler instance based on Tapable class implementation
compiler.hooks.beforeCompile.tapAsync('MyPlugin'.(params, callback) = > {
  params['MyPlugin - data'] = 'important stuff my plugin will use later';
  callback();
});
Copy the code
  1. Extended the feature of HookMap as a collection operation

See JavascriptParser-hooks for the source code

this.hooks = Object.freeze({
  / * *@type {HookMap<SyncBailHook<[CallExpressionNode, BasicEvaluatedExpression | undefined], BasicEvaluatedExpression | undefined | null>>} * /
  evaluateCallExpressionMember: new HookMap(
    () = > new SyncBailHook(["expression"."param")),... otherHooks, });Copy the code

In case of HookMap:

// JavascriptParser.js
const property =
    expr.callee.property.type === "Literal"
        ? `${expr.callee.property.value}`
        : expr.callee.property.name;
const hook = this.hooks.evaluateCallExpressionMember.get(property);
if(hook ! = =undefined) {
    return hook.call(expr, param);
}

// index.js
const a = expression.myFunc();

// MyPlugin.js
parser.hooks.evaluateCallExpressionMember
  .for('myFunc')
  .tap('MyPlugin'.(expression, param) = > {
    / *... * /
    return expressionResult;
  });
Copy the code

In the absence of HookMap:

// JavascriptParser.js
const property =
    expr.callee.property.type === "Literal"
        ? `${expr.callee.property.value}`
        : expr.callee.property.name;
return this.hooks[`evaluateCallExpressionMember${property.toFirstUpperCase()}`].call();

// index.js
const a = expression.myFunc();

// MyPlugin.js
parser.hooks.evaluateCallExpressionMemberMyFunc
  .tap('MyPlugin'.(expression, param) = > {
    / *... * /
    return expressionResult;
  });
Copy the code

Compiler

Compiler is the Compiler manager, which records the complete Webpack environment and configuration information and is responsible for compilation. When Webpack is started and finished, Compiler will only be generated once. Throughout the entire life cycle of Webpack packaging. The compiler object provides the current Webpack configuration information, as shown in compiler.options below.

Compiler uses the Tapable class internally to implement plug-in publishing and subscription, as shown in the following example.

const { AsyncSeriesHook, SyncHook } = require('tapable');

/ / create the class
class Compiler {
  constructor() {
    this.hooks = {
      run: new AsyncSeriesHook(['compiler']), // Asynchronous hooks
      compile: new SyncHook(['params']), // Synchronize the hook
    };
  }
  run() {
    // Execute asynchronous hooks
    this.hooks.run.callAsync(this.(err) = > {
      this.compile(onCompiled);
    });
  }
  compile() {
    // Execute the sync hook and pass the parameter
    this.hooks.compile.call(params); }}module.exports = Compiler;
Copy the code
const Compiler = require('./Compiler');

class MyPlugin {
  apply(compiler) {
    // Accept the compiler argument
    compiler.hooks.run.tap('MyPlugin'.() = > console.log('Start compiling... '));
    compiler.hooks.complier.tapAsync('MyPlugin'.(name, age) = > {
      setTimeout(() = > {
        console.log('Compiling... ');
      }, 1000); }); }}This is similar to the plugins configuration for webpack.config.js
// Pass a new instance to the plugins property

const myPlugin = new MyPlugin();

const options = {
  plugins: [myPlugin],
};

const compiler = new Compiler(options);

compiler.run();
Copy the code

The complete implementation is available in Webpack V5.38.1 Compiler.js source code

Commonly used hook

Compiler hooks are available on the Webpack website for a detailed description of most of the hooks

  • environment
  • afterEnvironment
  • entryOptions
  • afterPlugins
  • normalModuleFactory
  • compilation
  • make
  • .

Compilation

The Compilation represents a resource version build. When WebPack is running, each time a file change is detected, a new compilation is created, resulting in a new set of compilation resources. A Compilation object represents the current module resources, compile-generated resources, changing files, and state information about the dependencies being tracked, simply storing the compiled content in memory. The Compilation object also provides callbacks to plugins that need to customize their functions, so that plug-ins can choose to use extensions when doing their own customization.

The Compilation class also inherits from the Tapable class and provides some lifecycle hooks. See the following figure for the Compilation instance structure.

Commonly used hook

The documentation for most of the hooks from Compilation is available on the Webpack website

  • seal
  • finishModules
  • record
  • optimize
  • .

NormalModuleFactory

The NormalModuleFactory module is compiled by Compiler to generate various modules. Starting at the entry point, NormalModuleFactory decompiles each request, parses the file contents to find further requests, and then crawls the entire file by decomposing all requests and parsing new files. In the final phase, each dependency becomes an instance of a Module.

The NormalModuleFactory class extends Tapable and provides the following lifecycle hooks, as shown in the figure below for an example of NormalModuleFactory.

Commonly used hook

NormalModuleFactory Hooks Refer to the normalModuleFactory Hooks provided on the Webpack website for a detailed description of the documentation

  • NormalModuleFactory Hooks
  • factorize
  • resolve
  • resolveForScheme
  • afterResolve
  • createModule
  • module
  • createParser
  • parser
  • createGenerator
  • generator

JavascriptParser

Parser instances are used in WebPack to parse various modules. Parser is another Webpack class that inherits from Tapable and provides a series of hooks for plug-in developers to do custom operations during module parsing. JavascriptParser is the most commonly used parser in WebPack. For details about how to use it and its instance structure, see the following.

compiler.hooks.normalModuleFactory.tap('MyPlugin'.(factory) = > {
  factory.hooks.parser
    .for('javascript/auto')
    .tap('MyPlugin'.(parser, options) = > {
      parser.hooks.someHook.tap(/ *... * /);
    });
});
Copy the code

Commonly used hook

JavscriptParser Hooks Refer to Javascript hooks on Webpack for detailed documentation

  • evaluateTypeof
  • evaluate
  • evaluateIdentifier
  • evaluateDefinedIdentifier
  • evaluateCallExpressionMember
  • statement
  • statementIf
  • label
  • import
  • importSpecifier
  • export
  • exportImport
  • .

Does any of this look familiar to you if you have a brief understanding of Babel or AST?

  • @babel/types – callExpression
  • ESTree Spec – CallExpression @babel/parse relies on Acorn while Acorn uses the ESTree specification

Summary of Key Objects

  • Tapable: a small publishing and subscription library for the WebPack plugin architecture;
  • Compiler: Compiler manager. After Webpack starts, Compiler objects are created and persist until exit.
  • Compilation: Manager of a single edit process, running only with a compiler but creating a new Compilation object each time a file change triggers a recompilation.
  • NormalModuleFactory: Modules generate factory classes, examples of which are generated in compiler to process dependencies and generate module relationships starting with entry files;
  • JavascriptParser: JAVASCRIPT module parser. A generated instance of JavascriptParser is used in NormalFactory to parse JS modules.

Webpack builds flowchart

webpack()

Instantiate the Compiler object

graph TD O1[node api]-->A O2[webpack-cli]-->A O3[webpack-dev-server]-->A A["webpack(options, Callback) "] - > {B "Array. IsArray (options)"} -- > B is | | D [" createMultiCompiler (options) "] B Whether - > | | E [" createCompiler (options) "] D - > F {callback? } E > F F -- - > | | F1 is {options. Watch? F1} -- > is | | W/" compiler. Watch () "-- -- -- -- > > Return F1 whether | | Run [" compiler. The Run ()"] F - > whether | | Run Run - > Return [" Return compiler "]

createCompiler()

What was done when you created the Compiler

Graph of TD A [" createCompiler (rawOptions) "] - > B [" getNormalizedWebpackOptions (rawOptions) "] B - > | normalization The options | C [" applyWebpackOptionsBaseDefaults (options) "] C - > | writing context default values for the options | D [" new Compiler (options. The context) "] D - > | E | instantiation compiler object [" new NodeEnvironmentPlugin (). The apply (compiler) "] E - > F {" Array. IsArray (options. The plugin) "} F - > | | is F1 [" traverse plugin. Apply (compiler) "] - > plugins | G | application F - > whether | | G G [" applyWebpackOptionsDefaults (options) "] -- > | options Write the default value | Evt1 subgraph compiler. The hooks Evt1 [" compiler. Hooks. The environment. The call () "] -- > Evt2 Evt2["compiler.hooks.afterEnvironment.call()"] I["compiler.hooks.initialize.call()"] end Evt2-->H["new WebpackOptionsApply (). The process (options, the compiler) "] -- > | options based configuration application plug-ins | I I - > R [return compiler]

compiler.run() / compiler.watch()

What does compiler do at compile time

graph TD A["compiler.run(callback)"]-->B["getNormalizedWebpackOptions(rawOptions)"]-->Evt1 subgraph compiler.hooks Evt1["compiler.hooks.beforeRun.callAsync(compiler)"] Evt2["compiler.hooks.run.callAsync(compiler)"] Evt3["compiler.hooks.beforeCompile.callAsync(params)"] Evt4["compiler.hooks.compile.call(compiler)"] Evt5["compiler.hooks.make.callAsync(compilation)"] Evt6["compiler.hooks.finishMake.callAsync(compilation)"] Evt7["compiler.hooks.afterCompile.callAsync(compilation)"] Evt8["compiler.hooks.done.callAsync(stats)"] EvtNMF["compiler.hooks.normalModuleFactory.call(normalModuleFactory)"] EvtCMF["compiler.hooks.contextModuleFactory.call(contextModuleFactory)"] end Evt1-->Evt2 Evt2 - > E [" compiler. The readRecords () "] E - > | options. The reference recordsInputPath, this step is not important | F F["compiler.compile(onCompiled)"]-->H["compile.newCompilationParams()"] H.->H1["params.normalModuleFactory = new NormalModuleFactory()"]-->EvtNMF H1.->H2["params.contextModuleFactory = new ContextModuleFactory()"]-->EvtCMF-->|params|Evt3 Evt3-->Evt4 Evt4-->I["compiler.newCompilation(params)"] I-->J["new Compilation(compiler)"]-->EvtC1 subgraph compilation.hooks EvtC1["compilation.hooks.thisCompilation.call(compilation, params)"] EvtC1-->EvtC2["compilation.hooks.compilation.call(compilation, params)"] end EvtC2-->EntryEvt1 EvtC2-->Evt5 Evt5-->EntryEvt2-->EntryCD EntryCD["EntryPlugin.createDependency(entry, options)"]-->|dep|addEntry addEntry["compilation.addEntry(context, dep, options)"]-->Evt6 Evt6-->L["compilation.finish()"] L-->M["compilation.seal()"] M-->Evt7 Evt7 - > N [" compiler. EmitAssets () "] N - > O [" compiler. EmitRecords () "] O - > | options. The reference recordsOutputPath, this step is not important | P [" new Stats(compilation)"]-->|stats|Evt8 subgraph EntryPlugin EntryEvt1["compiler.hooks.compilation.tap"] EntryEvt2["compiler.hooks.make.tapAsync"] end

compilation.addEntry()

What did compilation do after the add entry started

graph TD


addEntry["compilation.addEntry"]
addEntryItem["compilation._addEntryItem"]
addModuleTree["compilation.addModuleTree"]
handleModuleCreation["compilation.handleModuleCreation"]
factorizeModule["compilation.factorizeModule"]
factorizeQueue["compilation.factorizeQueue.add"]
ensureProcessing["compilation.factorizeQueue._ensureProcessing"]
startProcessing["compilation.factorizeQueue._startProcessing"]
processor["compilation.factorizeQueue._processor"]
_factorizeModule["compilation._factorizeModule"]
factoryCreate["NormalModuleFactory.create"]

subgraph NormalModuleFactory
beforeResolve["normalModuleFactory.hooks.beforeResolve.callAsync(resolveData)"]
factorize["normalModuleFactory.hooks.factorize.callAsync(resolveData)"]
resolve["normalModuleFactory.hooks.resolve.callAsync(resolveData)"]
afterResolve["normalModuleFactory.hooks.afterResolve.callAsync(resolveData)"]
createModule["normalModuleFactory.hooks.createModule.callAsync(createData, resolveData)"]
end

addModule["compilation.addModule"]
buildModule["compilation.buildModule"]
processModuleDependencies["compilation.processModuleDependencies"]

addEntry-->addEntryItem-->addModuleTree
-->handleModuleCreation-->factorizeModule
-->factorizeQueue-->ensureProcessing-->
startProcessing-->processor-->
_factorizeModule-->factoryCreate-->
beforeResolve-->factorize-->resolve-->afterResolve-->createModule-->
addModule-->buildModule-->processModuleDependencies

Webpack HMR plug-in analysis

Run the demo

# Clone Webpack repository
$ git clone https://github.com/ShinyLeee/webpack.git

Link webpack globally
$ cd webpack && npm link

# Clone webpack-dev-server repository
$ git clone https://github.com/ShinyLeee/webpack-dev-server.git

# install dependencies
$ cd webpack-dev-server & npm i

# link webpack-dev-server to global
$ npm link

# link webpack and webpack-dev-server in node_modules to the global local repository just linked
$ npm link webpack && npm link webpack-dev-server

Install NDB for easy debugging
$ npm install -g ndb

Run react-hMR demo
$ cd examples/cli/hmr && ndb webpack-dev-server
Copy the code

HMR source code analysis

compiler.hooks.compilation

Compilation. DependencyFactories, compilation. DependencyTemplates inject HMR API (accept, technical)

compilation.hooks.record

  • Description: Writes a set of data retrieved when the fullHash event is triggered to recordings. json.
  • Source diff: feature/hooks. D9387f57c4ede4e67f520fffe761515fd0a9fc record – 78.

compilation.hooks.fullHash

  • The fullhash. call file is not listed in the official webPack document, so you can only use the fullhash. call file in the source code to find the corresponding location of the event.
    1. Chunks are traversed and hash written to chunkModuleHashes combined with the record event above and eventually produced to records.json.
    2. Record the changed module information to updatedModules, combined with the following processAssets event, Finally through compilation. EmitAsset method and comply with outputs. HotUpdateMainFilename (‘ [runtime]. [fullhash]. Hot – update. Json ‘) and Outputs. HotUpdateChunkFilename (‘ [runtime]. [fullhash]. Hot – update. Json ‘) output hot update js, json files.
  • Source diff: feature/hooks. Ff381577e67231299cd3ae81d43a1d62a837c fullHash – 244

compilation.hooks.processAssets:

  • Introduction: Hot-update. js and hot-update.json files. Note that these two files will not be generated when compiling the first time. Actual results can be found in the feature/hooks. ProcessAssets – f30e9bb46c5969f190ba07ef33e87cd274fe8116 processAssets event (present) Feature/hooks. ProcessAssets – e5fac945adcd50b587ec2706da228f169961496e (remove processAssets event, You can see that removing the processAssets event handler no longer produces the file resources associated with the hot update.

compilation.hooks.additionalTreeRuntimeRequirements

  • Introduction: Here the official webpack document does not list the document of this event, but its actual effect is the most obvious, injection of HMR runtime related code, the actual effect can be seen in the source code diff, you can see from the diff output js resource file HMR related code is all gone. So this is the core implementation layer of how the HMR plug-in works with Dev-Server.
  • Source diff: hooks. AdditionalTreeRuntimeRequirements – c16a0f1df8954cfabd6997db1d1900e85d654771

normalModuleFactory.hooks.parser.for(“javascript/auto”)

  • If the module.hot or import.meta. WebpackHot API exists in the business code while parsing the JS file, it registers the function hooks event and actually writes the code to the compiled source code.
  • Source diff: hooks. The parser – de6cf202ebcc404f2a6158fc80b7f537894a74d8

Webpack debugging tips

  • NPM link Local debugging
  • Test cases are analyzed with outputs and source code
  • Use NDB or vscode debug breakpoints for debugging
  • If the webPack build process is really complicated, you can get a sense of the execution flow of the object from the hooks on the object instance you need to study.
  • If you can’t find a hook on the official website, you can use hooks.[hook name]. Call to search principles.

conclusion

The overall WebPack build process is based on the Tapable event flow mechanism, which is very flexible, but loses readability and is really too… Nausea, some other methods and modules have TS type annotation is a good read, but comments are only basic description of parameter descriptions of methods almost no, this is also very painful, but some of the built-in ability basically is all based on some of the base class to implement, as long as the other understand understand a relatively convenient, Hooks, for example, are all based on the Tapable class, as well as some Module classes.