Webpack source code analysis 3

Time:2022-6-20

21. Locate the webpack packaging portal

The function of the 01 CMD file core is to assemble the node*/webpack/bin/webpack.js

02 webpack. The core operation in JS is require node_ modules/webpack-cli/bin/cli. js

03 cli.js
01 the current file generally has two operations, processing parameters and giving parameters to different logics (distribution business)
02 options
03 complier
04 complier. Run (as for what is done in run, we will look at it later. At present, we only focus on the code entry point)

A process of wepack

Merge configuration compilers beforerun

Instantiate compiler compilers run

Set the ability to read and write node files compilers beforecompile

Mount plugins compilers compile

Process the default plug-in (entry file) compilers make

22. Webpack handwriting implementation

./webpack.js

const Compiler = require('./Compiler')

const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin')



const webpack = function (options) {

//01 instantiate compiler object
let compiler = new Compiler(options.context)

compiler.options = options



//02 initialize nodeenvironmentplugin (enable the compiler to read and write specific files)
new NodeEnvironmentPlugin().apply(compiler)



//03 mount all plugins plug-ins to the compiler object
if (options.plugins && Array.isArray(options.plugins)) {

for (const plugin of options.plugins) {

plugin.apply(compiler)
}
}


//04 mount all webpack built-in plug-ins (portals)
// compiler.options = new WebpackOptionsApply().process(options, compiler);


//05 return the compiler object
return compiler

}


module.exports = webpack




const {

Tapable,
AsyncSeriesHook
} = require('tapable')



class Compiler extends Tapable {

constructor(context) {
super()
this.context = context

this.hooks = {

done: new AsyncSeriesHook(["stats"]),//

}
}
run(callback) {
callback(null, {
toJson() {
return {

Entries: [], // entry information of the current packaging

Chunks: [], // the chunk information of the current package

Modules: [], // module information

Assets: [], // the resources finally generated by the current packaging

}
}
})
}
}


module.exports = Compiler

23、entryOptionPlugin

WebpackOptionsApply
process(options, compiler)
EntryOptionPlugin

Entryoption is a hook instance,
Entryoption called tap in the apply method inside entryoptionplugin (registered event listener)
The above event listener is called after new completes entryoptionplugin
Itemtoplugin is a function that receives three parameters (context item ‘main)

SingleEntryPlugin
When calling itemtoplugin, an instance object is returned
There is a constructor responsible for receiving the context entry name above
Compilation hook listening
Make hook listening

./EntryOptionPlugin.js

const SingleEntryPlugin = require("./SingleEntryPlugin")


const itemToPlugin = function (context, item, name) {
  return new SingleEntryPlugin(context, item, name)
}


class EntryOptionPlugin {
  apply(compiler) {
    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
      itemToPlugin(context, entry, "main").apply(compiler)
    })
  }
}


module.exports = EntryOptionPlugin



./WebpackOptionsApply.js

const EntryOptionPlugin = require("./EntryOptionPlugin")

class WebpackOptionsApply {
  process(options, compiler) {
    new EntryOptionPlugin().apply(compiler)


    compiler.hooks.entryOption.call(options.context, options.entry)
  }
}


module.exports = WebpackOptionsApply





./SingleEntryPlugin.js

class SingleEntryPlugin {
  constructor(context, entry, name) {
    this.context = context
    this.entry = entry
    this.name = name
  }


  apply(compiler) {
    compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
      const { context, entry, name } = this
      console. Log ("the make hook listens for the execution of ~~~~~~")
      // compilation.addEntry(context, entry, name, callback)
    })
  }
}


module.exports = SingleEntryPlugin

24. Implement the run method

run(callback) {
 console. Log ('run method executed ~~~')


 const finalCallback = function (err, stats) {

  callback(err, stats)

 }


  const onCompiled = function (err, compilation) {

  console.log('onCompiled~~~~')
  finalCallback(err, {
  toJson() {
  return {

   entries: [],

   chunks: [],

   modules: [],

   assets: []

  }
  }
   })
}


    this.hooks.beforeRun.callAsync(this, (err) => {

    this.hooks.run.callAsync(this, (err) => {

    this.compile(onCompiled)
  })
})
}

25. Implement compile method

Newcompilationparams method call, return params, normalmodulefactory

The above operation is to obtain params

Then call the beforecompile hook to listen, and the compile listening is triggered in its callback

Call the newcompilation method, pass in the above params, and return a compilation

Createcompilation called

After the above operations, start the make hook monitoring

const {

Tapable,
SyncHook,
SyncBailHook,
AsyncSeriesHook,
AsyncParallelHook
} = require('tapable')



const NormalModuleFactory = require('./NormalModuleFactory')

const Compilation = require('./Compilation')



class Compiler extends Tapable {

constructor(context) {
super()
this.context = context

this.hooks = {

done: new AsyncSeriesHook(["stats"]),

entryOption: new SyncBailHook(["context", "entry"]),



beforeRun: new AsyncSeriesHook(["compiler"]),

run: new AsyncSeriesHook(["compiler"]),



thisCompilation: new SyncHook(["compilation", "params"]),

compilation: new SyncHook(["compilation", "params"]),



beforeCompile: new AsyncSeriesHook(["params"]),

compile: new SyncHook(["params"]),

make: new AsyncParallelHook(["compilation"]),

afterCompile: new AsyncSeriesHook(["compilation"]),

}
}
run(callback) {
console. Log ('run method executed ~~~')


const finalCallback = function (err, stats) {

callback(err, stats)

}


const onCompiled = function (err, compilation) {

console.log('onCompiled~~~~')
finalCallback(err, {
toJson() {
return {

entries: [],

chunks: [],

modules: [],

assets: []

}
}
})
}


this.hooks.beforeRun.callAsync(this, (err) => {

this.hooks.run.callAsync(this, (err) => {

this.compile(onCompiled)
})
})
}


compile(callback) {
const params = this.newCompilationParams()



this.hooks.beforeRun.callAsync(params, (err) => {

this.hooks.compile.call(params)
const compilation = this.newCompilation(params)



this.hooks.make.callAsync(compilation, (err) => {

console. Log ('make hook listening triggered ~~~~')
callback()
})
})
}


newCompilationParams() {
const params = {

normalModuleFactory: new NormalModuleFactory()

}


return params

}


newCompilation(params) {
const compilation = this.createCompilation()

}


createCompilation() {
return new Compilation(this)

}
}


module.exports = Compiler

26. Make process implementation

1、 Step
01 instantiate the compiler object (it will run through the whole process of webpack work)
02 call run method by compiler

2、 Compiler instantiation operation
01 the compiler inherits the tabable, so it has the ability to operate hooks (listen to events, trigger events, and webpack is an event flow)

02 after the compiler object is instantiated, many attributes are attached to it. The nodeenvironmentplugin operation enables it to
File read / write capability (our simulation uses FS provided by node)

03 after having the FS operation capability, the plugins are attached to the compiler object

04 establish a relationship between the internal default plug-in and the compiler, where entryoptionplugin handles the ID of the entry module

05 when instantiating the compiler, it only listens to the make hook (singleentryplugin)

5-1 there are two hooks in the apply method of the singleentryplugin module
5-2 the compilation hook enables compilation to create a common module using the normalmodulefactory factory
5-3 because it uses a self created module to load modules that need to be packaged
5-4 where the make hook is in the compiler Run will be triggered. This means that all preparations for a module before packaging are completed
5-5 addentry method call ()

3、 Run method execution (what you want to see is when the make hook is triggered)

01 in the run method, a bunch of hooks are triggered in sequence (beforerun run compile)

02 compile method execution

1 prepare parameters (where normalmodulefactory is used to create modules later)
2 trigger beforecompile
3 pass the parameters of the first step to a function and start to create a compilation (newcompilation)
4 inside the newcompilation call
  -Createcompilation called
  -This Compilation hook and compilation hook listening

03 the make hook is triggered after the compilation object is created

04 when we trigger the make hook to listen, we pass the compilation object to the past

4、 Summary

1 instantiate compiler
2 call the compile method
3 newCompilation
4 instantiates a compilation object (it is related to the compiler)
5 trigger make listening
6. The addentry method (which brings a lot of things such as context name entry) runs to compile

WebpackOptionsApply
process(options, compiler)
EntryOptionPlugin
Entryoption is a hook instance,
Entryoption called tap in the apply method inside entryoptionplugin (registered event listener)
The above event listener is called after new completes entryoptionplugin
Itemtoplugin is a function that receives three parameters (context item’main)
SingleEntryPlugin
When calling itemtoplugin, an instance object is returned
There is a constructor responsible for receiving the context entry name above
Compilation hook listening
Make hook listening

27. Addentry process analysis

01 when the make hook is triggered, it receives the compilation object implementation, and many contents are attached to it

02 deconstructed three values from compilation
Entry: the relative path of the module currently to be packaged (./src/index.js)
name: main
Context: root path of current project

03 dep deals with dependencies in the current entry module

04 addentry method called

05 there is an addentry method on the compilation instance, and then the_ Addmodulechain method to handle dependencies

06 in compilation, we can create a common module object through the normalmodulefactory factory

07 a packaging operation with 100 concurrency is enabled by default in the webpack. Currently, we see the normalmodule create()

08 in beforeresolve, a factory hook will be triggered to listen [the operation in this part is actually handling the loader, so we will focus on it]

09 after the above operations are completed, a function is obtained and stored in the factory, and then it is called

In this function call, another hook called resolver is triggered (for handling loaders, getting the resolver method means that all loaders have been handled)

11 after calling the resolver () method, it will enter the afterresolve hook, and then trigger the new normalmodule

12 save the module and add some other attributes after completing the above operations

13 calling buildmodule method to start compilation — — — — — — — — — — — — –dobuild

28. Implementation of addentry method

const path = require('path')

const Parser = require('./Parser')

const NormalModuleFactory = require('./NormalModuleFactory')

const { Tapable, SyncHook } = require('tapable')



//Instantiate a normalmodulefactory parser
const normalModuleFactory = new NormalModuleFactory()

const parser = new Parser()



class Compilation extends Tapable {

constructor(compiler) {
super()
this.compiler = compiler

this.context = compiler.context

this.options = compiler.options

//Enable compilation to read and write files
this.inputFileSystem = compiler.inputFileSystem

this.outputFileSystem = compiler.outputFileSystem

this. Entries = [] // store the array of all entry modules

this. Modules = [] // store the data of all modules

this.hooks = {

succeedModule: new SyncHook(['module'])

}
}


/**
*Complete module compilation
*@param {*} context the root of the current item

*@param {*} entry the relative path of the current entry

* @param {*} name chunkName main

*@param {*} callback callback

*/
addEntry(context, entry, name, callback) {

this._addModuleChain(context, entry, name, (err, module) => {

callback(err, module)

})
}


_addModuleChain(context, entry, name, callback) {

let entryModule = normalModuleFactory.create({

name,
context,
rawRequest: entry,

resource: path. posix. Join (context, entry), // the core function of the current operation is to return the absolute path of the entry entry

parser
})


const afterBuild = function (err) {

callback(err, entryModule)

}


this.buildModule(entryModule, afterBuild)



//Save the module after we finish this build operation
this.entries.push(entryModule)
this.modules.push(entryModule)
}


/**
*Complete specific build behavior
*@param {*} module the module that needs to be compiled at present

* @param {*} callback

*/
buildModule(module, callback) {

module.build(this, (err) => {

//If the code goes here, it means that the compilation of the current module is completed
this.hooks.succeedModule.call(module)
callback(err)
})
}
}


module.exports = Compilation




NormalModuleFactory

const NormalModule = require("./NormalModule")


class NormalModuleFactory {

create(data) {
return new NormalModule(data)

}
}


module.exports = NormalModuleFactory





./NormalModule


class NormalModule {

constructor(data) {
this.name = data.name

this.entry = data.entry

this.rawRequest = data.rawRequest

this. parser = data. Parser // todo: wait for completion

this.resource = data.resource

this._ Source // stores the source code of a module

this._ AST // stores the ast corresponding to the source code of a template

}


build(compilation, callback) {

/**
*01 read the module contents to be loaded in the future from the file. This
*02 if it is not a JS module at present, it needs to be processed by the loader and finally returned to the JS module
*03 after the above operations are completed, you can turn the JS code into an ast syntax tree
*04 the current JS module may reference many other modules, so we need to complete recursively
*05 after the previous steps are completed, we just need to repeat them
*/
this.doBuild(compilation, (err) => {

this._ast = this.parser.parse(this._source)

callback(err)
})
}


doBuild(compilation, callback) {

this.getSource(compilation, (err, source) => {

this._source = source

callback()
})
}


getSource(compilation, callback) {

compilation.inputFileSystem.readFile(this.resource, 'utf8', callback)

}
}


module.exports = NormalModule





./parser

const babylon = require('babylon')
const { Tapable } = require('tapable')


class Parser extends Tapable {
  parse(source) {
    return babylon.parse(source, {
      sourceType: 'module',
      Plugins: ['dynamicimport'] // the current plug-in can support import() dynamic import syntax
    })
  }
}


module.exports = Parser



const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncSeriesHook,
  AsyncParallelHook
} = require('tapable')


const Stats = require('./Stats')
const NormalModuleFactory = require('./NormalModuleFactory')
const Compilation = require('./Compilation')


class Compiler extends Tapable {
  constructor(context) {
    super()
    this.context = context
    this.hooks = {
      done: new AsyncSeriesHook(["stats"]),
      entryOption: new SyncBailHook(["context", "entry"]),


      beforeRun: new AsyncSeriesHook(["compiler"]),
      run: new AsyncSeriesHook(["compiler"]),


      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),


      beforeCompile: new AsyncSeriesHook(["params"]),
      compile: new SyncHook(["params"]),
      make: new AsyncParallelHook(["compilation"]),
      afterCompile: new AsyncSeriesHook(["compilation"]),
    }
  }
  run(callback) {
    console. Log ('run method executed ~~~')


    const finalCallback = function (err, stats) {
      callback(err, stats)
    }


    const onCompiled = function (err, compilation) {
      console.log('onCompiled~~~~')
      finalCallback(err, new Stats(compilation))
    }


    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.compile(onCompiled)
      })
    })
  }


  compile(callback) {
    const params = this.newCompilationParams()


    this.hooks.beforeRun.callAsync(params, (err) => {
      this.hooks.compile.call(params)
      const compilation = this.newCompilation(params)


      this.hooks.make.callAsync(compilation, (err) => {
        console. Log ('make hook listening triggered ~~~~')
        callback(err, compilation)
      })
    })
  }


  newCompilationParams() {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }


    return params
  }


  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)
  }
}


module.exports = Compiler

29. Module dependency

01 index Replace the require method in JS withwebpack_require
02 and will/ Replace title with/ src/title. js

03 realize recursive operation, so save the dependent module information for the next create

./NormalModule.js
 
 build(compilation, callback) {
    /**
     *01 read the module contents to be loaded in the future from the file. This
     *02 if it is not a JS module at present, it needs to be processed by the loader and finally returned to the JS module
     *03 after the above operations are completed, you can turn the JS code into an ast syntax tree
     *04 the current JS module may reference many other modules, so we need to complete recursively
     *05 after the previous steps are completed, we just need to repeat them
     */
    this.doBuild(compilation, (err) => {
      this._ast = this.parser.parse(this._source)


      //Here_ AST is the syntax tree of the current module. We can modify it and finally convert the ast back to code code
      traverse(this._ast, {
        CallExpression: (nodePath) => {
          let node = nodePath.node


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


            //Record the information of the currently dependent module to facilitate recursive loading later
            this.dependencies.push({
              name: this. Name, // todo: to be modified in the future
              context: this.context,
              rawRequest: moduleName,
              moduleId: depModuleId,
              resource: depResource
            })


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


      //The above operation is to use AST to modify the code as required. The following content is to use Convert the modified ast back to code
      let { code } = generator(this._ast)
      this._source = code
      callback(err)
    })



./compilation

const path = require('path')
const async = require('neo-async')
const Parser = require('./Parser')
const NormalModuleFactory = require('./NormalModuleFactory')
const { Tapable, SyncHook } = require('tapable')


//Instantiate a normalmodulefactory parser
const normalModuleFactory = new NormalModuleFactory()
const parser = new Parser()


class Compilation extends Tapable {
  constructor(compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    //Enable compilation to read and write files
    this.inputFileSystem = compiler.inputFileSystem
    this.outputFileSystem = compiler.outputFileSystem
    this. Entries = [] // store the array of all entry modules
    this. Modules = [] // store the data of all modules
    this.hooks = {
      succeedModule: new SyncHook(['module'])
    }
  }


  /**
   *Complete module compilation
   *@param {*} context the root of the current item
   *@param {*} entry the relative path of the current entry
   * @param {*} name chunkName main
   *@param {*} callback callback
   */
  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 of creating modules to achieve the purpose of reuse
   *@param {*} data some attribute values required to create the module
   *@param {*} doaddentry optional parameter. When loading the entry module, write the ID of the entry module to this entries
   * @param {*} callback
   */
  createModule(data, doAddEntry, callback) {
    let module = normalModuleFactory.create(data)


    const afterBuild = (err, module) => {
      //Using the arrow function can ensure that the point of this is determined at the time of definition
      //In afterbuild, we need to judge whether to handle dependent loading after the current module loading is completed
      if (module.dependencies.length > 0) {
        //The current logic indicates that the module has modules that need 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 this build operation
    doAddEntry && doAddEntry(module)
    this.modules.push(module)
  }


  /**
   *Complete specific build behavior
   *@param {*} module the module that needs to be compiled at present
   * @param {*} callback
   */
  buildModule(module, callback) {
    module.build(this, (err) => {
      //If the code goes here, it means that the compilation of the current module is completed
      this.hooks.succeedModule.call(module)
      callback(err, module)
    })
  }


  processDependencies(module, callback) {
    //01 the core function of the current function is to realize the recursive loading of a dependent module
    //02 the idea of loading a module is to create a module and then find a way to bring in the contents of the loaded module?
    //03 at present, we don't know how many modules a module depends on. At this time, we need to find a way to load all the dependent modules before executing the 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)
  }
}


module.exports = Compilation

30. Chunk process analysis