0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

webpack原理分析

wangyang
发布于: 2021-08-26 17:30:54
结合webpack源码讲述其工作原理

1. 概览

该篇文章主要讲的是根据案例webpack源码的执行主流程,一共分为几个阶段来讲述,然后每个阶段大致做了什么,怎么做的,从而达到我们在实际应用中我们可以做什么事。

2. tapable模块

直接讲干货;这是关于webpack5.51.1的分析; 首先了解tapable这个模块;说下为什么:因为webpack是一个以多个插件组合(包括自定义插件)形成的流水线的工具,插件是怎样被注入到webpack里的呢,就是通过tapable这个模块实现的,通俗一点就是发布订阅的模式;在初始化插件时会调用tap方法,然后会把tap方法里的回调存在队列里,然后在webpack的某个阶段触发刚刚加入tap时的方法。从而达到了不同的阶段做该阶段的事;举个简单的例子(不要在意一些细节的判断):

let tapable= {
  event:{},
  _tap: (type, fn)=>{
    this.event[type].push(fn)
  },
  _call: (type,...arg)=>{
    this.event[type].foreach(fn=>fn(args))
  }
}

// 注入插件: 
class plugins{
  constructor(){
    tapable._tap('type', (arg)=>{console.log('回调执行',arg)})
  }
}

// 触发插件:  
new plugins();
tapable._call(111);

小结:tapable工作原理大致是上述这样,后面我们会跟着源码讲解tapable模块,我们了解了插件在webpack的工作原理,接下来我正式进入webpack源码讲解;后面根据webpack的各个阶段进行讲解:一共大致分为init->run->make->seal ->emit等主流程


前序工作和配置(webpack5.51.1、webpack-cli4.8.0):

webpack.config.js

const path = require('path');
import plugin from './plugin';
module.exports = {
  mode: "production", // "production" | "development" | "none"  // 告诉webpack是生产环境还是开发环境.
  entry:  path.resolve(__dirname, "../main.js"), // string | object | array  // 默认 ./src
  // 入口起点,可以指定多个入口起点
  output: {
    // 输出,只可指定一个输出配置
    path: path.resolve(__dirname, "dist"), // string
    //  所有输出文件所在的目录
    filename: "bundle.js", // string    // 输出文件的名称
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use:[{
          loader: path.resolve(__dirname, './loader'),
        }],
      },
    ]
  },
  plugins:[
    new plugin(),
  ]
}

main.js

import './index';
console.log('main');

index.js

console.log('index')

3. init阶段

首先打开webpack源码webpack.js:看到wepack是一个接受option和callback俩个参数的function;首先调用create方法创建启动编译的对象compiler;通过new Compiler()创建;然后通过compiler.run方法开始启动;

image.png)


4. run阶段

run阶段以为正式进入编译阶段,这个阶段做了什么?接下来我们继续看源码,打开compiler模块我们看到run方法; image.png) 上图可以看到在run方法里定义了onCompiledrun俩个方法,onCompiled通过命名可以猜测是编译完成调用的方法,那么我先看run方法做了什么?首先看下this.hooks.beforeRun.callAsync()这个方法,同样看下图

image.png)

image.png)

上图可以看出,该方法来自文章最开始提及的核心模块tapable,我想大家应该能猜测出这一步干了啥?this.hooks.beforeRun的值是AsyncSeriesHook的实例;打开AsyncSeriesHook模块,一看源码发现,相当于AsyncSeriesHook继承Hook,所以this.hooks.beforeRun.callAsync就是调用Hook模块里的callAsync方法,接下来我们看hook模块,hook模块的callAsync方法是由this._createCall("async")得到,这个方法最终会返回一个函数function

(function anonymous(compiler, _callback
) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _hasError0 = false;
try {
_fn0(compiler);
} catch(_err) {
_hasError0 = true;
_callback(_err);
}
if(!_hasError0) {
_callback();
}

})
表达式说明: compiler此时就是最开始调用时传入的this,callback就是回调;
_x这个属性值就是上文说的一些插件使用tap方法注册的回调,这里目前只有一个地方使用了tap方法,
所以只有_x[0],如果是多个地方使用了,那么生成的代码的就是再追加一个try的逻辑;

image.png) 接下来 执行this.callAsync这个函数就是执行上述function;那么执行try时就会执行到之前tap注册时的回调函数;这就是插件的原理;我们在后续调用call hooks的时候都可以自定义插件来处理系统的一些问题;比如编译之前copy一些文件等;说到这里我想大家已经很明白我最开始所说的发布订阅了,接下来我们继续讲流程(流程异常的情况不讲); 执行run流程注册的回调,接下来我们看this.compile()这个流程,进入到该模块的compiler方法:

image.png)

1、 首先new newCompilationParams()初始化; 会得到有俩个重要的属性分别是normalModuleFactor和contextModuleFactory的对象;首先normalModuleFactor属性值是一个继承于ModuleFactory对象的NormalModuleFactory`的一个工厂生成的;normalModuleFactor同样也有自己的hook值等;

const NormalModuleFactory = {
cachePredicate:() => true,
ruleSet:{references: Map(0), exec: ƒ},
resolverFactory:ResolverFactory {hooks: {…}, cache: Map(0)},
context:'C:\\Users\\16422\\Desktop\\test',
fs:CachedInputFileSystem {fileSystem: {…}, _lstatBackend: CacheBackend, lstat: ƒ, lstatSync: ƒ, _statBackend: CacheBackend, …},
generatorCache:Map(0),
hooks:{resolve: Hook, resolveForScheme: HookMap, factorize: Hook, beforeResolve: Hook, afterResolve: Hook, …},
parserCache:Map(0),
}

这些属性先不做过多解释,后面调用再做详细解释;大致也能猜到将会处理我们业务代码中的依赖;contextModuleFactory这属性也同样如此;到此很浅的分析完了new newCompilationParams();

2、接下来继续执行const compilation = this.newCompilation(params);这句代码,compilation 是一个很重要的对象,compilation 是编译过程中的对象,后续module、chunk、assets等都挂到这个对象上;接下来看下this.newCompilation()方法,这方法创建一个这样的对象:

{
buildQueue:AsyncQueue {_name: 'build', _parallelism: 100, _processor: ƒ, _getKey: ƒ, _entries: Map(0), …}
buildDependencies:LazySet {_set: Set(0), _toMerge: Set(0), _toDeepMerge: Array(0), _needMerge: false, _deopt: false}
bail:false
_assetsCache:CacheFacade {_cache: Cache, _name: 'Compilation/assets'}
_assetsRelatedIn:Map(0)
_codeGenerationCache:CacheFacade {_cache: Cache, _name: 'Compilation/codeGeneration'}
_modules:Map(0)
_modulesCache:CacheFacade {_cache: Cache, _name: 'Compilation/modules'}
_rebuildingModules:Map(0)
additionalChunkAssets:(0) []
addModuleQueue:AsyncQueue {_name: 'addModule', _parallelism: 100, _processor: ƒ, _getKey: ƒ, _entries: Map(0), …}
assets:{}
assetsInfo:Map(0)
asyncEntrypoints:(0) []
builtModules:WeakSet
asyncEntrypoints:(0) []
bail:false
buildDependencies:LazySet {_set: Set(0), _toMerge: Set(0), _toDeepMerge: Array(0), _needMerge: false, _deopt: false}
buildQueue:AsyncQueue {_name: 'build', _parallelism: 100, _processor: ƒ, _getKey: ƒ, _entries: Map(0), …}
cache (get):ƒ deprecated(...args) {\n    if (!warned) {\n      warned = true;\n      if (code !== undefined) {\n        if (!codesWarned[code]) {\n          process.emitWarning(msg, 'DeprecationWarning', code, deprecated);\n          codesWarned[code] = true;\n        }\n      } else {\n        process.emitWarning(msg, 'DeprecationWarning', deprecated);\n      }\n    }\n    if (new.target) {\n      return Reflect.construct(fn, args, new.target);\n    }\n    return fn.apply(this, args);\n  }
cache (set):ƒ deprecated(...args) {\n    if (!warned) {\n      warned = true;\n      if (code !== undefined) {\n        if (!codesWarned[code]) {\n          process.emitWarning(msg, 'DeprecationWarning', code, deprecated);\n          codesWarned[code] = true;\n        }\n      } else {\n        process.emitWarning(msg, 'DeprecationWarning', deprecated);\n      }\n    }\n    if (new.target) {\n      return Reflect.construct(fn, args, new.target);\n    }\n    return fn.apply(this, args);\n  }
children:(0) []
childrenCounters:{}
chunkGraph:undefined
chunkGroups:(0) []
chunks:Set(0) {concat: ƒ, entry: ƒ, filter: ƒ, find: ƒ, findIndex: ƒ, …}
chunkTemplate:ChunkTemplate {_outputOptions: {…}, hooks: {…}}
codeGeneratedModules:WeakSet
codeGenerationResults:undefined
comparedForEmitAssets:Set(0)
compilationDependencies:{add: ƒ}
compiler:Compiler {hooks: {…}, webpack: ƒ, name: undefined, parentCompilation: undefined, root: Compiler, …}
compilerPath:''
contextDependencies:LazySet {_set: Set(0), _toMerge: Set(0), _toDeepMerge: Array(0), _needMerge: false, _deopt: false}
creatingModuleDuringBuild:WeakMap
dependencyFactories:Map(26) {class … => NormalModuleFacto… …, class … => NormalModuleFacto… …, class … => NormalModuleFacto… …, class … => NormalModuleFacto… …, class … => NormalModuleFacto… …, …}
dependencyTemplates:DependencyTemplates {_map: Map(42), _hash: '31d6cfe0d16ae931b73c59d7e0c089c0'}
emittedAssets:Set(0)
endTime:undefined
entries:Map(0)
entrypoints:Map(0)
errors:(0) []
factorizeQueue:AsyncQueue {_name: 'factorize', _parallelism: 100, _processor: ƒ, _getKey: ƒ, _entries: Map(0), …}
fileDependencies:LazySet {_set: Set(0), _toMerge: Set(0), _toDeepMerge: Array(0), _needMerge: false, _deopt: false}
fileSystemInfo:FileSystemInfo {fs: CachedInputFileSystem, logger: WebpackLogger, _remainingLogs: 40, _loggedPaths: Set(0), _snapshotCache: WeakMap, …}
globalEntry:{dependencies: Array(0), includeDependencies: Array(0), options: {…}}
hooks:{buildModule: Hook, rebuildModule: Hook, failedModule: Hook, succeedModule: Hook, stillValidModule: Hook, …}
inputFileSystem:CachedInputFileSystem {fileSystem: {…}, _lstatBackend: CacheBackend, lstat: ƒ, lstatSync: ƒ, _statBackend: CacheBackend, …}
logger:WebpackLogger {getChildLogger: ƒ, Symbol(webpack logger raw log method): ƒ}
logging:Map(0)
mainTemplate:MainTemplate {_outputOptions: {…}, hooks: {…}, renderCurrentHashCode: ƒ, getPublicPath: ƒ, getAssetPath: ƒ, …}
missingDependencies:LazySet {_set: Set(0), _toMerge: Set(0), _toDeepMerge: Array(0), _needMerge: false, _deopt: false}
moduleGraph:ModuleGraph {_dependencyMap: Map(0), _moduleMap: Map(0), _originMap: Map(0), _metaMap: Map(0), _cacheModuleGraphModuleKey1: undefined, …}
modules:Set(0) {concat: ƒ, entry: ƒ, filter: ƒ, find: ƒ, findIndex: ƒ, …}
moduleTemplates:{javascript: ModuleTemplate, asset: <accessor>, webassembly: <accessor>}
name:undefined
namedChunkGroups:Map(0)
namedChunks:Map(0)
needAdditionalPass:false
options:{amd: undefined, bail: undefined, cache: false, context: 'C:\\Users\\16422\\Desktop\\test', dependencies: undefined, …}
outputOptions:{assetModuleFilename: '[hash][ext][query]', charset: true, chunkFilename: '[id].bundle.js', chunkFormat: 'array-push', chunkLoading: 'jsonp', …}
processDependenciesQueue:AsyncQueue {_name: 'processDependencies', _parallelism: 100, _processor: ƒ, _getKey: ƒ, _entries: Map(0), …}
profile:false
rebuildQueue:AsyncQueue {_name: 'rebuild', _parallelism: 100, _processor: ƒ, _getKey: ƒ, _entries: Map(0), …}
records:{}
requestShortener:RequestShortener {contextify: ƒ}
resolverFactory:ResolverFactory {hooks: {…}, cache: Map(0)}
runtimeTemplate:RuntimeTemplate {outputOptions: {…}, requestShortener: RequestShortener}
startTime:undefined
usedChunkIds:null
usedModuleIds:null
}

后续就是对该对象进行处理;完善其属性值,然后正式进入编译阶段……


5. make阶段

5.1 建立入口module

首先执行this.hooks.make.callAsync();调用EntryPlugin插件的回调;

image.png)

回调执行compilation.addEntry()传入三个参数context、dep、options; 1、context是工程webpack的绝对路径; 2、dep是由本模块初始化时执行EntryPlugin.createDependency(entry, options)产生的对象,entry为webpackconfig配置的entry的值;执行该方法实际是调用 Dependency模块的constuctor来创建一个EntryDependency实例,包含属性为

entryDependency{
 loc:_loc:{name: 'main'}
_parentModule:undefined
_parentDependenciesBlock:undefined
_locSL:0
_locSC:0
_locN:'main'
_locI:undefined
_locEL:0
assertions:undefined
optional:false
range:undefined
request: // 模块的绝对路径
userRequest: // 模块的绝对路径
weak:false
}

3、options就是调用webpack方法传入的第一个参数经过处理得到的一个对象

{
chunkLoading:undefined
dependOn:undefined
filename:undefined
layer:undefined
library:undefined
name:'main'
publicPath:undefined
runtime:undefined
wasmLoading:undefined
}

三个参数分析完了,回到compilation.addEntry()方法,实际调用的compliation模块的_addEntryItem方法;我们阅读这些this._addModuleTree->this.addModuleTree->this.handleModuleCreation->this.factorizeModule()->this.factorizeQueue.add()流程;实际上是添加entry到factorizeQueue队列里,这里factorizeQueue实际是new AsyncQueue({ name: "build", parent: this.factorizeQueue,processor: this._buildModule.bind(this)})生成,调用add实际就是asyncQuen实例的add;那么最终会形成 asyncQuen._queued这个数组里对应着entry;然后通过Immediate这个Api来调用_queued队列;

image.png)

如图等待node执行Immediate该方法是就会调用root._ensureProcessing; root是asyncQuen的_root属性;

image.png)

这些代码的含义实际就是为了使asyncQuen实例形成一个树形结构;_root永远都是整个树的根数据,和vue-roter源码类似;

{
_children: [AsyncQueue]
0:AsyncQueue {
_activeTasks:0
_children:undefined
_ensureProcessing:ƒ ()
_entries_entries:Map(1) {C:\\Users\\1…k\\main.js => AsyncQueueEntry …}
   size (get):ƒ size()
  [[Entries]]:Array(1)
   0:{"C:\\Users\\16422\\Desktop\\webpack\\main.js" => AsyncQueueEntry}
   key:'C:\\Users\\16422\\Desktop\\webpack\\main.js'
   value:AsyncQueueEntry {
    callback:(err, module) => {…} // 调用this.addModule方法的回调
   callbacks:undefined
   error:undefined
   item:NormalModule {dependencies: Array(0), blocks: Array(0), type: 'javascript/auto', context: 
  'C:\\Users\\16422\\Desktop\\webpack', layer: null, …}
   result:undefined
}
_name:'addModule'
_needProcessing:false
_parallelism:1
_processor:ƒ ()
_queued:ArrayQueue {_list: Array(1), _listReversed: Array(0)}
}
1:AsyncQueue {_name: 'factorize', _parallelism: 1, _processor: ƒ, _getKey: ƒ, _entries: Map(1), …}
2:AsyncQueue {_name: 'build', _parallelism: 1, _processor: ƒ, _getKey: ƒ, _entries: Map(0), …}
length:3   ]
}

在factorizeQueue这个实例里可以通过parent访问到addModuleQueue实例,这里的顺序processDependenciesQueue._children=addModuleQueue._children=factorizeQueue._children=buildQueue;这个对应关系清楚后;root._ensureProcessing这个方法就会遍历执行__queue队列和_children的队列;执行队列里的每一项实际是执行的什么呢?

image.png)

从图中可以看出执行的是this._processor这个方法,这个方法前面可以知道是绑定的this._factorizeModule这个方法,最后又进入到buildModule的过程了,下面就是AsyncQueue 实例的结构

{
_activeTasks:1
_children:(3) [AsyncQueue, AsyncQueue, AsyncQueue]
_ensureProcessing:ƒ ()
_entries:Map(0)
_getKey:item => /** @type {any} */ (item)
_name:'processDependencies'
_needProcessing:true
_parallelism:100
_processor:ƒ ()
_queued:ArrayQueue {_list: Array(0), _listReversed: Array(0)}
_root:AsyncQueue {_name: 'processDependencies', _parallelism: 100, _processor: ƒ, _getKey: ƒ, _entries: Map(0), …}
_stopped:false
_willEnsureProcessing:false
}

继续调用this._processor()方法,会根据entry入口构建module,回调里调用this.__handleResult()方法处理module,会调用ewntry.calback,也就是构建AsyncQuen时传入的callback,也就是this.factorizeModule的回调; 执行完__handleResult方法回继续调用this.addModule(newModule);newModule就是一个含有main.js路径的normalModule对象;该方法就是把创建的newModule加入到this.addModule实例里的_queued队列;和this.factorizeQueue一样的数据结构;同样的执行和this.factorizeQueue逻辑。

小结:这里也是一个订阅和发布的模式,new AsyncQueue()方法进行订阅,setImmediate(root._ensureProcessing)进行发布。

image.png)

晓得上述大概流程的执行方式后,执行完addModule订阅AsyncQueue后会订阅buildModule的AsyncQueue,继续执行该订阅会调用_buildModule方法里的module.build();该方法调用NormalModule实例的build方法,执行this.doBuild();然后执行 runLoaders;为甚么要说下这个方法,这个方法涉及平时我们配置的loader;该方法在runLoaders模块里;runLoaders方法接受来个参数一个是options,一个是callback;

options:{
context:{version: 2, getOptions: ƒ, emitWarning: ƒ, emitError: ƒ, getLogger: ƒ, …}
loaders:(1) [{…}]
0:{loader: 'C:\\Users\\16422\\Desktop\\webpack\\build\\loader.js', options: undefined, ident: undefined}
ident:undefined
loader:'C:\\Users\\16422\\Desktop\\webpack\\build\\loader.js'
options:undefined
__proto__:Object
length:1
__proto__:Array(0)
processResource:(loaderContext, resourcePath, callback) => {\n\t\t\t\t\tconst resource = loaderContext.resource;\n\t\t\t\t\tconst scheme = getScheme(resource);\n\t\t\t\t\tif (scheme) {\n\t\t\t\t\t\thooks.readResourceForScheme\n\t\t\t\t\t\t\t.for(scheme)\n\t\t\t\t\t\t\t.callAsync(resource, this, (err, result) => {\n\t\t\t\t\t\t\t\tif (err) return callback(err);\n\t\t\t\t\t\t\t\tif (typeof result !== "string" && !result) {\n\t\t\t\t\t\t\t\t\treturn callback(new UnhandledSchemeError(scheme, resource));\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn callback(null, result);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tloaderContext.addDependency(resourcePath);\n\t\t\t\t\t\tfs.readFile(resourcePath, callback);\n\t\t\t\t\t}\n\t\t\t\t}
resource:'C:\\Users\\16422\\Desktop\\webpack\\main.js'
}

可以看到loaders属性值就是我最开始工程目录配置的l自定义loader;继续阅读到iteratePitchingLoaders 方法,进入该方法阅读到loadLoader;通过module = require(loader.path);获取到我写的自定义loader模块;然后进入teratePitchingLoaders方法;调用processResource()再调用options.processResource(loaderContext, resourcePath, function(err, buffer) buffer就是main.js文件的内容;再调用runSyncOrAsync(fn, loaderContext, args);fn就是自定义loader方法; image.png) 图片最后一句代码fn(context);将会执行loader.js的方法;context就是main.js的内容,所以自定义loader方法接受的第一个参数就是loader正则匹配的文件的内容。

小结:上述流程完成了loader对文件的转换;所以这个流程阶段我们可以自定义改变文件的内容。

接下来我们思考一个问题?怎样进行下一个依赖index.js进行分析,我们大胆试想一下,是否是需要对main.js进行语法分析,然后再生成moudle依赖;

5.2 分析依赖

执行buildModule订阅的AsyncQueue时,执行是绑定的__buildModule方法;开始创建模块,然后执行module.build调用的是normalModule模块里的build;在this.doBuild方法里获取main.js的source作为参数通过this.parser.parse()方法进行语法分析;在JavascriptParser模块里调用parse方法;得到main.js的语法树ast;

body:(3) [Node, Node, Node]
Node {type: 'ImportDeclaration', start: 0, end: 20, loc: SourceLocation, range: Array(2), …}
end:20
loc:SourceLocation {start: Position, end: Position}
range:(2) [0, 20]
source:Node {type: 'Literal', start: 7, end: 19, loc: SourceLocation, range: Array(2), …}
specifiers:(0) []
start:0
type:'ImportDeclaration'
__proto__:Object
1:Node {type: 'ExpressionStatement', start: 22, end: 36, loc: SourceLocation, range: Array(2), …}
2:Node {type: 'ExpressionStatement', start: 38, end: 58, loc: SourceLocation, range: Array(2), …}
length:3
__proto__:Array(0)
end:58

然后调用this.blockPreWalkStatements()进行语法树的分析;通过语法的type字段为ImportDeclaration来调用this.blockPreWalkImportDeclaration(statement),在方法里调用this.hooks.import.call(statement, source)触发HarmonyImportDependencyParserPlugin模块里的hooks;通过new HarmonyImportSideEffectDependency()创建依赖;最后加入到module的dependences队列里。 接下来进入到this.buildModule方法的回调里,可以看到会订阅processDependenciesQueue的AsyncQueue,然后执行这个订阅,这个订阅又会执行factorizeModule订阅;重复执行factorizeModule订阅->addmodule订阅->buildModule订阅->processDependenciesQueue订阅这个流程; 就会把创建依赖图谱执行完;最后会把生成module挂到compilation的_modules属性上

_modules:Map(2) {C:\\Users\\1…k\\main.js => NormalModule {…}, C:\\Users\\1…\\index.js => NormalModule {…}}
size (get):ƒ size()
[[Entries]]:Array(2)
0:{"C:\\Users\\16422\\Desktop\\webpack\\build\\loader.js!C:\\Users\\16422\\Desktop\\webpack\\main.js" => NormalModule}
key:'C:\\Users\\16422\\Desktop\\webpack\\build\\loader.js!C:\\Users\\16422\\Desktop\\webpack\\main.js'
value:NormalModule {dependencies: Array(1), blocks: Array(0), type: 'javascript/auto', context: 'C:\\Users\\16422\\Desktop\\webpack', layer: null, …}
1:{"C:\\Users\\16422\\Desktop\\webpack\\build\\loader.js!C:\\Users\\16422\\Desktop\\webpack\\index.js" => NormalModule}
length:2

类似这样的结构,到此依赖分析完成,并且make阶段也完成。

小结:make阶段进行了loader的解析、依赖分析生成依赖图谱、模块的hash值的生成等;生成modules;


6. seal 阶段

说哈大致流程: 1、通过module生成chunk 2、通过module、chunk生成assets 3、最后把assets输出到目录;

make阶段结束后进入compilation.seal方法;阅读源码发现触发了一系列的hooks,其中包括压缩等;然后通过this.entries生成chunk;看下chunk的结构:

{
groups:Set(0) {_sortFn: ƒ, _lastActiveSortFn: Symbol(not sorted), _cache: undefined, _cacheOrderIndependent: undefined}
auxiliaryFiles:Set(0)
files:Set(0)
filenameTemplate:undefined
chunkReason:undefined
contentHash:{}
debugId:1000
entryModule (get):ƒ entryModule() {\n\t\tconst entryModules = Array.from(\n\t\t\tChunkGraph.getChunkGraphForChunk(\n\t\t\t\tthis,\n\t\t\t\t"Chunk.entryModule",\n\t\t\t\t"DEP_WEBPACK_CHUNK_ENTRY_MODULE"\n\t\t\t).getChunkEntryModulesIterable(this)\n\t\t);\n\t\tif (entryModules.length === 0) {\n\t\t\treturn undefined;\n\t\t} else if (entryModules.length === 1) {\n\t\t\treturn entryModules[0];\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t"Module.entryModule: Multiple entry modules are not supported by the deprecated API (Use the new ChunkGroup API)"\n\t\t\t);\n\t\t}\n\t}
extraAsync:false
hash:undefined
groupsIterable (get):ƒ groupsIterable() {\n\t\tthis._groups.sort();\n\t\treturn this._groups;\n\t}
files:Set(0)
modulesIterable (get):ƒ modulesIterable() {\n\t\tconst chunkGraph = ChunkGraph.getChunkGraphForChunk(\n\t\t\tthis,\n\t\t\t"Chunk.modulesIterable",\n\t\t\t"DEP_WEBPACK_CHUNK_MODULES_ITERABLE"\n\t\t);\n\t\treturn chunkGraph.getOrderedChunkModulesIterable(\n\t\t\tthis,\n\t\t\tcompareModulesByIdentifier\n\t\t);\n\t}
ids:null
idNameHints:Set(0) {_sortFn: undefined, _lastActiveSortFn: Symbol(not sorted), _cache: undefined, _cacheOrderIndependent: undefined}
id:null
hash:undefined
groupsIterable (get):ƒ groupsIterable() {\n\t\tthis._groups.sort();\n\t\treturn this._groups;\n\t}
files:Set(0)
filenameTemplate:undefined
extraAsync:false
name:'main'
preventIntegration:false
rendered:false
renderedHash:undefined
runtime:undefined
}

6.1 处理chunk和module得到 codeGenerationResults值:

接下来利用生成的chunk来建立chunkGraph、chunkGroups并挂到compilation上,我们最终的目的是要把模块的代码进行输出;看看是怎么进行处理得到我们想要的code;通过connectChunkGroupAndChunk这个方法把chunk与ChunkGroup关联起来,ChunkGroup的chunk属性里维护着chunk的队列,通过chunkGraph.connectChunkAndEntryModule方法建立chunk和module的关系,chunkGraph的chunk维护着chunk,module维护着module队列;接下来调用this.codeGeneration,该方法会对chunkGraph.module进行操作生成一个包含module的队列;同时也会初始化codeGenerationResults挂到compilation对象上results属性里,接下来继续对module进行处理;通过this.codeGenerationModule这个方法是把module经过分析处理后的结果保存在results中;那么module是怎么被处理的?首先是被concatenatedModule.codeGeneration处理一些需要输出的code,类似:"webpack_require"这样的code;还处理了作用域和遍历分析每个module,处理分析后的ast;并返回处理后的结果给results;怎么分析每个module?首先对module的codeGeneration方法获取module里的代码块;然后对代码块处理后放入到_children 数组里,最终数据可以在compailer对象的codeGenerationResults属性值里查看结构为:

_source:ConcatSource {_children: Array(4), _isOptimized: false}
_children:(4) ['\n;// CONCATENATED MODULE: ./index.js\n', ReplaceSource, '\n;// CONCATENATED MODULE: ./main.js\n', ReplaceSource]
0:'\n;// CONCATENATED MODULE: ./index.js\n'
1:ReplaceSource {_source: CachedSource, _name: undefined, _replacements: Array(1), _isSorted: false}
2:'\n;// CONCATENATED MODULE: ./main.js\n'
3:ReplaceSource {_source: CachedSource, _name: undefined, _replacements: Array(1), _isSorted: false}
length:4

6.2 生成assets

利用上面生成的codeGenerationResults值和其他值去生成asstes;调用createChunkAssets的方法就是把生成manifest处理成fileManifest,然后调用fileManifest.render()生成最终的source,然后缓存到compilation.assets里;首先把codeGenerationResults值传入到this.getRenderManifest这个方法里;这个方法会触发javascriptModulesPlugins模块里注册的hooks;给返回结果绑定render方法;然后执行render方法,此时会调用javascriptModulePlugins模块的renderMain方法,该方法会在source基础上添加renderBootstrap的代码块;

_children:(7) ['/******/ (() => { // webpackBootstrap\n', '/******/ \t"use strict";\n', 'var __webpack_exports__ = {};\n', CachedSource, '\n', '/******/ })()\n', ';']
0:'/******/ (() => { // webpackBootstrap\n'
1:'/******/ \t"use strict";\n'
2:'var __webpack_exports__ = {};\n'
3:CachedSource {_source: ConcatSource, _cachedSourceType: undefined, _cachedSource: undefined, _cachedBuffer: undefined, _cachedSize: undefined, …}
4:'\n'
5:'/******/ })()\n'
6:';'
length:7

最终生成的代码会调用this.emitAsset()方法把assets挂到compilation的assets属性上;数据结构为: image.png)

6.3 处理assets

触发processAssets hook;会把this.assets的source进行处理最终输出,首先触发webpack-sources的CachedSource模块的sourceAndMap方法,整合assets里的source,遍历source得到合并后的code为:

/******/ (() => { // webpackBootstrap
/******/        "use strict";
var __webpack_exports__ = {};
;// CONCATENATED MODULE: ./index.js
/* harmony default export */ const index = ((name)=>{
    alert(66666);
    console.log(name);
});
;// CONCATENATED MODULE: ./main.js

alert(334445);
console.log('main');
index('index');
/******/ })()
;

这个流程最终生成了这样的asstes:

assets:{
bundle.js: {
_value: “上述代码块“
_valueAsBuffer:undefined
_valueIsBuffer:false
  }
}

7. emit阶段

该阶段是输出bundle文件;回到最开始run方法里回调中的定义的onCompiled方法;在onCompiled方法里this.this.emitAssets方法会先根据配置创建输出更目录dist; image.png) emitFiles作为创建dist后的回调,该回调里定义了一系列方法;首先执行writeOut;然后依次执行processMissingFile ->getContent(得到buildle文件的buffer)->updateWithReplacementSource(修改文件的size等)->doWrite; image.png) 最后调用this.outputFileSystem.writeFile输出build.js文件。

总结:最后方便大家阅读:附上简略整个的流程图;待完……

及时获取更新,了解更多动态,请关注https://www.gogoing.site

如果你觉得这篇文章对你有帮助,欢迎关注微信公众号-前端学堂,更多精彩文章等着你!

评论
fallback