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

  • throughyargsparsingconfig 与 shellConfiguration items in
  • webpackThe initialization process is based on step 1optionsgeneratecompilerObject, and then initializewebpackBuilt-in plugins andoptionsconfiguration
  • runRepresents the start of compilation and buildscompilationObject to store all data for this compilation
  • makePerform the actual build process, starting with the entry file and building the module until all modules are created
  • sealgeneratechunksforchunksPerform a series of optimizations and generate code to output
  • sealAfter the end,CompilationAll work on the instance is finished at this point, meaning that a build process is complete
  • emitWhen triggered,webpackWill traversecompilation.assets, generate all files, and then trigger the task pointdoneTo 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 firstentryFor each of themoduleI’m going to make a new onechunk
  • traversemodule.dependenciesAnd 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.