preface
Webpack is a powerful packaging tool, with flexible, rich plug-in mechanism, on the Internet about how to use Webpack and webpack principle analysis of technical documents emerge in an endless stream. Recently, I have been learning webpack myself, so I would like to record and share it with you. I hope it will be helpful to you. This article focuses on what happens during a build process for WebPack. (Let’s only study the overall process of research construction, not see the details 🙈)
Known, Webpack source code is a plug-in architecture, many functions are achieved through a lot of built-in plug-ins. Webpack wrote its own plug-in system for this purpose, called Tapable, which provides the function of registering and calling plug-ins. We want you to know something about Tapable before we work together
debugging
The most direct way to read the source code is to debug key code using breakpoints in Chrome. You can use the Node-Inspector to debug the debugger.
"scripts": {
"build": "webpack --config webpack.prod.js"."debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress",},Copy the code
Run NPM run build && NPM run debug
// Import file
import { helloWorld } from './helloworld.js';
document.write(helloWorld());
// helloworld.js
export function helloWorld() {
return 'bts';
}
// webpack.prod.js
module.exports = {
entry: {
index: './src/index.js',},output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
module: {
rules: [{test: /.js$/.use: 'babel-loader',},]},};Copy the code
Basic architecture
Let’s take a look at the main webPack process as a whole with a big picture and more details later
- through
yargs
parsingconfig
与shell
Configuration items in webpack
The initialization process is based on step 1options
generatecompiler
Object, and then initializewebpack
Built-in plugins andoptions
configurationrun
Represents the start of compilation and buildscompilation
Object to store all data for this compilationmake
Perform the actual build process, starting with the entry file and building the module until all modules are createdseal
generatechunks
forchunks
Perform a series of optimizations and generate code to outputseal
After the end,Compilation
All work on the instance is finished at this point, meaning that a build process is completeemit
When triggered,webpack
Will traversecompilation.assets
, generate all files, and then trigger the task pointdone
To end the build process
The build process
When I study other technical blogs, I have similar analysis of the main body process above. I understand the truth, but I can’t convince myself without interrupting the details. The following is a list of detailed actions for some mission points. It is recommended that interested partners hit several debugger
It is highly recommended that you type the debugger in the callback function of every important hook, otherwise you might skip away
Webpack preparation phase
Webpack startup entry, webpack-cli/bin/cli.js
const webpack = require("webpack");
// Use yargs to parse command line arguments and merge configuration file parameters (options),
// Then call lib/webpack.js to instantiate compile and return
let compiler;
try {
compiler = webpack(options);
} catch (err) {}
Copy the code
// lib/webpack.js
const webpack = (options, callback) = > {
// First check whether the configuration parameters are valid
/ / create a Compiler
let compiler;
compiler = new Compiler(options.context);
compiler.options = newWebpackOptionsApply().process(options, compiler); . if (options.watch ===true| |..) {... return compiler.watch(watchOptions, callback); } compiler.run(callback); }Copy the code
Create a Compiler
Compiler object, which can be understood as the scheduling center of Webpack compilation, is a compiler instance, in which the compiler object records complete webpack environment information. In each process of Webpack, Compiler is generated only once.
class Compiler extends Tapable {
constructor(context) {
super(a);this.hooks = {
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
make: new AsyncParallelHook(["compilation"]),
entryOption: new SyncBailHook(["context"."entry"])
// There are many different types of hooks defined
};
// ...}}Copy the code
As you can see, the Compiler object inherits from Tapable and defines many hooks when initialized.
Initialize the default plug-in and Options configuration
The WebpackOptionsApply class registers the corresponding plug-ins based on the configuration, and one of the more important ones is this one
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
Copy the code
Compiler’s entryOption hook is subscribed to in EntryOptionPlugin and relies on the SingleEntryPlugin
module.exports = class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
return newSingleEntryPlugin(context, item, name); }); }};Copy the code
The SingleEntryPlugin subscribed to the Compiler’s make hook and waited for addEntry to be executed in the callback, but the make hook had not yet been triggered
apply(compiler) {
compiler.plugin("compilation", (compilation, params) => {
const normalModuleFactory = params.normalModuleFactory;
// The factory object for SingleEntryDependency is NormalModuleFactory
compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory);
});
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
// Create a single-entry dependency
const dep = SingleEntryPlugin.createDependency(entry, name);
// Enter the construction phasecompilation.addEntry(context, dep, name, callback); }); }Copy the code
run
After initializing compiler, determine whether watch is started according to options’ watch. If watch is started, call Compiler. watch to monitor the build file; otherwise, start compiler.run to build the file. Compiler. run is the entry method for this compilation, which means it’s time to start compiling.
Build build phase
Call the Compiler.run method to start the build
run(callback) {
const onCompiled = (err, compilation) = > {
this.hooks.done.callAsync(stats, err => {
return finalCallback(null, stats);
});
};
// executes a callback subscribed to the Compiler. beforeRun hook plug-in
this.hooks.beforeRun.callAsync(this, err => {
// Executes a callback subscribed to the Compiler.run hook plug-in
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled);
});
});
}
Copy the code
Compiler.compile started to actually execute our build process, the core code is as follows
compile(callback) {
// Instantiate the core factory object
const params = this.newCompilationParams();
// Executes a callback subscribed to the Compiler.beforecompile hook plug-in
this.hooks.beforeCompile.callAsync(params, err => {
Execute the callback to compiler.compile
this.hooks.compile.call(params);
// Create the Compilation object for this Compilation
const compilation = this.newCompilation(params);
// executes a callback subscribed to the Compiler.make hook plug-in
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err= > {
compilation.seal(err= > {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation); }); })})})})}Copy the code
During the compile phase, the Compiler object starts instantiating two core factory objects, NormalModuleFactory and ContextModuleFactory. Factory objects are, as their name implies, used to create instances, and they are subsequently used to create module instances, including NormalModule and ContextModule instances.
Compilation
The core code for creating the Compilation object is as follows:
newCompilation(params) {
// Instantiate the Compilation object
const compilation = new Compilation(this);
this.hooks.thisCompilation.call(compilation, params);
// Call this.hooks.compilation to notify the plugin of interest
this.hooks.compilation.call(compilation, params);
return compilation;
}
Copy the code
The Compilation object is the core and most important object in the subsequent build process. It contains all the data during a build. This means that a build process corresponds to a Compilation instance. The hooks compilaiion and thisCompilation are fired when the Compilation instance is created.
In the Compilation object:
- Modules Records all parsed modules
- Chunks keep track of all chunks
- Assets record all files to be generated
These three properties already contain most of the Compilation object information, but it’s not clear what each module instance in Modules actually is. Let’s not worry about that, because the Compilation object has just been generated.
make
After the Compilation instance is created, the webpack preparation phase is complete, and the next step is to start the modules generation phase.
Enclosing hooks. Make. CallAsync plug-in () subscribed to make hook callback function. Going back to the previous section, during the initialization of the default plug-in (the WebpackOptionsApply class), the Compiler’s make hook is subscribed to in the SingleEntryPlugin plug-in and waits for the compiler.addentry method to execute in the callback.
Generated modules
The compilation.addEntry method triggers parsing of the first modules, the index.js entry file we configured in entry. Before diving into the process of building Modules, let’s take a look at the concept of a module instance module.
modules
Dependency can be understood as a Dependency object that has not yet been resolved into a module instance. For example, an entry module in a configuration, or other modules that a module depends on, will become a Dependency object. Each Dependency has a factory object. For example, in the debuger code, the index.js entry file first generates SingleEntryDependency, and the factory object is NormalModuleFactory. (The SingleEntryPlugin plugin has a code attached to it. If you have any doubts, please go ahead and check it out.)
// Create a single-entry dependency
const dep = SingleEntryPlugin.createDependency(entry, name);
// Enter the construction phase
compilation.addEntry(context, dep, name, callback);
Copy the code
The make event subscribed to by the SingleEntryPlugin passes the created single entry dependency to the compiler.addentry method, which basically executes _addModuleChain()
_addModuleChain
_addModuleChain(context, dependency, onModule, callback) {
...
// Find the corresponding factory function based on the dependency
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// Call the factory function NormalModuleFactory create to generate an empty NormalModule object
moduleFactory.create({
dependencies: [dependency]
...
}, (err, module) => {
...
const afterBuild = (a)= > {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null.module);
});
};
this.buildModule(module.false.null.null, err => { ... afterBuild(); })})}Copy the code
_addModuleChain receives the parameters in the dependency of the incoming entry depend on, using the corresponding NormalModuleFactory factory function. The create method to generate an empty module object, This module is stored in the compiler. modules object and dependencies. Module object in the callback, and since it is an entry file, in compiler. entries. We then perform buildModule to get to the actual buildModule content.
buildModule
The buildModule method primarily implements module.build(), corresponding to normalModule.build ().
// NormalModule.js
build(options, compilation, resolver, fs, callback) {
return this.doBuild(options, compilation, resolver, fs, err => {
...
// In a moment}}Copy the code
Let’s start with what’s going on in doBuild
doBuild(options, compilation, resolver, fs, callback) {
...
runLoaders(
{
resource: this.resource, // /src/index.js
loaders: this.loaders, // `babel-loader`
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
...
const source = result.result[0];
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source), resourceBuffer, sourceMap ); })}Copy the code
In short, doBuild calls the corresponding loaders to convert our module into a standard JS module. Here, use babel-loader to compile index.js, and source is the compiled code from babel-loader.
// source "debugger; import { helloWorld } from './helloworld.js'; Document. The write (helloWorld ());"Copy the code
At the same time, this._source object will be generated with two fields, name is our file path and value is the compiled JS code. The source code for the module is ultimately stored in the _source property, which can be obtained via _source.source(). Go back to the build method in NormalModule
build(options, compilation, resolver, fs, callback) {
...
return this.doBuild(options, compilation, resolver, fs, err => {
const result = this.parser.parse(
this._source.source(),
{
current: this.module: this.compilation: compilation,
options: options }, (err, result) => { } ); }}Copy the code
After doBuild, any of our modules are converted to standard JS modules. The next step is to call the parser. parse method to parse the JS into an AST.
// Parser.js
const acorn = require("acorn");
const acornParser = acorn.Parser;
static parse(code, options) {
...
let ast = acornParser.parse(code, parserOptions);
return ast;
}
Copy the code
The generated AST results are as follows:
import { helloWorld } from './helloworld.js'
const xxx = require('XXX')
Compilation
afterBuild()
processModuleDependencies()
addModuleDependencies()
factory.create()
make
compilation.seal
Generate chunks
The compilation.seal method mainly generates chunks, performs a series of optimizations on the chunks, and generates the code to be output. Chunk in webpack can be interpreted as a module configured in entry or a dynamically imported module.
The main attribute inside chunk is _modules, which records all the contained module objects. So to create a chunk, you need to find all of its modules. Here is a brief description of the chunk generation process:
- The first
entry
For each of themodule
I’m going to make a new onechunk
- traverse
module.dependencies
And add the dependent modules to the chunk generated in the previous step - If a module is introduced dynamically, create a new chunk for it and iterate over the dependencies
The following figure shows the this.chunks generated by our demo. There are two modules in _modules, namely the entry index module, which depends on the HelloWorld module.
compilation.seal
this.hooks.optimizeModulesBasic.call(this.modules);
this.hooks.optimizeModules.call(this.modules);
this.hooks.optimizeModulesAdvanced.call(this.modules);
this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups);
this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups);
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups); .Copy the code
For example, the SplitChunksPlugin plug-in subscribed to the Compilation optimizeChunksAdvanced hook. Now that our modules and chunks are generated, it’s time to generate the files.
Generate the file
First of all need to produce the final code, mainly in the compilation. Call the compilation in the seal. The createChunkAssets method.
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
constmanifest = template.getRenderManifest({ ... })... for (const fileManifest ofmanifest) { source = fileManifest.render(); }... this.emitAsset(file, source, assetInfo); }Copy the code
The createChunkAssets method traverses chunks to render each chunk to generate code. The compilation object instantiates three MainTemplate, ChunkTemplate, and ModuleTemplate at the same time. These three objects are used to render Chunk to get the final code template. The difference between them is that MainTemplate is used to render entry chunks, ChunkTemplate is used to render non-entry chunks, and ModuleTemplate is used to render modules in chunks.
Here, the render methods of MainTemplate and ChunkTemplate are used to generate different “wrapper code”, and the entry chunk of MainTemplate requires a startup code with webpack, so there will be some function declaration and startup. In the wrapper code, the code for each module is rendered through the ModuleTemplate, but again only “wrapper code” is generated to encapsulate the real module code, which is provided through the source method of the module instance. This may not be easy to understand, but look directly at the code in the final build file, as follows:
emitAsset
compilation.assets
compilation
seal
compilation
emit
The hook emit will be executed before Compiler starts generating the file. This is the last chance for us to modify the final file, after which the generated file cannot be changed.
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
Copy the code
Webpack simply iterates through compiler. assets to generate all the files and then triggers the hook Done to end the build process.
conclusion
We have gone through the process of building the WebPack core, and hope that after reading the full article, you can understand the principle of WebPack
This article code has been deleted and changed for better understanding. Limited ability, if there is an incorrect place welcome to correct, exchange and study together.