Hello everyone, AS a composer, I knew only the surface of Webpack before. Although I could barely cope with it in the past, I didn’t have a thorough understanding of the internal operation mechanism of Webpack until I started systematic learning recently This article is a handy Webpack that takes you handwriting and you’ll learn:

  1. Understand the Webpack workflow
  2. Familiar with WebPack configuration
  3. Understand webpack internals

But to note that this is my study notes, simple Webpack core source code is not original, is coupled with their own understanding of the product interested partners can discuss ~

Understand the core concepts of WebPack

Entry

The entry point indicates which module WebPack should use as a starting point for building its internal dependency diagram.

Once at the entry point, WebPack finds out which modules and libraries are (directly and indirectly) dependent on the entry point.

Each dependency is then processed and finally exported to a file called bundles.

Output

The Output attribute tells WebPack where to export the bundles it creates and how to name these files, with the default being./dist.

Basically, the entire application structure is compiled into a folder in the output path you specify.

Module

Modules, in Webpack everything is a module, a module corresponds to a file. Webpack recursively finds all dependent modules starting from the configured Entry.

Chunk

Code block: A Chunk is composed of multiple modules for code merging and splitting.

Loader

Loader enables Webpack to handle non-javascript files (WebPack itself only understands JavaScript).

Loader can convert all types of files into valid modules that WebPack can handle, and then you can take advantage of WebPack’s packaging capabilities to process them.

In essence, WebPack Loader converts all types of files into modules that the application’s dependency diagram (and ultimately its bundle) can reference directly.

Plugin

Loaders are used to transform certain types of modules, while plug-ins can be used to perform a wider range of tasks.

Plug-ins range from packaging optimization and compression to redefining variables in the environment. Plug-in interfaces are extremely powerful and can be used to handle a wide variety of tasks.

Reference link: juejin.cn/post/684490…

1. Understand the workflow of Webpack (key points)

  1. Initialization parameters: Read and merge parameters from configuration files and Shell statements to arrive at the final configuration object
  2. Initialize the Compiler object with the parameters obtained in the previous step
  3. Load all configured plug-ins
  4. The run method of the execution object begins compilation
  5. Locate the import/export file according to the entry in the configuration
  6. From the entry file, all configured Loaders are called to compile the module
  7. Find the module that the module depends on, and then recurse this step until all the entry dependent files have been processed by this step
  8. Chunks are assembled into modules based on the dependencies between the entries and modules
  9. Each chunk is then converted into a separate file and added to the output list
  10. After determining the output content, determine the output path and file name based on the configuration, and write the file content to the file system

It is not necessary to understand all the processes at the beginning, because the following code will have a mental image of the processes above. The diagram I drew below will help you understand:

2. Initialization preparations

(1). Create a file mini-webpack and initialize NPM

npm init  
Copy the code

(2). Add Babel dependency package to pageckage.json


"devDependencies": {
 "@babel/generator": "^ 7.14.5"."@babel/parser": "^ 7.14.6"."@babel/traverse": "^ 7.14.5"."babel-types": "^ 6.26.0",},Copy the code

(3). Join NPM to execute the command

"scripts": {
  "build": "node debugger.js"
},
Copy the code

Create a mini-webpack/debugger.js file

2. Write the mini-webpack/debugger.js file

const webpack = require('./webpack')
const webpackOptions = require("./webpack.config")

// Compiler represents the entire compilation process and is an object
const compiler = webpack(webpackOptions)
// Call the run method to initiate compilation
compiler.run((err, stats) = > {

    const result = stats.toJson({
        files: true.// Produce those files
        assets: true.// What resources are generated
        chunks: true.// Which code blocks are generated
        modules: true.// Module information
        entries: true.// Entry information
    })
    // View the compilation result
    console.log(JSON.stringify(result, null.2));
})
Copy the code

This file basically calls the WebPack instance and returns a compilation result: you don’t have to look at the compilation result in detail to understand what the field does

 
 {
    hash: '50ee8c2052552e5a4565'.// The hash value generated by this compilation
    version: '5.51.1'./ / webpack version
    time: 65.// Call time
    builtAt: 1630300492483.// Build the timestamp
    publicPath: 'auto'.// Access path of the resource
    outputPath: '/XXX//XXX/XXX/XXX/mini-webpack/dist'.// Output directory
    assetsBychunksName: {main: ['main.js']},
    "assets": [{"type": "asset"."name": "main.js"."size": 167."emitted": true."comparedForEmit": false."cached": false."info": {
                    "javascriptModule": false."size": 167
                },
                "chunksNames": [
                    "main"]."chunksIdHints": []."auxiliarychunksNames": []."auxiliarychunksIdHints": []."related": {},
                "chunks": [
                    "main"]."auxiliarychunks": []."isOverSizeLimit": false}]."chunks": [{"names": [
                    "main"]."files": [
                    "main.js"],}],"modules": [{"type": "module"."moduleType": "javascript/auto"."layer": null."size": 1."sizes": {
                    "javascript": 1
                },
                "built": true."codeGenerated": true."buildTimeExecuted": false."cached": false."identifier": "/Users/peiyahui/Desktop/code/xxx/mini-webpack/src/index.js"."name": "./src/index.js"."nameForCondition": "/Users/peiyahui/Desktop/code/xxx/mini-webpack/src/index.js"."index": 0."preOrderIndex": 0."index2": 0."postOrderIndex": 0."cacheable": true."optional": false."orphan": false."issuer": null."issuerName": null."issuerPath": null."failed": false."errors": 0."warnings": 0."id": "./src/index.js"."issuerId": null."chunks": [
                    "main"]."assets": []."reasons": [{"moduleIdentifier": null."module": null."moduleName": null."resolvedModuleIdentifier": null."resolvedModule": null."type": "entry"."active": true."explanation": ""."userRequest": "./src/index.js"."loc": "main"."moduleId": null."resolvedModuleId": null}]."usedExports": null."providedExports": null."optimizationBailout": []."depth": 0}]./ / the entry point
    "entrypoints":
        {
            "main":
                {
                    "name":
                        "main"."chunks":
                        [
                            "main"]."assets": [{"name": "main.js"."size": 167}]."filteredAssets":
                        0."assetsSize":
                        167."auxiliaryAssets": []."filteredAuxiliaryAssets":
                        0."auxiliaryAssetsSize":
                        0."children": {},"childAssets": {},"isOverSizeLimit":
                        false}}}Copy the code

3. Write the mini-webpack/webpack.js file

The first step is to import the webpack.js file, which is the webPack entry file, and write:

// webpack
function webpack(options) {
    //1. Initialize parameters: Read and merge parameters from configuration files and Shell statements to get the final configuration object
    let shellConfig = process.argv.slice(2).reduce((shellConfig, item) = > {
        let [key, value] = item.split("=");
        shellConfig[key.slice(2)] = value
        return shellConfig
    }, {})
    letfinalConfig = {... options, ... shellConfig};// 2. Initialize the Compiler object with the parameters obtained in the previous step
    let compiler = new Compiler(finalConfig)
    // 3. Load all configured plug-ins
    let {plugins} = finalConfig
    //
    for (let plugin of plugins) {
        plugin.apply(compiler)
    }
    return compiler
}
module.exports = webpack
Copy the code

Process.argv. slice(2) This is used to parse webpack execution parameters such as webpack –mode=development according to = Split key(mode) and value(development)

4. Write a mini – webpack/webpack. Config. Js file

The first step is to introduce the webpack.config.js file, which is the WebPack configuration file

const path = require('path');
const RunPlugin = require('./plugins/run-plugin');
const DonePlugin = require('./plugins/done-plugin');
const AssetPlugin = require('./plugins/assets-plugin');
module.exports = {
    mode:'development'.// Production mode
    devtool:false.context:process.cwd(),// context directory./ src. the default is the root directory. The default is the directory where the current command is executed
    entry: {/ / the entry
        entry1:'./src/entry1.js'.entry2:'./src/entry2.js'
    },
    output: {// Output address
        path:path.join(__dirname,'dist'),
        filename:'[name].js'
    },
    resolve: {// Recognize file suffixes by default
        extensions: ['.js'.'.jsx'.'.json']},module: {// Matches the suffix field of the test file to load the corresponding loader
        rules:[
            {
                test:/.js$/,
                use:[
                    path.resolve(__dirname,'loaders'.'logger1-loader.js'),
                    path.resolve(__dirname,'loaders'.'logger2-loader.js']}]},plugins: [/ / webpack plug-in
        new RunPlugin(),
        new DonePlugin(),
        new AssetPlugin()
    ]
}
Copy the code

Webpack configuration, I believe you have some understanding, mainly is to understand the above remarks of the configuration field

Write two Loaders and two plugins

Loader and plguin are very simple, mainly with webpack loading loader and plugin principle (1)mini-webpack/loader/logger1-loader

function loader(source) {
    console.log("loading")
    return source + "/ / 1"
}
module.exports = loader
Copy the code

(2)mini-webpack/loader/logger2-loader.js

function loader(source) {
    console.log("loading22222222222")
    return source + 2 "/ /"
}
module.exports = loader
Copy the code

(3)mini-webpack/plugins/done-plugins.js

class RunPlugins {
    apply(compiler) {
        compiler.hooks.done.tap("DonePlugins".() = > {
            console.log("Compilation is done.")}}}module.exports = RunPlugins
Copy the code

(4)mini-webpack/plugins/run-plugins.js

class RunPlugins {
    apply(compiler) {
        compiler.hooks.run.tap("RunPlugins".() = >{
            console.log("I'm compiling.")}}}module.exports = RunPlugins
Copy the code

6. Compiler class (emphasis)

Remember in step 1 that debuger.js calls Compiler’s run method and returns a result? Remember in step 3 when you needed to generate an instance of the Compiler class?

Hey hey, I believe you have connected.

The Compiler class’s main function is to concatenates the compilation process and trigger the hook function to respond to the plug-in. Instead of doing compilation work, it leaves the Compiler class alone

mini-webpack/Compiler.js

const fs = require("fs");
const path = require("path");
const Complication = require("./compilcation");
let {SyncHook} = require("tapable")

class Compiler {
    constructor(options) {
        this.options = options
        this.hooks = {
            run: new SyncHook(), The compilation has just started
            emit: new SyncHook(['assets']), // Will be triggered when the file is to be written
            done: new SyncHook(),  // Will be completed by the time the change is completed}}//
    //4. Execute the run method of the Compiler object to start compiling
    run(callback) {
        this.hooks.run.call();// Triggers the run hook
        //5. Locate the import/export file according to the entry in the configuration
        this.compile((err, stats) = > {
            this.hooks.emit.call(stats.assets);
            //10. After determining the output content, determine the output path and file name based on the configuration, and write the file content to the file system
            for (let filename in stats.assets) {
                let filePath = path.join(this.options.output.path, filename);
                fs.writeFileSync(filePath, stats.assets[filename], 'utf8');
            }

            callback(null, {
                toJson: () = > stats
            });
        });

        // In the middle is our compilation process
        this.hooks.done.call();  // Trigger the done hook after compiling
    }

    //
    compile(callback) {
        let complication = new Complication(this.options)
        complication.build(callback)
    }
}

module.exports = Compiler
Copy the code

The tapable.js hook function is implemented using tapable.js, which is similar to node’s publishare-subscribe mode

7. Writing an Impairment class (difficulty)

The only thing the xonly class does is compile

mini-webpack/Complication.js

/ * * *@name: compilcation
 * @author: peiyahui
 * @date: 2021/8/31 12:51 下午
 * @description: compilcation *@update: 2021/8/31 12:51 下午
 */
const path = require('path')
const fs = require('fs')
const types = require('babel-types');
const parser = require('@babel/parser');// The source code is converted to the AST abstract syntax tree
const traverse = require('@babel/traverse').default;// Walk through the syntax tree
const generator = require('@babel/generator').default;// Re-generate the syntax tree
const baseDir = toUnitPath(process.cwd());

//
function toUnitPath(filePath) {
// return filePath.replace(/\/g, '/'); // I don't know why there is an error in the nuggets code quick, please open the coding time
}

class Complication {
    constructor(options) {
        this.options = options;
        this.entries = [] // Entry information
        this.modules = []  // Module information
        this.chunks = []  // Which code blocks are generated
        this.files = []  // Produce those files
        this.assets = []  // What resources are generated
    }


    build(callback) {
        let entry = {}
        // 5. Locate the import/export file according to the entry in the configuration
        if (typeof this.options.entry === "string") {
            entry.main = this.options.entry
        } else {
            entry = this.options.entry
        }

        // Handle the entry
        for (let entryName in entry) {
            // Get the absolute path to entry1. Context is the live text (execution environment), the default is the root directory
            let entryFilePath = path.join(this.options.context, entry[entryName])
            // 6. From the entry file, invoke all configured Loader to compile the module
            let entryModule = this.buildModule(entryName, entryFilePath)  / / return
            //
            // this.modules.push(entryModulePath);

            // 8. Chunks are assembled into modules based on the dependencies between entries and modules
            let chunk = {
                name: entryName, entryModule, modules: this.modules.filter(item= > {
                    returnitem.name === entryName || item.extraNames.includes(entryName); })};this.entries.push(chunk)
            this.chunks.push(chunk) // Add to the code block
        }
        // 9. Convert each Chunk into a separate file and add it to the output list
        //9. Convert each Chunk into a separate file and add it to the output list
        this.chunks.forEach(chunk= > {
            // Replace the file name
            let filename = this.options.output.filename.replace('[name]', chunk.name);
            // this.assets is the output list. The output filename value is the output content
            this.assets[filename] = getSource(chunk);
        });
        console.log('this.chunks--------->'.this.chunks)
        // Compiler.run() functions (err, stats) => {}
        callback(null, {
            entries: this.entries,
            chunks: this.chunks,
            modules: this.modules,
            files: this.files,
            assets: this.assets
        });
    }

    // Name: Name modulePath Absolute path of the module
    buildModule(name, modulePath) {
        // 6. From the entry file, invoke all configured Loader to compile the module
        // 6.1 Reading file Contents
        let sourceCode = fs.readFileSync(modulePath, 'utf8'); //console.log('entry1');
        let rules = this.options.module.rules    // Loader path
        let loaders = []  // Find a matching loader
        //
        for (let i = 0; i < rules.length; i++) {
            let {test} = rules[i];
            // If the rule's re matches the module's path
            if(modulePath.match(test)) { loaders = [...loaders, ...rules[i].use]; }}// Load the loader from back to front
        sourceCode = loaders.reduceRight((sourceCode, loader) = > {
            // Load synchronously
            return require(loader)(sourceCode);
        }, sourceCode);
        // 7. Find the module that the module depends on and repeat this step until all the dependent files have been processed by this step
        // Get the current module module ID./ SRC /index.js
        let moduleId = '/' + path.posix.relative(baseDir, modulePath);
        let module = {id: moduleId, dependencies: [], name, extraNames: []};  // Generate a module format
        let ast = parser.parse(sourceCode, {sourceType: 'module'}); // Generate the AST syntax tree
        traverse(ast, {
            //
            CallExpression: ({node}) = > {
                if (node.callee.name === 'require') {
                    // The relative path of the dependent module
                    let moduleName = node.arguments[0].value;//./title1
                    // Get all the directories of the current module
                    let dirname = path.posix.dirname(modulePath);// /
                    // Compatible platform path writing (/////)
                    let depModulePath = path.posix.join(dirname, moduleName);
                    let extensions = this.options.resolve.extensions;   / / extensions
                    depModulePath = tryExtensions(depModulePath, extensions);// The extension name is already included
                    C: / / get dependent module ID/aproject/zhufengwebpack202106/4. The flow/SRC/title1. Js
                    // the relative path to the project root directory./ SRC /title1.js
                    let depModuleId = '/' + path.posix.relative(baseDir, depModulePath);
                    //require('./title1'); =>require('./src/title1.js');
                    node.arguments = [types.stringLiteral(depModuleId)];  // Add parameters to the node
                    // The absolute path of the dependent module is placed in the dependency array of the current module


                    module.dependencies.push({depModuleId, depModulePath}); }}})//
        let {code} = generator(ast); // Compile the code
        module._source = code;// The module source code points to the newly generated source code after the syntax tree transformation
        //7. Find the module that the module depends on and repeat this step until all the dependent files have been processed by this step
        module.dependencies.forEach(({depModuleId, depModulePath}) = > {
            / / cache
            let depModule = this.modules.find(item= > item.id === depModuleId);
            if (depModule) {
                depModule.extraNames.push(name);
            } else {
                let dependencyModule = this.buildModule(name, depModulePath);  // Compile recursively, depending on modules
                this.modules.push(dependencyModule); }});return module; }}// 
function getSource(chunk) {
    return `
    (() => {
        var modules = ({
            ${chunk.modules.map(module= >`"The ${module.id}":(module,exports,require)=>{
                        The ${module._source}
                    }
                `).join(', ')}}); var cache = {}; function require(moduleId) { var cachedModule = cache[moduleId]; if (cachedModule ! == undefined) { return cachedModule.exports; } var module = cache[moduleId] = { exports: {} }; modules[moduleId](module, module.exports, require); return module.exports; } var exports = {}; (() = > {${chunk.entryModule._source}}) (); }) (); `
}
Copy the code
Function tryExtensions(modulePath, extensions) {extension.unshift (''); for (let i = 0; i < extensions.length; i++) { let filePath = modulePath + extensions[i]; //./title.js // Return if (fs.existssync (filePath)) {return filePath; } } throw new Error(`Module not found`); } module.exports = ComplicationCopy the code

The two most important functions here are Build and buildModule, so once we understand how these functions work, we can understand the complications class. Now let’s look at the flow chart of these two functions

8. Build function workflow analysis

Accepts the argument :callback

9. BuildModule function workflow parsing

I believe you have a certain understanding of the implementation process and internal implementation principle of Webpack. Welcome to discuss with you