This article is also from the course study notes of Pull Education to understand the Webpack packaging process

1. Enter NPX webpack or YARN webpack on the terminal to pack. Then node_modules/.bin/webpack. CMD is displayed

CMD = webpack/bin/webpack.js

3, in webpack/bin/webpack.js it will

require(webpack-cli/bin/cli.js)
Copy the code

This is to execute the cli.js file, which contains a self-calling function that starts the Webpack process

4. In CLI. js, options and command line parameters will be processed first, that is, the user set options and default options and command line parameters will be merged and passed

const webpack = require("webpack");
Copy the code

Import the export Webpack function for webpack/lib/webpack, and then execute it

5, execute webpack(options) return compiler, where webpack function steps

  1. Instantiate the Compiler object

    • Attach the context on options to the Compiler instance
    • Initialize the hooks object, which initialize the corresponding hooks. The core lifecycle hooks include:

    entryOption -> beforeRun -> run -> beforCompile -> compile -> thisCompilation -> compilation -> make -> afterCompile -> emit

  2. The options passed in are mounted to the Compiler

  3. Initialize NodeEnvironmentPlugin(to make compiler concrete file read and write)

new NodeEnvironmentPlugin().apply(compiler)
Copy the code
  1. Mount the Piugin plug-in on options to the Compiler, i.e. iterate through the plugin.apply method

    if (options.plugins && Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            plugin.apply(compiler)
        }
    }
Copy the code
  1. Mount the webPack built-in plug-in, execute the entryOption hook, and mount the compilation.addentry () method execution to the make hook

    // webpack.js
    new WebpackOptionsApply().process(options, compiler);
Copy the code
    // WebpackOptionsApply.js
    class WebpackOptionsApply {
        // The purpose of process is to mount the compilation.addentry () method execution to the make hook
        process(options, compiler) {
            new EntryOptionPlugin().apply(compiler)
            compiler.hooks.entryOption.call(options.context, options.entry)
        }
    }
Copy the code

6. Execute the compiler.run method

  1. Assign the parameter callback to the variable finalCallback

  2. Define the variable onCompiled, where emitAssets is executed, that is, the chunk processed is written to the specified file and output to dist

  3. Execute beforeRun, run lifecycle hooks, and compiler.compile in the run lifecycle done callback

    this.hooks.beforeRun.callAsync(this.(err) = > {
        this.hooks.run.callAsync(this.(err) = > {
            this.compile(onCompiled)
        })
    })
Copy the code

7. Execute the compiler.compile method

  1. To invoke the compiler. Params newCompilationParams initialize variables

    newCompilationParams() {
        // Enable Params to create modules
        const params = {
            normalModuleFactory: new NormalModuleFactory()
        }
        return params
    }
Copy the code
  1. Execute beforeCompile, compile lifecycle hooks in sequence

  2. After executing the compile lifecycle hook, compiler. NewCompilation (Params) is called to initialize compilation. ThisCompilation is also executed in the newCompilation method, The Compilation life cycle hook

    • Compiler…. is mounted to Compilation in the Compilation constructor A lot of property
    // Compiler.js
    newCompilation(params) {
        const compilation = this.createCompilation()
        this.hooks.thisCompilation.call(compilation, params)
        this.hooks.compilation.call(compilation, params)
        return compilation
    }
    createCompilation() {
        return new Compilation(this)}// Compilation.js
    class Compilation extends Tapable {
        constructor(compiler) {
            super(a)this.compiler = compiler
            this.context = compiler.context
            this.options = compiler.options
            // Compilation can read and write files
            this.inputFileSystem = compiler.inputFileSystem
            this.outputFileSystem = compiler.outputFileSystem
            this.entries = []  // Store an array of all entry modules
            this.modules = [] // Store all module data
            this.chunks = []  // Store the chunk generated in the current packaging process
            this.assets = []
            this.files = []
            this.hooks = {
            succeedModule: new SyncHook(['module']),
            seal: new SyncHook(),
            beforeChunks: new SyncHook(),
            afterChunks: new SyncHook()
        }
    }
Copy the code
  1. Execute the Make lifecycle hook after generating the compilation

    • This is where the compilation.addEntry method that was mounted to the make hook when the entryOption hook was executed. This method takes four parameters: Context, Entry, name, and callback
    • The compilation._addModuleChain method is called in addEntry
    • The compilation.createModule method is called in _addModuleChain. The createModule method is executed in the following order:
      1. Create a module from normalModuleFactory, which is used to load entry files
      1. Initialize the afterBuild method, where the dependencies of the currently created module are loaded
      1. Compilation. BuildModule (module, afterBuild) is called, where the module.build() method is called
        • 01 reads from the file the contents of the Module to be loaded in the future, this
        • 02 If the current module is not a JS module, Loader processes it and returns the JS module
        • 03 After the above operations are complete, you can convert the JS code into the AST syntax tree
        • The current JS module may reference many other modules, so we need to recurse to complete
        • Once we’ve done 05, we just need to repeat
      1. By executing the doAddEntry method, you push the currently created entry file module into compilation.entries
      1. Push the current entry file module into compiler.modules
      1. Perform the dependency module load, which is steps 3-5 above
  2. After executing the make lifecycle hook, the done callback to the Make hook is executed, which executes the compilation.seal() method as follows

    • Execute the SEAL and beforeChunks hooks in the compilation
    • Go through compiler. entries and merge each of the entry modules and its dependencies into a chunk and push it into the compiler. chunks array
    • Call compilation. CreateChunkAssets generated code
  3. Calling compiler.emitAssets executes onCompiled in the compiler.run method

    // Compilation.js
    /** * Finish compiling the module *@param {*} Context Root * of the current project@param {*} Entry Relative path of the current entry *@param {*} name chunkName main
    * @param {*} The callback callbacks * /
    addEntry(context, entry, name, callback) {
        this._addModuleChain(context, entry, name, (err, module) = > {
        callback(err, module)})}_addModuleChain(context, entry, name, callback) {
        this.createModule({
        parser,
        name: name,
        context: context,
        rawRequest: entry,
        resource: path.posix.join(context, entry),
        moduleId: '/' + path.posix.relative(context, path.posix.join(context, entry))
        }, (entryModule) = > {
        this.entries.push(entryModule)
        }, callback)
    }

    /** * define a method to create modules for reuse *@param {*} Data Some of the property values * needed to create the module@param {*} DoAddEntry Optional. Write the id of the entry module to this.entries * when loading the entry module@param {*} callback* /
    createModule(data, doAddEntry, callback) {
        let module = normalModuleFactory.create(data)

        const afterBuild = (err, module) = > {
        // In afterBuild we need to determine if we need to process dependencies after the current module loads
        if (module.dependencies.length > 0) {
            // The current logic indicates that the module has a module that needs to be loaded, so we can define a separate method to implement it
            this.processDependencies(module.(err) = > {
            callback(err, module)})}else {
            callback(err, module)}}this.buildModule(module, afterBuild)

        // Save the Module after we finish the build operation
        doAddEntry && doAddEntry(module)
        this.modules.push(module)}/** * Completes the specific build behavior *@param {*} Module The module that currently needs to be compiled@param {*} callback* /
    buildModule(module, callback) {
        module.build(this.(err) = > {
        // If the code goes here, it means that the current Module is compiled
        this.hooks.succeedModule.call(module)
        callback(err, module)})}processDependencies(module, callback) {
        // 01 The core function of the current function is to implement a dependent module recursive loading
        // 02 The idea of loading modules is to create a module and then find a way to load the contents of the module.
        // 03 We do not know how many modules the module depends on, so we need to make sure that all the modules are loaded before executing callback. Neo - async 】 【
        let dependencies = module.dependencies

        async.forEach(dependencies, (dependency, done) = > {
        this.createModule({
            parser,
            name: dependency.name,
            context: dependency.context,
            rawRequest: dependency.rawRequest,
            moduleId: dependency.moduleId,
            resource: dependency.resource
        }, null, done)
        }, callback)
    }

    seal(callback) {
        this.hooks.seal.call()
        this.hooks.beforeChunks.call()

        // 01 All current entry modules are stored in the Entries array of the compilation object
        // 02 Encapsulating chunk means taking an entry point, finding all its dependencies, putting their source code together, and then merging them

        for (const entryModule of this.entries) {
        // Core: create a module load the content of the existing module, and record the module information
        const chunk = new Chunk(entryModule)

        // Save chunk information
        this.chunks.push(chunk)

        // Assign a value to the chunk attribute
        chunk.modules = this.modules.filter(module= > module.name === chunk.name)

        }

        // After the chunk process is cleared, the chunk code is processed (template file + source code in the module == chunk.js).
        this.hooks.afterChunks.call(this.chunks)

        // Generate code content
        this.createChunkAssets()

        callback()
    }
    createChunkAssets() {
        for (let i = 0; i < this.chunks.length; i++) {
            const chunk = this.chunks[i]
            const fileName = chunk.name + '.js'
            chunk.files.push(fileName)

            // 01 Obtain the path of the template file
            let tempPath = path.posix.join(__dirname, 'temp/main.ejs')
            // 02 Read the contents of the module file
            let tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
            // 03 Get the render function
            let tempRender = ejs.compile(tempCode)
            // 04 Render data according to EJS syntax
            let source = tempRender({
                entryModuleId: chunk.entryModule.moduleId,
                modules: chunk.modules
            })

            // Output file
            this.emitAssets(fileName, source)
        }
    }

    emitAssets(fileName, source) {
        this.assets[fileName] = source
        this.files.push(fileName)
    }
Copy the code
// NormalModule.js
class NormalModule {
    constructor(data) {
        this.context = data.context
        this.name = data.name
        this.moduleId = data.moduleId
        this.rawRequest = data.rawRequest
        this.parser = data.parser // TODO:Wait to finish
        this.resource = data.resource
        this._source  // Store the source code of a module
        this._ast // Store the ast corresponding to a template source code
        this.dependencies = [] // Define an empty array to hold information about modules loaded by dependencies
    }

    build(compilation, callback) {
        01 / * * * to read from the file needs to be load the module content in the future, this is not a * 02 if the current js module requires the Loader for processing, Finally return js module * 03 after the above operation can be converted into the AST syntax tree * 04 the current JS module may reference many other modules, so we need to recurse * 05 after the completion of the previous, we just need to repeat it */
        this.doBuild(compilation, (err) = > {
        this._ast = this.parser.parse(this._source)

        // _ast is the syntax tree of the current Module, and we can modify it before converting the AST back to code
        traverse(this._ast, {
            CallExpression: (nodePath) = > {
            let node = nodePath.node

            // Locate the node where require is located
            if (node.callee.name === 'require') {
                // Get the original request path
                let modulePath = node.arguments[0].value  // './title'
                // Retrieves the name of the currently loaded module
                let moduleName = modulePath.split(path.posix.sep).pop()  // title
                // [Currently our packer only handles js]
                let extName = moduleName.indexOf('. ') = = -1 ? '.js' : ' '
                moduleName += extName  // title.js
                // Finally we want to read the contents of the current js, so we need an absolute path
                let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
                // define the current module id as OK.
                let depModuleId = '/' + path.posix.relative(this.context, depResource)  // ./src/title.js

                // Record the information about the current dependent module, which is convenient for later recursive loading
                this.dependencies.push({
                name: this.name, // TODO:Need to modify in the future
                context: this.context,
                rawRequest: moduleName,
                moduleId: depModuleId,
                resource: depResource
                })

                // Replace the content
                node.callee.name = '__webpack_require__'
                node.arguments = [types.stringLiteral(depModuleId)]
            }
            }
        })

        // This is done using ast as required. The following is done using.... Convert the modified AST back to code
        let { code } = generator(this._ast)
        this._source = code
        callback(err)
        })
    }

    doBuild(compilation, callback) {
        this.getSource(compilation, (err, source) = > {
        this._source = source
        callback()
        })
    }

    getSource(compilation, callback) {
        compilation.inputFileSystem.readFile(this.resource, 'utf8', callback)
    }
    }
Copy the code

7. Execute the compiler.emitassets () method and the callback passed in when compiler.run() is executed

  • The EMIT lifecycle hook in Compiler. emitAssets that calls compiler basically completes a build
    emitAssets(compilation, callback) {
        01 Create dist 02 Write the file after the directory is created

        // 01 Defines a utility method to perform the file generation operation
        const emitFlies = (err) = > {
        const assets = compilation.assets
        let outputPath = this.options.output.path

        for (let file in assets) {
            let source = assets[file]
            let targetPath = path.posix.join(outputPath, file)
            this.outputFileSystem.writeFileSync(targetPath, source, 'utf8')
        }

        callback(err)
        }

        // Start file writing after directory creation
        this.hooks.emit.callAsync(compilation, (err) = > {
        mkdirp.sync(this.options.output.path)
        emitFlies()
        })

    }
Copy the code