To prepare

tapable

Webpack is essentially an event flow mechanism, and its workflow is to connect various plug-ins in series, and the core of this is tapable, the most core of Webpack, The Compiler responsible for compiling and the Compilation responsible for creating bundles are instances of Tapable. Tapable exposes a number of hook classes that can be used to create hooks for plug-ins.

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook
} = require("tapable"); 
Copy the code

enhanced-resolve

Github.com/webpack/enh…

Enhanced – Resolve is an asynchronous, highly configurable library for resolving file paths. With enhanced- Resolve, Webpack resolves three file paths: absolute, relative, and module paths. Webpack can generate the configuration of enhanced- Resolve based on the parameters in the configuration and the default configuration, and then leverage the enhanced- Resolve library to resolve the various paths.

acorn & ast estree

Github.com/acornjs/aco…

Github.com/estree/estr…

Acorn is a small and fast javascript parser implemented entirely in javascript. Pass the code to be parsed to Acorn. parse to output an AST that follows the Estree specification. Estree is a JSON-style AST, and popular implementations of Bable and ESLint are also based on Estree. For example, let code = 1 + 1; After parsing into ast:

{
  "type": "Program",
  "start": 0,
  "end": 11,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 5,
      "end": 10,
      "expression": {
        "type": "BinaryExpression",
        "start": 5,
        "end": 10,
        "left": {
          "type": "Literal",
          "start": 5,
          "end": 6,
          "value": 1,
          "raw": "1"
        },
        "operator": "+",
        "right": {
          "type": "Literal",
          "start": 9,
          "end": 10,
          "value": 1,
          "raw": "1"
        }
      }
    }
  ],
  "sourceType": "script"
}
Copy the code

loader-runner

Github.com/webpack/loa…

Juejin. Cn/post / 684490…

Webpack is used to control the library that executes the loader.

neo-async & async

Github.com/suguru03/ne…

Github.com/caolan/asyn…

Async document

The Async library is a library of asynchronous invocation tools that provide a number of methods for controlling asynchronous processes. Neo-async is an enhanced library of Async that has been optimized for performance. There are many scenarios in Webpack that utilize the neo-Async library to execute multiple methods in parallel.

demo

The WebPack version number used in this example is 4.44.2.

// a.js (webpack config) import add from './b.js' add(1, 2) import('./c').then(del => del(1, 2) 2)) ----- // b.js import mod from './d.js' export default function add(n1, n2) { return n1 + n2 } mod(100, 11) ----- // c.js import mod from './d.js' mod(100, 11) import('./b.js').then(add => add(1, 2)) export default function del(n1, n2) { return n1 - n2 } ----- // d.js export default function mod(n1, n2) { return n1 % n2 }Copy the code

Webpack related configurations:

{ mode: "development", entry: { app: "./debug/src/a.js" }, devtool: "none", output: { path: Path. resolve(__dirname, "dist"), filename: "[name].[chunkhash].js"}, module: {rules: [// 前 {enforce: "pre", test: / \. Js $/, use: "Babel - loader"}, {/ / regular matching test: / \. M? Js $/, exclude: / (node_modules | bower_components) /, use: {loader: "Babel - loader", the options: {presets: [" @ Babel/preset - env "]}}}, / / rear {enforce: "post", the test: / \. Js $/, use: "babel-loader" } ] } };Copy the code

Where, A. js is the entry file configured in webPack Config. A. js depends on B. js/ C. js, while B. js depends on D. js and C. js depends on D. js/ B. js.

Finally compiled by Webpack, two chunk files will be generated, among which:

  • app.hash.jsContains thewebpack runtimeCode anda.js/b.js/d.jsModule code;
  • 0.bundle.hash.jsContains asynchronously loadedc.jsThe code.

The build process

Preparation before compilation

Executed first

const compiler = webpack(config);
Copy the code

Create a build process and pass in the configuration above. In the Webpack function, it is executed first

const webpackOptionsValidationErrors = validateSchema(
  webpackOptionsSchema,
  options
);
Copy the code

Do a type check on the incoming configuration and execute

if (Array.isArray(options)) { compiler = new MultiCompiler( Array.from(options).map(options => webpack(options)) ); } else if (typeof options === "object") { options = new WebpackOptionsDefaulter().process(options); compiler = new Compiler(options.context); / /... }Copy the code

If the configuration passed in is an array, multiple compilation processes are created. We’ll start with the case where the incoming configuration is an object. After performing

options = new WebpackOptionsDefaulter().process(options);
Copy the code

Merge the default system configuration and configuration files. Then, using the merged configuration, execute

compiler = new Compiler(options.context);
Copy the code

Create the Compiler object. The Compiler class defines the entire build flow and is an extension of Tapable, attaching a bunch of hooks done/ Runemit /seal, etc. After the Compiler object is created, execute

if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); }}}Copy the code

Register the plug-in in the configuration, and you can see that the Compiler instance is passed in when the plug-in is registered. The Compiler contains all the hooks in the build process, so the plug-in can register events in the hooks in the Compiler instance for processing. The compiler’s partial hooks pass in the compilation object parameters, which in turn contain many of the hooks that the resource builds.

Execute after registering the plug-in

compiler.options = new WebpackOptionsApply().process(options, compiler);
Copy the code

Register and activate some default plug-ins depending on the options configuration. The Compiler object is now created.

perform

compiler.run()
Copy the code

Start the compilation process. In the compiler.compile method, execute first

const params = this.newCompilationParams();
Copy the code

Initialize the parameters of the Compilation object

newCompilationParams() {
  const params = {
    normalModuleFactory: this.createNormalModuleFactory(),
    contextModuleFactory: this.createContextModuleFactory(),
    compilationDependencies: new Set()
  };
  return params;
}
Copy the code

The NormalModuleFactory class is used to create an instance of normalModule, and an instance of normalModule is a Module. It is executed in the new NormalModuleFactory

this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
Copy the code

This method formats the loader configuration in preparation for later module applications of loader.

And then execute it

const compilation = this.newCompilation(params);
Copy the code

This method instantiates a compilation object, which also extends to the Tapable class, and also mounts a bunch of hooks. A compilation object represents a build of a resource.

At this point, the preparation before compilation is complete. The compiler.hook. Make hook is called to start building the module.

Building blocks

Parsing the entry module from the compiler.addentry method.

Resolve the path

Starting with calling moduleFactory.create, the first step is to parse the path. Trigger normalModuleFactory. Hooks. Resolver hooks, execution

const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);
Copy the code

The result is two Resolver objects, where the resolver is an instance of the enhanced resolve mentioned earlier, which resolves absolute paths asynchronously. The Inline loader is then parsed

const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!" ); const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!" ); const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!" ); let elements = requestWithoutMatchResource .replace(/^-? ! +/, "") .replace(/!! +/g, "!" ) .split("!" );Copy the code

The absolute path of the inline loader and the current module is then resolved in parallel via asynclib. parallel.

asyncLib.parallel( [ callback => this.resolveRequestArray( contextInfo, context, elements, // inline loader loaderResolver, callback ), callback => { // ...... normalResolver.resolve( contextInfo, context, resource, {}, (err, resource, resourceResolveData) => { if (err) return callback(err); callback(null, { resourceResolveData, resource }); } ); } ], (err, results) => { // ...... })Copy the code

In the callback, get the completed inline loader and the absolute path to the current file, then continue parsing the laoder in the configuration, execute

const result = this.ruleSet.exec({ resource: resourcePath, realResource: matchResource ! == undefined ? resource.replace(/\? .*/, "") : resourcePath, resourceQuery, issuer: contextInfo.issuer, compiler: contextInfo.compiler });Copy the code

RuleSet here is the ruleSet object created in Constructor when new NormalModuleFactory is created. In the new RuleSet object, the loader configuration in the configuration has been formatted

{
  resource: function(),
  resourceQuery: function(),
  compiler: function(),
  issuer: function(),
  use: [
      {
          loader: string,
          options: string,
          <any>: <any>
      }
  ],
  rules: [<rule>],
  oneOf: [<rule>],
  <any>: <any>,
}
Copy the code

Where resource is a conditional function that can be executed to filter out the loader corresponding to the current module, use is loader and configuration. For example, in Demo:

The module: {rules: [/ / front {enforce: "pre", test: / \. Js $/, use: "Babel - loader"}, {/ / regular matching test: / \. M? Js $/, exclude: /(node_modules|bower_components)/, use: { loader: "babel-loader", options: { presets: "@ Babel/preset - env"}}}, / / rear {enforce: "post", the test: / \. Js $/, use: "Babel - loader"}]}Copy the code

After formatting isIn the implementationthis.ruleSet.execThen the corresponding of the current module can be obtainedloader, such asdemoIn the entry filea.js, the matching result after parsing is:

After the matching loader results are obtained, the path of the three sets of Loaders will be parsed in parallel

asyncLib.parallel( [ this.resolveRequestArray.bind( this, contextInfo, this.context, useLoadersPost, / / rear loader loaderResolver), enclosing resolveRequestArray. Bind (this, contextInfo, enclosing context, useLoaders, / / ordinary loader loaderResolver), enclosing resolveRequestArray. Bind (this, contextInfo, enclosing context, useLoadersPre, / / front loader loaderResolver)], (err, results) = > {/ /... })Copy the code

After obtaining the path, merge the inline, front-loader, normal, and post-loader sort.

  • normalloaderOrder of execution: pre -> normal -> Inline -> POST
  • Before the resource pathxxx! =!Decorations: pre -> Inline -> Normal -> POST
  • Before the resource path-!Decoration: Inline -> POST
  • Before the resource path!Decoration: pre -> Inline -> POST
  • Before the resource path!!!!!Adornment: inline

This gives you the loaders in the correct order to match the modules.

The resolve process ends at this point.

Compile the module

After the path is resolved, run

createdModule = new NormalModule(result);
Copy the code

Create a Module, each of which is a NormalModule object. Build the module from the normalModule.dobuild method. Executed first

const loaderContext = this.createLoaderContext(
  resolver,
  options,
  compilation,
  fs
);
Copy the code

Generate a context object for all loaders to use, and execute

runLoaders( { resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { // ...... })Copy the code

Pass the Module to loader for processing. The runloader here is a method from the Loader-Runner library above that executes the loaders in sequence. Get the module processed by loader in the callback and execute it

const result = this.parser.parse( this._ast || this._source.source(), { current: this, module: this, compilation: compilation, options: options }, (err, result) => { if (err) { handleParseError(err); } else { handleParseResult(result); }});Copy the code

Start parsing module. In the parse method, it is executed first

ast = Parser.parse(source, {
  sourceType: this.sourceType,
  onComment: comments
});
Copy the code

willmoduleConform toEstreeSpecification of theast, here parsingastIs the use of theacornLibrary. In order toa.jsFor example, convert toastAs shown in figure:

The next step is to traverse the tree, execute

if (this.hooks.program.call(ast, comments) === undefined) {
    this.detectMode(ast.body);
    this.prewalkStatements(ast.body);
    this.blockPrewalkStatements(ast.body);
    this.walkStatements(ast.body);
}
Copy the code

These lines of code process the AST line by line, resolving dependencies in the module.

In the enclosing hooks. The program. The call (ast, comments) this hook, will trigger HarmonyDetectionParserPlugin and UseStrictPlugin this two plug-ins callback.

  • HarmonyDetectionParserPluginIf there is one in the codeimportorexportOr of typejavascript/esmSo it’s going to increaseHarmonyCompatibilityDependencyHarmonyInitDependencyRely on.
  • UseStrictPluginUsed to check whether files existuse strictIf yes, add oneConstDependencyRely on.

In detectStrictMode, it checks if the current execution block has Use Strict, and if so, it is executed

this.scope.isStrict = true
Copy the code

The prewalkStatements function handles variables. Each node is iterated through, calling different handlers depending on statement.type. For example, the first node of A. js is importDeclaration, which processes this node, First trigger HarmonyImportDependencyParserPlugin plug-in, increase ConstDependency and HarmonyImportSideEffectDependency dependence. Then trigger HarmonyImportDependencyParserPlugin plug-ins, execution

parser.scope.renames.set(name, "imported var");
Copy the code

Set the value of Add to imported var.

The walkStatements step dives deeper into the function, making calls to the contents of the function

this.detectMode(expression.body.body);
this.prewalkStatement(expression.body);
this.walkStatement(expression.body);
Copy the code

Loop through dependencies in functions. For example, the third sentence import(‘./c’).then(del => del(1, 2)), which is an asynchronous loading dependency statement, is also a function call statement. The ImportParserPlugin plugin generates an ImportDependenciesBlock dependency and adds it to module.blocks.

// ImportParserPlugin const depBlock = new ImportDependenciesBlock( param.string, expr.range, Object.assign(groupOptions, { name: chunkName }), parser.state.module, expr.loc, parser.state.module ); / / parser. State. The current for the current processing module of the parser. State. The current. The addBlock (depBlock);Copy the code

When allastAfter being parsed, all dependencies of the current module are obtained. Common dependencies are saved inmodule.dependenciesAsynchronous dependencies are stored inmodule.blocksIn the. fora.jsFor example, the obtained dependencies are:

Then perform

this._initBuildHash(compilation);
Copy the code

Generate the hash value and store it in this._buildhash, where the module is compiled.

Go back to the callback, execute the afterBuild callback, and then call

compilation.processModuleDependencies(module, err => {
  if (err) return callback(err);
  callback(null, module);
});
Copy the code

Based on the resolved dependencies, the dependent modules are cyclically compiled.

All modules are now compiled. Eventually, all files are converted to modules, and module and dependencies are added to generate chunk and package files.

Generate the chunk

Go back to the compiler.hook. Make callback and execute the compiler.seal function. A large number of hooks are triggered in the SEAL method, providing a large number of hooks for hacking into the WebPack build process.

And then we start generating chunk. In this process, the entry module configured in config is first traversed and an empty chunk is created for each entry module. An entryPoint is then instantiated, which is a chunkGroup containing runtimeChunk. Each chunkGroup can contain as many chunks as possible.

And then call

GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);
Copy the code

Establish relationships among Module, Chunk, and Entrypoint.

And then call

buildChunkGraph(
  this,
  /** @type {Entrypoint[]} */ (this.chunkGroups.slice())
);
Copy the code

Establish relationships between chunk and other dependencies. In the buildChunkGraph method, three methods are called

// PART ONE
visitModules(
  compilation,
  inputChunkGroups,
  chunkGroupInfoMap,
  blockConnections,
  blocksWithNestedBlocks,
  allCreatedChunkGroups
);

// PART TWO
connectChunkGroups(
  blocksWithNestedBlocks,
  blockConnections,
  chunkGroupInfoMap
);

// Cleaup work
cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
Copy the code

In visitModules, this is done first

const blockInfoMap = extraceBlockInfoMap(compilation); ----------------------------------- const extraceBlockInfoMap = compilation => { // ...... For (const module of compile.modules) {// Loop each module blockQueue = [module]; currentModule = module; while (blockQueue.length > 0) { block = blockQueue.pop(); blockInfoModules = new Set(); blockInfoBlocks = []; / /... // For synchronous modules, {for (const dep of block.dependencies) iteratorDependency(dep); } if (block.blocks) {for (const b of block.blocks) iteratorBlockPrepare(b); } const blockInfo = { modules: blockInfoModules, blocks: blockInfoBlocks }; blockInfoMap.set(block, blockInfo); BlockInfoMap}} return blockInfoMap; };Copy the code

In this method, for allmoduleI’m going to iterate. I’m going to iteratemoduleThe process will be on thismoduledependenciesDependency to get thismoduleThe dependency module will handle this at the same timemoduleblocks(asynchronously loaded modules). The traversal process will set up the basicmodule graph, including ordinarymoduleAnd asynchronousmodule(block), and finally stored in amap(blockInfoMap) , represents the dependencies between modules. fordemo, the relationship diagram can be obtained:

At this point, we only get the dependency relationship between empty chunk and individual modules. Chunk and module are not associated yet. The next step is to add the associated dependencies to chunk. For example, a. Js, the key steps are:

  1. To deal witha.jsAnd geta.jsDependencies inconst blockInfo = blockInfoMap.get(block);
  2. Common modules for dependenciesb.js, if the currentchunkIf no, joinchunkAnd cache it. For asynchronous modulesc.js, it isc.jsCreate a newchunkAnd add to the loop;
  3. In the currentchunkIn, loop processingb.jsDependency in;
  4. In the newchunkIn, loop processingc.jsDependency in.

When the loop has done all the processingmoduleWhen,chunkThe graph is also generated fordemoIn terms of generationchunkFigure as follows

Then perform

connectChunkGroups(
  blocksWithNestedBlocks,
  blockConnections,
  chunkGroupInfoMap
);
Copy the code

Establish a father-son relationship between chunkGroup. There is no module in chunk2, which naturally has nothing to do with other chunks.

Then perform

cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
Copy the code

Clear empty chunkgroups that have no parent-child relationship.

Final generatedchunkThe graph is:

Generate the file

Generate a hash

After chunk is generated, the seal hook is called

this.createHash();
Copy the code

Generates the hash value. This method does two main things, generating a hash for module and a hash for chunk.

The generated code for the Module hash is as follows:

createHash() { //...... const modules = this.modules; for (let i = 0; i < modules.length; i++) { const module = modules[i]; const moduleHash = createHash(hashFunction); module.updateHash(moduleHash); module.hash = moduleHash.digest(hashDigest); module.renderedHash = module.hash.substr(0, hashDigestLength); } / /... }Copy the code

The key updateHash method is encapsulated in the implementation of each Module class. For normalModule, this method is:

updateHash(hash) {
  hash.update(this._buildHash);
  super.updateHash(hash);
}
Copy the code

Where _buildHash is the Module hash generated when the module is compiled. Super. The update code is

updateHash(hash) {
    hash.update(`${this.id}`);
    hash.update(JSON.stringify(this.usedExports));
    super.updateHash(hash);
}
Copy the code

You can see that module ID and exports information used are also updated into the hash. Super. The update code is

updateHash(hash) {
  for (const dep of this.dependencies) dep.updateHash(hash);
  for (const block of this.blocks) block.updateHash(hash);
  for (const variable of this.variables) variable.updateHash(hash);
}
Copy the code

What information is written to the hash for each dependency is determined by the updateHash method in xxxDependency. As you can see, a module hash contains:

  • eachmoduleIn its own unique need to writehashInformation in;
  • module idAnd what’s being usedexportsInformation;
  • Dependent information.

The chunk hash generation code is as follows:

createHash() { //...... for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; const chunkHash = createHash(hashFunction); try { // ...... chunk.updateHash(chunkHash); const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate; template.updateHashForChunk( chunkHash, chunk, this.moduleTemplates.javascript, this.dependencyTemplates ); this.hooks.chunkHash.call(chunk, chunkHash); chunk.hash = /** @type {string} */ (chunkHash.digest(hashDigest)); hash.update(chunk.hash); chunk.renderedHash = chunk.hash.substr(0, hashDigestLength); this.hooks.contentHash.call(chunk); } catch (err) { this.errors.push(new ChunkRenderError(chunk, "", err)); }} / /... }Copy the code

The first step is execution

chunk.updateHash(chunkHash); ------------------------------------------ updateHash(hash) { hash.update(`${this.id} `); hash.update(this.ids ? this.ids.join(",") : ""); hash.update(`${this.name || ""} `); for (const m of this._modules) { hash.update(m.hash); }}Copy the code

Writes hash information for id, IDS, name, and all modules it contains.

Then write the template information that generated chunk:

const template = chunk.hasRuntime()
                  ? this.mainTemplate
                  : this.chunkTemplate;
template.updateHashForChunk(
  chunkHash,
  chunk,
  this.moduleTemplates.javascript,
  this.dependencyTemplates
);
Copy the code

Webpack divides templates into two types: mainTemplate, which eventually generates code containing the Runtime, and chunkTemplate. We’ll focus on mainTemplate’s updateHashForChunk method

updateHashForChunk(hash, chunk, moduleTemplate, dependencyTemplates) { this.updateHash(hash); this.hooks.hashForChunk.call(hash, chunk); / /... } ---------------------------------------------------- updateHash(hash) { hash.update("maintemplate"); hash.update("3"); this.hooks.hash.call(hash); }Copy the code

The template type mainTemplate is written, and the triggered Hash hook and hashForChunk hook write the output of some files.

After storing all the relevant information into the hash buffer, call the digest method to generate the final hash, and then extract the required length from the hash. The hash of chunk is obtained.

this.fullHash = /** @type {string} */ (hash.digest(hashDigest));
this.hash = this.fullHash.substr(0, hashDigestLength);
Copy the code

Generate the file

Is called after the hash value is generated

this.createChunkAssets(); 
Copy the code

To determine what text content is ultimately exported to each chunk. The render template is mainTemplate or chunkTemplate, depending on whether chunk contains the WebPack Runtime code. In addition to generating common Module code, mainTemplate also includes generating Runtime code, and chunkTemplate is mainly used for generating common chunk code.

const template = chunk.hasRuntime()
                  ? this.mainTemplate
                  : this.chunkTemplate;
Copy the code

Then use getRenderManifest to get what render needs.

const manifest = template.getRenderManifest({
  chunk,
  hash: this.hash,
  fullHash: this.fullHash,
  outputOptions,
  moduleTemplates: this.moduleTemplates,
  dependencyTemplates: this.dependencyTemplates
}); 
Copy the code

Then perform

source = fileManifest.render();
Copy the code

Used to generate code. Take the chunk of the demo portal as an example. Since the chunk of the demo portal contains runtime code, mainTemplate is used, and the code generation process of mainTemplate includes generating common Module code. So let’s use mainTemplate as an example to illustrate the code generation process:

// mainTemplate manifest render: () => compilation.mainTemplate.render( hash, chunk, moduleTemplates.javascript, dependencyTemplates ), -------------- render(hash, chunk, moduleTemplate, DependencyTemplates) {const buf = this.renderBootstrap(Hash, chunk, moduleTemplate) const buf = this.renderBootstrap(hash, chunk, moduleTemplate) dependencyTemplates ); // The method registered in MainTemplate wraps the Runtime code, Let source = this.hooks. Render. Call (new OriginalSource(template.prefix (buf, " \t") + "\n", "webpack/bootstrap" ), chunk, hash, moduleTemplate, dependencyTemplates ); / /... return new ConcatSource(source, ";" ); }Copy the code

After calling hooks. Render, we get the chunk containing the Runtime bootstrap code, and finally return an instance of the ConcatSource type. The final code is saved in the Children of the ConcatSource class.

Let’s focus on how the module code is generated. In this.links.render. Call, execute this.links.modules. Call to generate module code

this.hooks.render.tap( "MainTemplate", (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => { const source = new ConcatSource(); / /... source.add( this.hooks.modules.call( new RawSource(""), chunk, hash, moduleTemplate, dependencyTemplates ) ); source.add(")"); return source; });Copy the code

This hook is registered in JavascriptModulesPlugin, execution of the Template. RenderChunkModules method:

compilation.mainTemplate.hooks.modules.tap( "JavascriptModulesPlugin", (source, chunk, hash, moduleTemplate, dependencyTemplates) => { return Template.renderChunkModules( chunk, m => typeof m.source === "function", moduleTemplate, dependencyTemplates, "/******/ " ); }); -------------------------------------------------------- static renderChunkModules( chunk, filterFn, moduleTemplate, dependencyTemplates, prefix = "" ) { // ...... const allModules = modules.map(module => { return { id: module.id, source: moduleTemplate.render(module, dependencyTemplates, { chunk }) }; }); / /... return source; } --------------------------------- render(module, dependencyTemplates, options) { try { const moduleSource = module.source( dependencyTemplates, this.runtimeTemplate, this.type ); const moduleSourcePostContent = this.hooks.content.call( moduleSource, module, options, dependencyTemplates ); const moduleSourcePostModule = this.hooks.module.call( moduleSourcePostContent, module, options, dependencyTemplates ); const moduleSourcePostRender = this.hooks.render.call( moduleSourcePostModule, module, options, dependencyTemplates ); return this.hooks.package.call( moduleSourcePostRender, module, options, dependencyTemplates ); } catch (e) { e.message = `${module.identifier()}\n${e.message}`; throw e; }}Copy the code

In module.source, execute the this.generator.generate method:

source(dependencyTemplates, runtimeTemplate, type = "javascript") { // ...... const source = this.generator.generate( this, dependencyTemplates, runtimeTemplate, type ); / /... return cachedSource; }Copy the code

The Generator here is created during the NormalModuleFactory creation of the NormalModule. Execute this.sourceBlock in generate:

generate(module, dependencyTemplates, runtimeTemplate) {
  // ......
  this.sourceBlock(
    module,
    module,
    [],
    dependencyTemplates,
    source,
    runtimeTemplate
  );

  return source;
}
Copy the code

In sourceBlock, the dependency is looped first

sourceBlock( module, block, availableVars, dependencyTemplates, source, runtimeTemplate ) { for (const dependency of block.dependencies) { this.sourceDependency( dependency, dependencyTemplates, source, runtimeTemplate ); } / /... } ------------------------------ sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate) { const template = dependencyTemplates.get(dependency.constructor); / /... template.apply(dependency, source, runtimeTemplate, dependencyTemplates); }Copy the code

That is, implement the Apply method in dependency one by one. For a. s., the first is dependent on HarmonyCompatibilityDependency, it is the apply method

apply(dep, source, runtime) { const usedExports = dep.originModule.usedExports; if (usedExports ! == false && ! Array.isArray(usedExports)) { const content = runtime.defineEsModuleFlagStatement({ exportsArgument: dep.originModule.exportsArgument }); source.insert(-10, content); }}Copy the code

Insert __webpack_require__.r(__webpack_exports__); . The __webpack_require__.r method adds an __esModule attribute to the __webpack_exports__ object, identifying it as an ES module.

Then there is the harmonyInit Dependency, whose Apply method traverses all dependencies and executes the Dependency’s template.Harmonyinit method. In this process, A.js code

import add from './b.js'
Copy the code

In the corresponding HarmonyImportSideEffectDependency and HarmonyImportSpecifierDependency template. HarmonyInit method will be performed at this moment, and then get the following

/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b.js */ "./debug/src/b.js");
Copy the code

ConstDependency and HarmonyImportDependency do nothing here.

Then the HarmonyImportSpecifierDependency, get it

Object(_b_js__WEBPACK_IMPORTED_MODULE_0__["default"])
Copy the code

The corresponding

add()
Copy the code

Add in this sentence. This completes the dependency.

We then loop through the asynchronous module in sourceBlock:

sourceBlock( module, block, availableVars, dependencyTemplates, source, runtimeTemplate ) { // ...... for (const childBlock of block.blocks) { this.sourceBlock( module, childBlock, availableVars.concat(vars), dependencyTemplates, source, runtimeTemplate ); } / /... }Copy the code

As you can see, processing asynchronous modules takes the dependencies out of blocks and calls the sourceBlock loop.

When the module is parsed, the resulting ReplaceSource object contains an array replacements containing the source code conversion operations. Each element in the array has the following structure:

[Start position of replacement source, end position of replacement source, end content of replacement, priority]Copy the code

At this point, the operations on the source code are available. Then perform

const moduleSourcePostRender = this.hooks.render.call(
  moduleSourcePostModule,
  module,
  options,
  dependencyTemplates
);
Copy the code

The main job of this hook function is to wrap the module code that has been completed above. The package content is mainly a module loading system of Webpack itself, including module import and export, etc. The final generated form of each module code is as follows:

/***/ (function(module, __webpack_exports__, __webpack_require__) {// Module generates the code wrapped inside the function /***/})Copy the code

Then perform

return this.hooks.package.call(
  moduleSourcePostRender,
  module,
  options,
  dependencyTemplates
);
Copy the code

This hook is used to add comments.

At this point, the wrapped code for a Module is complete.

RenderChunkModules = renderChunkModules = renderChunkModules = renderChunkModules = renderChunkModules = renderChunkModules

{ /***/ "./debug/src/a.js": /*! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** (webpack)/debug/src/a.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / / *! No exports provided */ /***/ (function(module, __webpack_exports__, __webpack_require__) {// Module contents...... /***/ }), /***/ "./debug/src/b.js": /*! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** (webpack)/debug/src/b.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / / *! Exports provided: default */ /***/ (function(module, __webpack_exports__, __webpack_require__) {// Module contents...... /***/ }), /***/ "./debug/src/d.js": /*! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** (webpack)/debug/src/d.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / / *! Exports provided: default */ /***/ (function(module, __webpack_exports__, __webpack_require__) {// Module contents...... / /}) / * * * * * * * * * /}Copy the code

At this point, all the code is complete.

The output file

After going through all of the above phases, execute emitAssets to save the generated source in Assets. Then go back to compiler and call

this.outputFileSystem.mkdirp(outputPath, emitFiles);
Copy the code

Concatenate the files in the emitFiles method, get the file path, file name, and write the file to the output directory.

reference

Webpack core module Tapable parsing

The use and principle analysis of Webpack core library Tapable

Webpack with Rollup behind Acorn

Webpack 4 source code main process analysis

An overview of the Webpack series

Webpack source code analysis