preface: WebPack as a packaging tool, its input is a variety of static files and configuration parameters, can achieve flexible and extensible plug-in configuration and loaders loading, and finally output the packaged bundle file. The picture below is the webpack diagram in the official website, Webpack can pack the whole world!In working with WebPack, there have been several questions:
- What exactly does the process in the middle of Webpack do?
- At what stage do loaders and plugins work?
- How does WebPack support such a complex configuration without affecting performance?
- Why can a packaged bundle run directly in a browser?
If you already know the answers to these questions, cross them off. Hopefully this article will give you some inspiration if you don’t already know.
Before we can analyze the source code, we need to know something about Tapable (the answer to question 3).
Webpack is event-driven in nature, moving from one event to the next. It exposes a large number of hooks for internal/external plugins throughout the build process (webPack website), and Tapable is at the heart of all of this. Tapable is similar to the publish-subscribe model, but it provides a more complex hook type. We can understand that the Webpack packaging process is composed of various events in the packaging stage (such as compile compile event), and the implementation of the plug-in is to bind its own callback function on these event hooks, so that the plug-in can run its own logic at a certain event, so as to achieve the extensibility of Webpack.
What is Webpack Tapable?
What is Webpack doing?
What is Webpack doing for example?
// a.js (webpack config entry file)
import add from './b.js'
add(1.2)
import('./c').then(del= > del(1.2) -- -- -- -- --// b.js
import mod from './d.js'
export default function add(n1, n2) {
return n1 + n2
}
mod(100.11) -- -- -- -- --// c.js
import mod from './d.js'
mod(100.11)
import('./b.js').then(add= > add(1.2))
export default function del(n1, n2) {
return n1 - n2
}
-----
// d.js
export default function mod(n1, n2) {
return n1 % n2
}
Copy the code
The dependencies of each file are as follows:
// webpack.config.js
module.exports = {
entry: {
app: 'a.js'
},
output: {
filename: '[name].js'.chunkFilename: '[name].bundle.js'.publicPath: '/'}},Copy the code
After the above code is packaged by Webpack, it will eventually output two packaged files:
- App.js – contains the code of A.j, B.js and D.js
- 0. Bundle. js – contains the code for C. js
Looking at this, we see that WebPack packages our files and outputs them according to certain rules (of course, we haven’t used plug-ins and loaders yet), but it’s ok if you don’t know how these two packages are output, which will be explained later.
How does Webpack do it? We start to get into the main topic, this article only part of the core Webpack source code, throw a brick to draw inspiration, so that we have a general understanding of the running process and life cycle of Webpack, the future encounter any webpack problems, you can have confidence to face (oh yeah).
At packaging time we will execute the following packaging commands on the command line:
webpack --config webpack.config.js
Copy the code
This command is equivalent to executing the following code in webpack-CLI:
This mimics the code in webpack-CLI, which is equivalent to typing webpack on the command line.
const options = require("./webpack.config.js");
const compiler = webpack(options);
compiler.run();
Copy the code
Load the WebPack configuration file and pass it to Webpack. The webpack file returns a compiler and runs the compiler. So, let’s look at the webpack entry first – what does webpack.js do?
A. Webpack. Js
The following code is the main part of webpack.js:
const webpack = (options, callback) = >{...// Override webPack's default configuration with our custom configuration to return the composite configuration
options = new WebpackOptionsDefaulter().process(options);
// Pass the code entry to the compiler to instantiate the compiler
compiler = new Compiler(options.context);
// The Node environment plug-in is mounted on a compiler hook
newNodeEnvironmentPlugin({... }).apply(compiler);// Custom plug-ins are mounted on hooks in the compileroptions.plugin.apply(compiler); .// The plugins used in other webPack configuration properties are mounted in compiler
compiler.options = newWebpackOptionsApply().process(options, compiler); .return compiler;
}
Copy the code
Explain a few things here:
- Webpackage 4.0 can achieve zero configuration. In the absence of user configuration, it will use the default configuration to package index.js from the SRC folder into main.js from the dist folder (many other default configurations will not be described here).
- New Compiler(options.context) this code means passing the absolute path to the current folder to the Compiler.
- The apply method of the plug-in is called in turn, and the plug-in is mounted on the Compiler. The plug-in can listen to all subsequent event nodes. A reference to the Compiler instance is also passed to the plug-in so that the plug-in can invoke the API provided by WebPack through compiler.
Plug-in examples:
class NodeEnvironmentPlugin {
apply(compiler) {
compiler.inputFileSystem = new CachedInputFileSystem(
new NodeJsInputFileSystem(),
60000
);
//....
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin".compiler= > {
if(compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge(); }); }}module.exports = NodeEnvironmentPlugin;
Copy the code
In the figure above, the code core of a plug-in in the Node environment is shown. This plugin implements the file system we need to use when parsing a file (resolver). The plug-in mounts its own event on the Compiler’s beforeRun hook. When executing the beforeRun event, the file read system is mounted to the Compiler, so that other plug-ins in the Compiler can use the file read system.
Webpack.js does a few things:
- Consolidate configuration parameters (user and default)
- Attaching node environment plug-ins and user-configured plug-ins to the compiler hooks enables each plug-in to execute its own logic at different stages of compilation.
- Return compiler instance
Next, execute Compiler.run (); Operation:
2. The compiler. Js
Webpack. js is used to prepare for compilation, Compiler. js is used to control the compilation process, and the compiler.js file is the real compilation core.
Compiler. Run: beforeRun hook –>run hook –> compile compile method –> onCompiled run
run(callback) {
const onCompiled = (err, compilation) = > {
// The callback function after compiling
// File output}...// Trigger the beforeRun hook to increase file access
this.hooks.beforeRun.callAsync(this.err= > {
// Trigger the run hook to reduce compilation of cached modules and speed up compilation of modules
this.hooks.run.callAsync(this.err= >{...this.compile(onCompiled);
});
}
Copy the code
Webpack decouples the hooks from the hooks that bind callback functions to the hooks (such as the beforeRun module that reads the file and the run module that handles the cache), making the code logic very clear.
Next, the core is the compile method:
compile(callback) {
this.hooks.beforeCompile.callAsync(params, err= > {
/ / compile
this.hooks.compile.call(params);
// newCompilation is the compiler used by WebPack
const compilation = this.newCompilation(params);
// Start compiling
this.hooks.make.callAsync(compilation, err= >{...// The compilation is complete
compilation.finish(err= >{...// Encapsulate the hook Chunk build and package optimizations that perform optimization
compilation.seal(err= >{...this.hooks.afterCompile.callAsync(compilation, err= >{...return callback(null, compilation); }); }); }); }); }); }}Copy the code
The compile function has the following logic: BeforeCompile hooks –>compile hooks –> instantiate the compiler compilation object –>make hooks –>compile ends –> SEAL wraps –>afterCompile hooks –> execute the compile callback function
The Compiler.js file does a few things:
- Start compilation. Compilation begins by instantiating the compilation object compilation and passing it to the Make hook. The real core compilation is done by the compilation object.
- Manage output. OnCompiled is executed after compilation to collate the output data.
- The entire compilation process is controlled by hooks.
In the table below, there are several hooks of Compiler. The core hook to start compiling is make.
The key hook | Hook type | Hook parameters | role |
---|---|---|---|
beforeRun | AsyncSeriesHook | Compiler | Preparation activities before running, mainly enable the function of reading files. |
run | AsyncSeriesHook | Compiler | The “machine” is already running, and caching is enabled before compilation to improve efficiency. |
beforeCompile | AsyncSeriesHook | params | To prepare for Compilation, create the ModuleFactory, create the Compilation, and bind the ModuleFactory to the Compilation. |
compile | SyncHook | params | Compile the |
make | AsyncParallelHook | compilation | Start building modules from the addEntry function of the Compilation |
afterCompile | AsyncSeriesHook | compilation | We’re done compiling |
shouldEmit | SyncBailHook | compilation | Get a telegram from the compilation to determine whether the compilation was successful and whether output can begin. |
emit | AsyncSeriesHook | compilation | Output file |
afterEmit | AsyncSeriesHook | compilation | The output is |
done | AsyncSeriesHook | Status | Success or failure, the dust has settled. |
After compiling the callback function:
const onCompiled = (err, compilation) = > {
if (this.hooks.shouldEmit.call(compilation) === false) {...this.hooks.done.callAsync(stats, err= >{... }return
}
this.emitAssets(compilation, err= >{...if (compilation.hooks.needAdditionalPass.call()) {
...
this.hooks.done.callAsync(stats, err= >{}); }; })}Copy the code
This function generates the compiled content into a file. The Compiler. EmitAssets method is used to package the file.
We can see that the Compiler file controls the compilation process through various hooks. But the actual compilation is in the compiler.js file.
so? Let’s take a look at the core compiler function: compiler.js
3. Compilation. Js
The compilation.js file does a few things:
- Start with the entry module, parse the entry type, process the module with loaders and Babel, and save the module
- Find the module dependencies, recurse to the above steps to get the module chain
- After compiling, the compiler is told to start processing the output file
// Start compiling
this.hooks.make.callAsync(compilation, err= > {
// The compilation is complete
compilation.finish(err= > {
Copy the code
In Compiler. js, we found that after executing the make hook, we did not execute any compilation object methods, but instead executed the compiler. finish method directly in its callback function.
So when exactly did it start compiling?
In fact, WebPack is compiled at plug-in binding time. In the plug-in for entry, the callback event is bound to the make hook, where the addEntry operation is performed, and the actual compilation process begins.
Interstitials: What did the entry plug-in do to prepare for the addEntry operation?
// WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
Copy the code
The code above, registered in EntryOptionsPlugin compiler. Hooks. EntryOption hook event handler, it will be different depending on the type of the entry value entry, to distinguish the single entry/entrance. Instantiate different entry plug-in types (SingleEntryPlugin, MultiEntryPlugin, etc.) to prepare for subsequent recursive parsing of the entry file.
Cut off!
//SingleEntryPlugin.js
class SingleEntryPlugin {...apply(compiler) {
compiler.hooks.compilation.tap(
"SingleEntryPlugin".(compilation, { normalModuleFactory }) = > {
// Store key-value pairs to associate SingleEntryDependency with normalModuleFactorycompilation.dependencyFactories.set( SingleEntryDependency, normalModuleFactory ); }); compiler.hooks.make.tapAsync("SingleEntryPlugin".(compilation, callback) = >{... compilation.addEntry(context, dep, name, callback); }); }... }Copy the code
Taking the single-entry plug-in as an example, what does the SingleEntryPlugin do?
- The Compilation event callbacks are registered.
- The make event callback is registered.
- The compilation event is triggered when the compiler is instantiated (before a make action is executed), and plug-ins that bind this hook can take two parameters, the compiler itself and the module factory. The callback function associates the SingleEntryDependency with the normalModuleFactory module factory, essentially associating a single entry file with the normalModuleFactory module factory.
- The addEntry method is called during the make phase, and then _addModuleChain is entered for the formal compilation phase.
// compiler.js
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name); .this._addModuleChain(
...
return callback(null.module); ; }Copy the code
The addEntry method does two things:
- Call the addEntry hook
- The _addModuleChain method is called, and the callback function carried by the make hook is executed to tell the Compiler that compilation is complete.
_addModuleChain does a few things (important) :
_addModuleChain(context, dependency, onModule, callback){...// Get the module factory type
const moduleFactory = this.dependencyFactories.get(Dep); .this.semaphore.acquire(() = > {
// Create a new module
moduleFactory.create(
...
// Add the module to compiler.modules
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
// The entry module joins compilation.entries
onModule(module);
// Process module dependencies and then convert them into module chains
const afterBuild = () = > {
if (addModuleResult.dependencies) {
this.processModuleDependencies(module.err= >{... }); }};if (addModuleResult.build) {
Parse loader generate
this.buildModule(module.false.null.null.err= >{...// Determine the dependency
afterBuild();
});
});
}
Copy the code
_addModuleChain process analysis:
1) Obtain the module factory type corresponding to the entry
Obtain either multiModuleFactory (multi-entry module production factory) or normalModuleFacotry (single-entry module factory), depending on the entry type
2) Call moduleFactory.create to create the entry module
For a single entry, moduleFactory.create calls the normalModuleFacotry create method.
What does moduleFactory.create do?
create(data, callback) {
/ /... Omitting logic
this.hooks.beforeResolve.callAsync({},
(err, result) = > {
/ /...
// Triggers the Factory event in normalModuleFactory.
const factory = this.hooks.factory.call(null);
// Ignored
if(! factory)return callback();
factory(result, (err, module) = > {
/ /...
callback(null.module); }); }); }Copy the code
- The beforeResolve event is triggered
- Triggers the Factory event in NormalModuleFactory. NormalModuleFactory Constructor has a section of logic to register factory events.
- To execute the factory method, the main process is as follows:
The factory method does two things:
- Obtain the absolute path of the file and find the absolute path of loaders and loaders based on the file type.
- Generate an instance of normalModule and store the file path and loaders path into it.
3) addModule
Once you’ve got the module instance, store it in the global compilation. modules array and in the _modules object. This stage can be thought of as the Add stage, which stores all module information in Compilation for later use when it is bundled into chunks.
4) Call buildModule to parse the module and output the dependency list
This phase does the following:
- Run the Loader
- Parser Parses the AST
- WalkStatements resolve dependencies
Spoiler: What does buildModule do? For an instance of moduleNormalModule, compilation. buildModule actually calls the normalModule. build method:
The normalModule. build method has the following logic:
/ / NormalModule. The build method
build(options, compilation, resolver, fs, callback) {
/ /...
return this.doBuild(options, compilation, resolver, fs, err= > {
/ /...
try {
// Here the source is converted to the AST to parse out all dependencies
const result = this.parser.parse(/ * * /);
if(result ! = =undefined) {
// parse is synchandleParseResult(result); }}catch(e) { handleParseError(e); }})}/ / NormalModule doBuild method
doBuild(options, compilation, resolver, fs, callback) {
/ /...
// Execute various loadersrunLoaders({... },(err, result) = > {
/ /...
// createSource converts the runLoader result to a string for subsequent processing
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
/ /...}); }Copy the code
Divided into two parts:
- The doBuild function: loaders the source file. The doBuild method actually takes the contents of the file and processes them with loaders.
- DoBuild function callback: Use Acorn to convert the code to the AST abstract syntax tree, traverse the AST to find all dependencies for the file, and add dependency instances to the Module.
4) Execute afterBuild, the buildModule callback
const afterBuild = () = >{.../ / if a dependent, enter processModuleDependencies
if (addModuleResult.dependencies) {
this.processModuleDependencies(module.err= > {
if (err) return callback(err);
callback(null.module);
});
} else {
return callback(null.module); }};Copy the code
- Perform processModuleDependencies method, processing module dependency
- Dependencies are handled in the same way as the main file, repeating the entire create–>build–> Add –>processDep process to build the entire module chain.
After the compilation process is complete and the module chain is generated, the make event callback is executed to tell the Compiler that the compilation is complete. The seal operation starts. Procedure
One of the key points in compiling is AST(Abstract Syntax tree). Click here to learn more
4. Seal encapsulation
this.hooks.make.callAsync(compilation, err= >{... compilation.seal(err= >{...this.hooks.afterCompile.callAsync(compilation, err
...
});
});
});
});
Copy the code
After all modules have been converted by loaders, the Compilation’s SEAL hook is executed to start the chunk build and optimize the packaging process based on dependencies.
This process merges chunks based on the entry files and configuration parameters for optimizing subcontracting. Because this part of the code is too long and nested, the code will not be pasted here for parsing. Next, we take the code at the beginning of the article as an example to explain the process of chunk formation, which is easy to understand:
First, WebPack generates the Module Graph (shown below) for all modules. The process of creating the module map is to start from the entry file, analyze the synchronous dependency modules and asynchronous dependency blocks of each module, and then perform the above steps for the synchronous dependency. Asynchronous dependencies are analyzed separately so that they can be packaged separately later.
Secondly, the figure above is the result of generating basic Chunk graph based on the Module graph, generating 3 Chunkgroups.
The basic Chunk graph is created by creating an import file and an asynchronous file, and WebPack creates a chunk for each of them. Starting with the import file, webPack iterates through its synchronous dependency modules and their dependency modules. And associate the module with the chunks of the inbound files (which belong to the inbound chunks) until no synchronization dependencies are found (resulting in the first chunkGroup below). In the traversal process, if the asynchronous dependency is encountered, create a separate chunk, and then perform the above process to associate modules and asynchronous chunk, thus completing the formation of the last two chunkgroups.
The last step is to optimize the Chunk Graph. Observe the result of basic Chunk graph: it is found that d.js exists in three packages and B. js exists in two packages, which will cause repeated packaging, so we need to optimize packaging. The d.js required by the last two asynchronously loaded packages have been synchronously loaded in the entry package, and the priority of synchronous loading is higher than that of asynchronous loading, so the D.js required by the last two chunkgroups can be removed. Look at the b. Js needed in the third package, which has been loaded in the form of synchronization in the entrance package, so there is no need. Delete the third package and get the final chunk.
Output package files
After the chunk consolidation is complete, the package file is finally output. The module and chunk files we get are code aggregated by require and cannot be run in the browser. Webpack provides the template to generate code in the _webpack_require() format (essentially Webpack implements the simple require function) so that the packaged code can run in the browser.
See this article for more details
Finally, the final JS is output to the path of Output via emitAssets.
Attached is a Webpack flowchart.
Reference blog:Juejin. Cn/post / 684490… Juejin. Cn/post / 684490… Juejin. Cn/post / 684490…