webpack编译流程初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象用上一步得到的参数初始化Compiler对象加载所有配置的插件执行对象的run方法开始执行编译根据配置中的entry找出入口文件从入口文件出发,调用所有配置的Loader对模块进行编译再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk再把每个Chunk转换成一个单独的文件加入到输出列表在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统 在以上过程中,Webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用Webpack提供的API改变Webpack的运行结果 1。1entry srcentry1。jslettitlerequire(。title)console。log(entry12,title) srcentry2。jslettitlerequire(。title。js)console。log(entry2,title) srcitle。jsmodule。exportstitle1。2loader。jsloader的本质就是一个函数,一个用于转换或者说翻译的函数把那些webpack不认识的模块lesssassbaxx转换为webpack能认识的模块jsjson loaderslogger1loader。jsfunctionloader1(source){letnameentry1;returnsourcelogger1letnameentry1;logger1}module。exportsloader1 loaderslogger2loader。jsfunctionloader2(source){letnameentry1;returnsourcelogger2letnameentry1;logger2}module。exportsloader21。3plugin。js pluginsdoneplugin。jsclassDonePlugin{apply(compiler){compiler。hooks。done。tap(DonePlugin,(){console。log(done:结束编译)})}}module。exportsDonePlugin pluginsrun1plugin。jsclassRunPlugin{apply(compiler){在此插件里可以监听run这个钩子compiler。hooks。run。tap(Run1Plugin,(){console。log(run1:开始编译)})}}module。exportsRunPlugin pluginsrun2plugin。jsclassRunPlugin{apply(compiler){compiler。hooks。run。tap(Run2Plugin,(){console。log(run2:开始编译)})}}module。exportsRunPlugin1。4webpack。config。js webpack。config。jsconstpathrequire(path)constRun1Pluginrequire(。pluginsrun1plugin)constRun2Pluginrequire(。pluginsrun2plugin)constDonePluginrequire(。pluginsdoneplugin)module。exports{mode:development,devtool:false,context:process。cwd,entry:{entry1:。srcentry1。js,entry2:。srcentry2。js,},output:{path:path。resolve(dirname,dist),filename:〔name〕。js,},resolve:{extensions:〔。js,。jsx,。tx,。tsx〕,},module:{rules:〔{test:。js,use:〔path。resolve(dirname,loadersloader2。js),path。resolve(dirname,loadersloader1。js)〕,},〕,},plugins:〔newDonePlugin(),newRun2Plugin(),newRun1Plugin()〕,}1。5debugger。js debugger。jsconstfsrequire(fs)constwebpackrequire(。webpack2)constwebpackrequire(webpack)constwebpackConfigrequire(。webpack。config)debuggerconstcompilerwebpack(webpackConfig)4。执行Compiler对象的run方法开始执行编译compiler。run((err,stats){if(err){console。log(err)}else{stats代表统计结果对象conststatsJsonJSON。stringify(stats。toJson({files:true,代表打包后生成的文件assets:true,其实是一个代码块到文件的对应关系chunks:true,从入口模块出发,找到此入口模块依赖的模块,或者依赖的模块依赖的模块,合在一起组成一个代码块modules:true,打包的模块每个文件都是一个模块}))fs。writeFileSync(。statsJson。json,statsJson)}})1。6webpack。js webpack2。jsconstCompilerrequire(。Compiler)functionwebpack(options){1。初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象argv〔0〕是Node程序的绝对路径argv〔1〕正在运行的脚本nodedebuggermodeproductionconstargvprocess。argv。slice(2)constshellOptionsargv。reduce((shellOptions,options){optionsmodedevelopmentconst〔key,value〕options。split()shellOptions〔key。slice(2)〕valuereturnshellOptions},{})console。log(shellOptions,shellOptions)constfinalOptions{。。。options,。。。shellOptions}2。用上一步得到的参数初始化Compiler对象constcompilernewCompiler(finalOptions)3。加载所有配置的插件const{plugins}finalOptionsfor(letpluginofplugins){订阅钩子plugin。apply(compiler)}returncompiler}module。exportswebpack1。7Compilation Compiler。jsconst{SyncHook}require(tapable)constCompilationrequire(。Compilation)constfsrequire(fs)constpathrequire(path)Compiler模块是webpack的主要引擎classCompiler{constructor(options){this。optionsoptionsthis。hooks{run:newSyncHook(),在开始编译之前调用done:newSyncHook(),在编译完成时执行}}run(callback){this。hooks。run。call()在编译开始前触发run钩子执行在编译的过程中会收集所有的依赖的模块或者说文件stats指的是统计信息moduleschunksfilesbundleassets指的是文件名和文件内容的映射关系constonCompiled(err,stats,fileDependencies){10。在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统for(letfilenameinstats。assets){letfilePathpath。join(this。options。output。path,filename)fs。writeFileSync(filePath,stats。assets〔filename〕,utf8)}callback(err,{toJson:()stats})for(letfileDependencyoffileDependencies){监听依赖的文件变化,如果依赖的文件变化后会开始一次新的编译fs。watch(fileDependency,()this。compile(onCompiled))}}this。hooks。done。call()在编译完成时触发done钩子执行调用compile方法进行编译开始一次新的编译this。compile(onCompiled)}开启一次新的编译compile(callback){每次编译都会创建一个新的Compilation实例letcompilationnewCompilation(this。options,this)compilation。build(callback)}}module。exportsCompiler1。8Compilation Compilation。jsconstpathrequire(path)constfsrequire(fs)constparserrequire(babelparser)consttypesrequire(babeltypes)consttraverserequire(babeltraverse)。defaultconstgeneratorrequire(babelgenerator)。defaultconstbaseDirnormalizePath(process。cwd())functionnormalizePath(path){returnpath。replace(g,)}classCompilation{constructor(options,compiler){this。optionsoptions配置参数this。options。contextthis。options。contextnormalizePath(process。cwd())this。compilercompilerthis。modules〔〕这里放置本次编译涉及的所有的模块this。chunks〔〕本次编译所组装出的代码块this。assets{}存放输出的文件key是文件名,值是文件内容this。files〔〕代表本次打包出来的文件this。fileDependenciesnewSet()本次编译依赖的文件或者说模块}build(callback){5。根据配置中的entry找出入口文件letentry{}格式化入口文件if(typeofthis。options。entrystring){entry。mainthis。options。entry}else{entrythis。options。entry}对入口进行遍历for(letentryNameinentry){获取入口文件的绝对路径letentryFilePathpath。posix。join(baseDir,entry〔entryName〕)把此入口文件添加到文件依赖列表中this。fileDependencies。add(entryFilePath)6。从入口文件出发,调用所有配置的Loader对模块进行编译letentryModulethis。buildModule(entryName,entryFilePath)this。modules。push(entryModule)8。根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunkletchunk{name:entryName,入口名称entryModule,入口的模块。srcentry。jsmodules:this。modules。filter((module)module。names。includes(entryName)),此入口对应的模块}this。chunks。push(chunk)}9。再把每个Chunk转换成一个单独的文件加入到输出列表this。chunks。forEach((chunk){constfilenamethis。options。output。filename。replace(〔name〕,chunk。name)this。files。push(filename)组装chunkthis。assets〔filename〕getSource(chunk)})callback(null,{modules:this。modules,chunks:this。chunks,assets:this。assets,files:this。files,},this。fileDependencies)}编译模块param{}name模块所属的代码块(chunk)的名称,也就是entry的nameentry1entry2param{}modulePath模块的绝对路径buildModule(entryName,modulePath){1。读取文件的内容letrawSourceCodefs。readFileSync(modulePath,utf8)获取loader的配置规则let{rules}this。options。module根据规则找到所有的匹配的loader适用于此模块的所有loaderletloaders〔〕rules。forEach((rule){用模块路径匹配正则表达式if(modulePath。match(rule。test)){loaders。push(。。。rule。use)}})调用所有配置的Loader对模块进行转换lettransformedSourceCodeloaders。reduceRight((sourceCode,loaderPath){constloaderFnrequire(loaderPath)returnloaderFn(sourceCode)},rawSourceCode)7。再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理获取当前模块,也就是。srcentry1。js的模块IDletmoduleId。path。posix。relative(baseDir,modulePath)创建一个模块ID就是相对于根目录的相对路径dependencies就是此模块依赖的模块name是模块所属的代码块的名称,如果一个模块属于多个代码块,那么name就是一个数组letmodule{id:moduleId,dependencies:newSet(),names:〔entryName〕}this。modules。push(module)letastparser。parse(transformedSourceCode,{sourceType:module})Visitor是babel插件中的概念,此处没有traverse(ast,{CallExpression:({node}){如果调用的方法名是require的话,说明就要依赖一个其它模块if(node。callee。namerequire){。代表当前的模块所有的目录,不是工作目录letdepModuleNamenode。arguments〔0〕。value。titleletdepModulePath获取当前的模块所在的目录if(depModuleName。startsWith(。)){暂时先不考虑nodemodules里的模块,先只考虑相对路径constcurrentDirpath。posix。dirname(modulePath)要找当前模块所有在的目录下面的相对路径depModulePathpath。posix。join(currentDir,depModuleName)此绝对路径可能没有后续,需要尝试添加后缀获取配置的扩展名后缀constextensionsthis。options。resolve。extensions尝试添加后缀返回最终的路径depModulePathtryExtensions(depModulePath,extensions)}else{如果不是以。开头的话,就是第三方模块depModulePathrequire。resolve(depModuleName)}把依赖的模块路径添加到文件依赖列表this。fileDependencies。add(depModulePath)获取此依赖的模块的ID,也就是相对于根目录的相对路径letdepModuleId。path。posix。relative(baseDir,depModulePath)修改语法树,把依赖的模块名换成模块IDnode。arguments〔0〕types。stringLiteral(depModuleId)把依赖的模块ID和依赖的模块路径放置到当前模块的依赖数组中module。dependencies。add({depModuleId,depModulePath})}},})转换源代码,把转换后的源码放在source属性,用于后面写入文件let{code}generator(ast)module。sourcecode;〔。。。module。dependencies〕。forEach(({depModuleId,depModulePath}){判断此依赖的模块是否已经打包过了或者说编译过了letexistModulethis。modules。find((module)module。iddepModuleId)if(existModule){existModule。names。push(entryName)}else{letdepModulethis。buildModule(entryName,depModulePath)this。modules。push(depModule)}})returnmodule}}param{}modulePathparam{}extensionsreturnsfunctiontryExtensions(modulePath,extensions){if(fs。existsSync(modulePath)){returnmodulePath}for(leti0;iextensions。length;i){letfilePathmodulePathextensions〔i〕if(fs。existsSync(filePath)){returnfilePath}}thrownewError(找不到{modulePath})}functiongetSource(chunk){return((){varmodules{{chunk。modules。filter((module)module。id!chunk。entryModule。id)。map((module){module。id}:module{{module。source}})。join(,)}};varcache{};functionrequire(moduleId){varcachedModulecache〔moduleId〕;if(cachedModule!undefined){returncachedModule。exports;}varmodulecache〔moduleId〕{exports:{}};modules〔moduleId〕(module,module。exports,require);returnmodule。exports;}varexports{};((){{chunk。entryModule。source}})();})();}module。exportsCompilation webpack2。js总结1。文件作用webpack。js文件 webpack方法接收webpack。config。js参数,返回compiler实例初始化参数始化Compiler对象实例加载所有配置的插件Compiler文件Compiler模块是webpack的主要引擎constructor方法:初始化一些hooksrun方法执行插件订阅的一系列hooks创建Compilation实例并执行实例的build(onCompiled)方法(开启一次新的编译)onCompiled回调在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统执行compiler。run方法的回调,传入info监听依赖的文件变化,如果依赖的文件变化后会开始一次新的编译Compilation文件 build方法。根据配置中的entry找出入口文件从入口文件出发,调用所有配置的Loader对模块进行编译再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk再把每个Chunk转换成一个单独的文件加入到输出列表执行成功后的回调2。流程总结初始化参数:初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象(命令行优先级高)开始编译用上一步得到的参数初始化Compiler对象初始化options参数和hooks(run:newSyncHook(),在开始编译之前调用。。。)加载所有配置的插件:在配置中找到plugins数组遍历plugins执行每个插件的apply方法,并把compiler实例传进去(每个插件都有一个apply方法)执行compiler。hooks。run。tap等方法注册事件执行compiler实例的run方法开始执行编译整个过程伴随着触发插件的注册个各种钩子函数this。hooks。done。call()。。。开启一次新的编译,创建一个新的Compilation实例执行实例的build方法,传入完成的回调编译模块根据配置中的entry找出入口文件格式化入口文件,变成对象形式对入口进行遍历,获取入口文件的绝对路径,添加到文件依赖列表中loader转换:从入口文件出发,调用所有配置的Loader对模块进行转换(最终返回module对象)读取处理文件的内容根据规则找到所有的匹配的loader调用所有配置的Loader对模块进行转换(从上到下,从右向左)获取当前模块模块id,相对于根目录的相对路径创建一个module对象constmodule{ id:。srcentry1。js,相对于根目录的相对路径 dependencies:〔{depModuleId:。srctitle。js,depModulePath:xxx}〕,dependencies就是此模块依赖的模块 names:〔entry1〕,name是模块所属的代码块的名称,如果一个模块属于多个代码块,那么name就是一个数组 2。hrsource:xxx,存放对应的源码 }编译模块分析依赖,再递归遍历本步骤直到所有入口依赖模块的文件都经过了本步骤的处理将loader编译后的代码调用parse转换为ast遍历语法树,如果存在require或者import,说明就要依赖一个其它模块获取依赖模块的绝对路径,添加到文件依赖列表中获取此依赖的模块的ID,也就是相对于根目录的相对路径修改语法树,把依赖的模块名换成模块ID把依赖的模块ID和依赖的模块路径放置到当前模块module的依赖数组中调用generator(ast),把转换后的源码放在module。source属性,用于后面写入文件遍历module。dependencies,递归构建module,构建好的存储到this。modules上,如果第二个入口也依赖该模块,直接取用,只需要给该模块的name属性上添加上入口信息输出资源组装chuck对象:组装constchuck{ name:entry1,入口名称 entryModule,入口的模块的module{id,name,dependencies,source} modules:〔{}〕,入口依赖模块的集合 }this。chunks。push(chunk)生成bundle文件把每个Chunk转换成一个单独的文件加入到输出列表获取要生成的文件名称并把文件名添加到this。files中获取文件内容并给this。assets对象执行compilation。build方法的回调写入文件在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统