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
-
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
-
The options passed in are mounted to the Compiler
-
Initialize NodeEnvironmentPlugin(to make compiler concrete file read and write)
new NodeEnvironmentPlugin().apply(compiler)
Copy the code
-
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
-
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
-
Assign the parameter callback to the variable finalCallback
-
Define the variable onCompiled, where emitAssets is executed, that is, the chunk processed is written to the specified file and output to dist
-
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
-
To invoke the compiler. Params newCompilationParams initialize variables
newCompilationParams() {
// Enable Params to create modules
const params = {
normalModuleFactory: new NormalModuleFactory()
}
return params
}
Copy the code
-
Execute beforeCompile, compile lifecycle hooks in sequence
-
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
-
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:
-
- Create a module from normalModuleFactory, which is used to load entry files
-
- Initialize the afterBuild method, where the dependencies of the currently created module are loaded
-
- 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
- Compilation. BuildModule (module, afterBuild) is called, where the module.build() method is called
-
- By executing the doAddEntry method, you push the currently created entry file module into compilation.entries
-
- Push the current entry file module into compiler.modules
-
- Perform the dependency module load, which is steps 3-5 above
-
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
-
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