简介

上一篇讲述了如何理解tapable这个钩子机制,因为这个是webpack程序的灵魂。虽然钩子机制很灵活,而然却变成了我们读懂webpack道路上的阻碍。每当webpack运行起来的时候,我的心态都是佛系心态,祈祷中间不要出问题,不然找问题都要找半天,还不如不打包。尤其是loader和plugin的运行机制,这两个是在什么时候触发的,作用于webpack哪一个环节?这些都是需要熟悉webpack源码才能有答案的问题。

大家就跟着我一步步揭开webpack的神秘面纱吧。

如何调试webpack

本小节主要描述了,如何调试webpack,如果你有自成一派的调试方法,或者更加主流的方法,可以留言讨论讨论。

简易版webpack启动

工欲善其事,必先利其器。我相信大家刚学习webpack的时候一定是跟着官方文档运行webpack打包网站。

webpack上手文档,->萌新指路

初级操作应该依赖webpack-cli,通过在小黑框中输入npx webpack --config webpack.config.js,然后enter执行打包。虽然webpack-cli会帮助我们把大多数打包过程中会出现的问考虑进去,但是这样会使我们对webpack的源码更加陌生,似乎配置就是一切。

这种尴尬的时候,我们就要另辟蹊径来开发,并不用官方的入门方法。

我写的一个简易启动webpack的调试代码,如下方所示:

//载入webpack主体
let webpack=require('webpack');
//指定webpack配置文件
let config=require("./webpack.config.js");
//执行webpack,返回一个compile的对象,这个时候编译并未执行
let compile=webpack(config);
//运行compile,执行编译
compile.run();
复制代码

如果大家想知道我这段代码的灵感来源于哪里?我会告诉大家是来自webpack-cli。

挑出关键运行的部分,然后重组就可以做一个简易的webpack启动了。

话唠笔者:我为什么要这么做?代码越少分析起来越简单,“无关”代码越多,我们的视线就会被这些代码所困住而寸步难行。当然等到这部分掌握了,再去看cli的代码,也许收获会更大一些。

配置的温馨提醒

虽然我们都会配置Entry,但是我们可能会忽略Context的配置,如果我们的cmd在当下的目录,那么执行是OK的,但是如果我们不在当前目录下,然后执行,那么很有可能路径会出现问题,为了防止遮掩的悲剧产生,我推荐机上context配置也就是context:你当前项目的绝对路径

module.exports = {
//...
context: path.resolve(__dirname, 'app')
};
复制代码

打断点!debugger

关键部分来了,写一个简易个webpack主要就是为了方便打断点!增加程序的可读性。

非vscode玩家入口

如果你是小黑框(termial)和chrome的爱好者,以下方法请收下!点击获取参考文档,这里有详细的操作过程。

node --inspect-brk debugger1.js
复制代码

然后我们就可以愉快地像调试网页一样在亲切的chrome上玩耍了。但是问题来了,没有断点的调试,太可怕了,虽然每一步都显示非常地好,不过我并不想知道fs的读取,timer的运行和模块的加载等node原生方法,next的点击了几百下,webpack主流程并没有走几步,这极大的挑战了我的耐心,如果有小伙伴一步步next到了最后一步,希望你能来和我们分享一下。为了防止过于细节,这个时候我们可以在适当的地方打断点:

options = new WebpackOptionsDefaulter().process(options);
debugger//是他是他就是他,我们的救星
compiler = new Compiler(options.context);
复制代码

WebpackOptionsDefaulter运行之后,程序便会自动停下任君调试。

vscode的玩家

如果是vscode的玩家,除了上述的debugger方法,我们还可以直接打红点,作为断点,这样更加方便。最后还可以一键清除所有的断点。

同时也可以在当前断点的时候,在调试控制台,输入自己想要了解的参数。

webpack主流程是什么

对于webpack的主流程的解释,我分为了以下三种:

简介版本:webpack的过程就通过Compiler发号施令,Compilation专心解析,最后返回Compiler输出文件。

专业版本:webpack的过程是通过Compiler控制流程,Compilation专业解析,ModuleFactory生成模块,Parser解析源码,最后通过Template组合模块,输出打包文件的过程。

粗暴版本:webpack就是打散源码再重组的过程。

源码解读

我们直接开始从专业版本来理解webpack吧。从上方的启动代码我们可以看到webpack(config)是启动webpack打包的关键代码,也就是webpack.js是我们第一个研究对象。

因为笔者各种调试webpack,各种断点,导致源码的行数和线上的行数不一致,所以这里我会直接抛出代码而不是行数,大家自行对着webpack的源码对照。

一切的源头webpack.js

大家以为我会从第一步引入开始解析吗?不存在的,我们直接从关键逻辑开始吧。

options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
···省略自定义插件的绑定
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
复制代码

是不是觉得不知所云,不要慌,我们一行行看下来,这里的每一行都很重要。

options = new WebpackOptionsDefaulter().process(options);这一行的关键字Default,通过关键字我们可以猜测到这个类的作用就是将我们webpack.config.js中自定义的部分,覆盖webpack默认的配置。

挑一行这个类中的代码,便于大家理解。

this.set("entry", "./src");
复制代码

这个就是入口的默认配置,如果我们不配入口,程序就会自动找src下方的文件打包。

话痨的笔者:webpack4.0有一个很大的特色就是零配置,无需webpack.config.js我们都可以打包。为什么呢?难道是webpack真的不需要配置了吗?做到人工智能了?不!因为有默认配置,就像所有的程序都有初始化的默认配置。

new Compiler(options.context),非常重要的编译器,基本上编译的流程就是出自这个类。 options.context这个值是当前文件夹的绝对路径,通过WebpackOptionsDefaulter.js默认配置的代码片段的代码片段既可以理解。这个类稍后分析。

this.set("context", process.cwd());
复制代码

然后就是一系列,对于compiler的配置以及将NodeEnvironmentPlugin的hooks以及自定义的插件plugins也是钩子分别挂入compiler之中,挂入之后触发environment的一些钩子。相当于开车前会启动车子一样。比如在解析文件(resolver)时一定会用到的文件系统,如何读取文件。这个就是将inputFileSystem输入文件系统挂载了compiler上,然后通过compiler来控制那些插件需要这个功能,就派发给他。

class NodeEnvironmentPlugin {
apply(compiler) {
compiler.inputFileSystem = new CachedInputFileSystem(
new NodeJsInputFileSystem(),
60000
);
//....
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
});
}
}
module.exports = NodeEnvironmentPlugin; 复制代码

compiler.options = new WebpackOptionsApply().process(options, compiler);,这里又对options做处理的,如果说第一步是格式化配置,那么这边就是将配置在compiler中激活。这个类很重要,因为compiler中的激活了许多钩子,同时在一些钩子上挂上(tap)了函数。

关键配置options激活解析:

  • 这个是parse的一个解析器,如果文件是js,就会使用到这个parse,也就是说这个是在loader的时候进行的。

    new JavascriptModulesPlugin().apply(compiler);
    复制代码
  • 这一行是用于解析也就是入口的解析,是SingleEntryPlugin还是MultiEntryPlugin。这个方法相当于入口程序已经就绪,就等后续的一声令下就可以运行了。

    new EntryOptionPlugin().apply(compiler);
    compiler.hooks.entryOption.call(options.context, options.entry);
    复制代码
  • 当插件钩子都挂上后,执行的钩子。

    compiler.hooks.afterPlugins.call(compiler);
    复制代码
  • 接着是各类路径解析的钩子,根据我们的自定义resolver来解析。

    compiler.resolverFactory.hooks.resolveOptions
    复制代码

关键点突破Compiler.js

可以说Compiler.js这个类才是真正得控制了webpack打包的流程,如果说webpack.js所做的事是准备,那么Compiler就是撸起袖子就是干。

constructor

我们从constructor开始解析Compiler

Compiler首先是定义了一堆钩子,如果大家观察仔细会发现这就是流程的各个阶段(此处的代码可读性很友好),也就是各个阶段都有个钩子,这意味着什么?我们可以利用这些钩子挂上我们的插件,所以说Compiler很重要。

关键钩子 钩子类型 钩子参数 作用
beforeRun AsyncSeriesHook Compiler 运行前的准备活动,主要启用了文件读取的功能。
run AsyncSeriesHook Compiler “机器”已经跑起来了,在编译之前有缓存,则启用缓存,这样可以提高效率。
beforeCompile AsyncSeriesHook params 开始编译前的准备,创建的ModuleFactory,创建Compilation,并绑定ModuleFactory到Compilation上。同时处理一些不需要编译的模块,比如ExternalModule(远程模块)和DllModule(第三方模块)。
compile SyncHook params 编译了
make AsyncParallelHook compilation 从Compilation的addEntry函数,开始构建模块
afterCompile AsyncSeriesHook compilation 编译结束了
shouldEmit SyncBailHook compilation 获取compilation发来的电报,确定编译时候成功,是否可以开始输出了。
emit AsyncSeriesHook compilation 输出文件了
afterEmit AsyncSeriesHook compilation 输出完毕
done AsyncSeriesHook Stats 无论成功与否,一切已尘埃落定。

Compiler.run()

从函数的名称我们大致可以猜出他的作用,不过还是从Compiler的运行流程来加深对Compiler的理解。Compiler.run()开跑!

首先触发beforeRun这个async钩子,在这个钩子中绑定了读取文件的对象。接着是run这个async钩子,在这个钩子中主要是处理缓存的模块,减少编译的模块,加速编译速度。之后才会进去入Compiler.compile()的编译环节。

this.hooks.beforeRun.callAsync(this, err => {
....
this.hooks.run.callAsync(this, err => {
....
this.compile(onCompiled);
....
});
....
});
复制代码

等Compiler.compile运行结束之后会回调run中名为onCompiled的函数,这个函数的作用就是将编译后的内容生成文件。我们可以看到首先是shouldEmit判断是否编译成功,未成功则结束done,打印相应信息。成功则调用Compiler.emitAssets打包文件。

if (this.hooks.shouldEmit.call(compilation) === false) {
...
this.hooks.done.callAsync(stats, err => {
...
}
return }
this.emitAssets(compilation, err => {
...
if (compilation.hooks.needAdditionalPass.call()) {
...
this.hooks.done.callAsync(stats, err => {});
};
}) 复制代码

Compiler.compile()

上一节只讨论了Compiler.run方法的整体流程,并未提及Compiler.compile,这个compiler顾名思义就是编译的意思。那么编译的过程中究竟发生了写什么呢?

const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
...
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
...
compilation.finish();
ompilation.seal(err => {
...
this.hooks.afterCompile.callAsync(compilation, err => {
...
此处是回调函数,这个函数主要用于将编译成功的代码输出
...
});
});
});
});
复制代码

首先是定义了params并传入了hooks.compile这个钩子中,params就是模块工厂,其中最常用的就是normalModuleFactory,将这个工厂传入钩子中,方便之后的插件或钩子操作模块。

钩子想要和程序产生联系,比如在compiler中加内容,就需要将Compiler传入钩子中,才可行,否则并无接口暴露给插件。

然后是beforeCompile预备一下,接着就是启动compile这个钩子。

这里新建了Compilation,一个很重要的专注于编译的类。

hooks.make这个钩子就是正式启动编译了,所以这个钩子执行完毕就意味这编译结束了,可以进行封装seal了。那么make这个钩子触发的时候,执行了那些步骤呢?

大家是否还记得在webpack.js中提到过的EntryOptionPlugin

    new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
复制代码

来自笔者的话痨:webpack的模块构建其实是通过entry,也就是入口文件开始分析,开始构建。也就是说一个入口文件会触发一次Compliation.addEntry,然后触发之后就是Compilation开始构建模块了。

EntryOptionPlugin是帮助我们处理入口类型的插件,他会webpack.config.js中entry的不同配置帮助我们搭配不同的EntryPlugin。通过entry配置进入的一共有3种类型,SingleEntryPlugin,MultiEntryPlugin和DynamicEntryPlugin,根据名字就能够轻易区分他们的类型。一般一个compiler只会触发一个EntryPlugin,然后在这个EntryPlugin中,会有我们构建模块的入口,也就是compilation的入口。

compiler.hooks.make.tapAsync("SingleEntryPlugin|MultiEntryPlugin|DynamicEntryPlugin",(compilation, callback) => {
...
compilation.addEntry(context, dep, name, callback);
...
});
复制代码

除了帮助我们打开compilation的大门之外,???EntryPlugin还绑定了一个事件就是,当前入口的模块工厂类型。

compiler.hooks.compilation.tap("SingleEntryPlugin",(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
);
});
复制代码

这个钩子函数帮我们定义了SingleEntry的模块类型,那么之后compliation编译的时候就会使用normalModuleFactory来创造模块。

make这个钩子相当于一个转折点,我们从主流程中跳转到正真编译的流程之中——compilation,一个专注于编译优化的类。

等compilation编译成功之后,再回到compiler主战场,我们将编译成功的内容emitAssest到硬盘上。

专业编译100年——Compilation.js

如果说Compiler是流程,那么Compilation就是编译主场了。也就是源代码经过他加工之后才得到了升华变成了规规矩矩的模样。

Compilation的工作总结起来就是,添加入口entry,通过entry分析模块,分析模块之间的依赖关系,就像图表一样。构建完成之后就开始seal,封装了这个阶段Compilation干了一系列的优化措施以及将解析后的模块转化为标准的webpack模块,输出备用,前提是你将优化plugin挂到了各个优化的hooks上面,触发了优化的钩子,但是钩子上也要注册了函数才能生效。

好了我们从Compile得到的信息来按照出场顺序分析Compilation.js

addEntry——一切开始的地方

上一节提到的SingleEntryPlugin(还有其他的EntryPlugin),就是一个启动口,等到触发compile.hooks.make的时候,就会启动SingleEntryPlugin中的compilation.addEntry这个方法,这个方法就是启动构建入口模块,成功后将入口模块添加到程序之中。

//context,entry,name都是options中的值。
addEntry(context, entry, name, callback) {
this._addModuleChain(context,entry,module => {
this.entries.push(module);
},(err, module) => {
...
if (module) {
slot.module = module;
} else {
const idx = this._preparedEntrypoints.indexOf(slot);
if (idx >= 0) {
this._preparedEntrypoints.splice(idx, 1);
}
}
...
return callback(null, module);
}
);
}
复制代码

添加模块的依赖_addModuleChain

这个方法是模块构建的主要方法,由addEntry调用,等模块构建完成之后返回。

  • _addModuleChain,构建模块,同时保存模块间之间的依赖。

    • const moduleFactory = this.dependencyFactories.get(Dep);moduleFactory.create(...),这里的moduleFactory其实就是当前模块的类型的创造工厂,create就是从这个工厂中创造除了新产品(新模块)。

      • this.addModule(module)->this.modules.push(module);,将模块加入compilation.modules之中。
      • onModule(module);, 这个方法调用了addEntry中this.entries.push(module),也就是将入口模块加入compilation.entries。
      • this.buildModule->this.hooks.buildModule.call(module);module.build(...),这个方法就是给出了一个可以对module进行操作的hooks,大家可以自行定义plugin对此进行操作。之后便是模块自行的一个创建,这个创建的方法更具模块类型而定,比如normalModuleFactor创建的模块就来自NormalModule这个类。
        • _addModuleChain的内置方法afterBuild(),这个方法就是获取模块和模块依赖的创建所耗费的时间,然后如果有回调函数就执行回调函数。

构建结束之后,回到Compiler,finish我们的构建

这里finish干了两件事,一件就是出发了结束构建的钩子,然后就是收集了每个模块构建是产生的问题。

一切就绪,开始封装seal(callback)

产品已经准备好,准备打包出口。

开始逐个执行优化的钩子,如果大家有写优化的钩子的化。

开始优化:

此处是优化依赖的hook

此处是优化module的hook

此处是优化Chunk的hook

。。。。。

太多优化了,笔者已经开溜了。

优化结束之后开始执行来自Compiler的回调函数,也就是将生成文件。

除了各类钩子的call之外,seal还干了一件很重要时就是将格式化的js,通过Template模版,重新聚合在一起,然后回调Compiler生成文件。这一块会在之后Template的时候具体分析。

笔者有话说,其实主流程就是Compiler和Compliation,这两个类互相合作。接下来还有几个比较关键的类,不过从我的角度看来,不属于主要流程,但是很重要,因为是模块创建的类。就像是流水线上的产品一样,产品本身和流水线的流程无关。

模块的发源地—moduleFactory

moduleFactory是模块的实例,不过并不属于主流程,就像是乐高的零件一样,没有它,我们会拼又如何?巧妇难为无米之炊!需要编译的moduleFactory分为两类context和normal,我基本上遇到的都是normal类型的,所以这里以noraml类为主解释moduleFactory。

他的使命

既然他是工厂,那么他的使命就是制作产品。这里模块就是产品,因此工厂只需要一个就够了。我们的工厂是在Compiler.compile中创建的,并将此作为参数传入了compile.hooks.beforeCompilecompile.hooks.compile这两个钩子之中,这意味着我们在写这两个钩子的挂载函数的时候,就可以调用这个工厂帮我们创建处理模块了。

const NormalModule = require("./NormalModule");
const RuleSet = require("./RuleSet");
复制代码

这两个参数很重要,一个是产品本身,也就是通过NormalModule创建的实例就是模块。RuleSet就是loaders,其中包括自带的loader和自定义的loader。也就是说Factory干了两件事,第一件是匹配了相对应的parser,将parser配置成了专门用于当前模块的解析器将源码解析成AST模式,第二件是创建generator用于生成代码也就是还原AST(这一块是模版生成的时候会用到),第三件是创建模块,构建模块的时候给他找到相映的loader,替换源码,添加相映的依赖模块,然后在模块解析的时候提供相应的parser解析器,在生成模版的时候提供相应的generator。

normalModule类

Fatory提供了原料(options)和工具(parser),就等于将参数输给了自动化的机器,这个normalModule就是创造的机器,由他来build模块,并将源码变为AST语法树。

build(options, compilation, resolver, fs, callback) {
//...
return this.doBuild(options, compilation, resolver, fs, err => {
//...
this._cachedSources.clear();
//...
try {
const result = this.parser.parse(//重点在这里。
//....
);
//... });
}
复制代码

在Compilation中模块创建好之后,开始触发module的build方法,开始生成模块,他的逻辑很简单,就是输入source源文件,然后通过reslover解析文件loader和依赖的文件,并返回结果。然后通过loader将此转化为标准的webpack模块,存储source,等待生成模版的时候备用。

等到需要打包的时候,就将编译过的源码在重组成JS代码,主要通过Facotry给模块配备的generator。

source(dependencyTemplates, runtimeTemplate, type = "javascript") {
//...获取缓存
const source = this.generator.generate(
this,
dependencyTemplates,
runtimeTemplate,
type
);
//...存到缓存中
return cachedSource;
} 复制代码

loader进行曲

loader究竟在哪里执行,如何执行

对于初学者来说,loader和plugin可能会傻傻地分不清(没错,我就是那个傻子)。深入了解源码之后,我才明明白白了解两者的不同。

懵懂的我 了解套路的我
区别1: plugin范围广,嗯,含义真的很广 区别1: plugin可以在任何一个流程节点出现,loader有特定的活动范围
区别2: 配置地方不一致,loader的配置很奇怪,居然不是module.loaders,而是module.ruleset 区别2: plugin可以做和源码无关的事,比如监控,loader只能解析源码变成标准模块。

那么loader究竟在哪里执行的呢?了解了CompilationNormalModuleFactoryNormalModule的功能之后,听我娓娓道来loader是如何进入module的!

首先是Compilation._addModuleChain开始添加模块时,触发了Compilation.buildModule这个方法,然后调用了NormalModule.build,开始创建模块了。创建模块之时,会调用runLoaders去执行loaders,但是对于loader所在的位置,程序还是迷茫的,所以这个时候需要请求NormalModuleFactory.resolveRequestArray,帮我们读取loader所在的地址,执行并返回。就这样一个个模块生成,一个个loader生成,直到最后一个模块创建完毕,然后就到了Compilation.seal的流程了。

灵魂Parser

等到当前模块处理完loaders之后,将导入模块变成标准的JS模块之后,就要开始分解源码了,让它变成标准的AST语法树,这个时候就要依靠Parser。Parser很强大,他帮助我们将不规范的内容转化为标准的模块,方便打包活着其他操作。Parser相当于一个机器,源文件进入,然后处理,然后输出,源文件并未于Parser产生化学作用。Parser不是按照normalModule创建的个数存在的,而是按照模块的类型给匹配的。想想如果工厂中给每一个产品都配一个解析器,那么效率成功地biubiubiu下降了了。

javascript类型的Parser一共有3个类型,"auto"、"script"和"module",根据模块的需求,Factoy帮我们匹配不同类型的Parser。

normalModuleFactory.hooks.createParser.for("javascript/auto").tap("JavascriptModulesPlugin", options => {
return new Parser(options, "auto");
});
normalModuleFactory.hooks.createParser.for("javascript/dynamic").tap("JavascriptModulesPlugin", options => {
return new Parser(options, "script");
});
normalModuleFactory.hooks.createParser.for("javascript/esm").tap("JavascriptModulesPlugin", options => {
return new Parser(options, "module");
});
复制代码

Parser实则呢么解析我们的源码的呢?

首先先变成一个AST——标准的语法树,结构化的代码,方便后期解析,如果传入的source不是ast,也会被强制ast再进行处理。

这个解析库,webpack用的是acorn。

static parse(code, options) {
.....
ast = acorn.parse(code, parserOptions);
.....
return ast;
}
parse(source, initialState) {
//...
ast = Parser.parse(source, {
sourceType: this.sourceType,
onComment: comments
});
//...
}
复制代码

叮咚——你的打包模版Template

终于到了收尾的时候了,不过这个部分也不及简单呢。

Template是在compilation.seal的时候触发的们也就是模块构建完成之后。我们要将好不容易构建完成的模块再次重组成js代码,也就是我们在bundle中见到的代码。

我们打包出来的js,总是用着相同的套路?这是为什么?很明显有个标准的模版。等到我们的源文件变成ast之后,准备输出的处理需要依靠Template操作如何输出,以及webpack-source帮助我们合并替换还是ast格式的模块。最后按照chunk合并一起输出。

Template的类一共有5个:

  • Template.js
  • MainTemplate.js
  • ModuleTemplate.js
  • RuntimeTemplate
  • ChunkTemplate.js

当然!模版替换是在Compilation中执行的,毕竟Compilation就像一个指挥者,指挥者大家如何按顺序一个个编译。

Compilation.seal触发了MainTemplate.getRenderManifest,获取需要渲染的信息,接着通过中的钩子触发了mainTemplate.hooks.renderManifest这个钩子,调用了JavascriptModulePlugin中相应的函数,创建了一个含有打包信息的fileManifest返回备用。

result.push({
render: () =>
compilation.mainTemplate.render(
hash,
chunk,
moduleTemplates.javascript,
dependencyTemplates
),
filenameTemplate,
pathOptions: {
noChunkHash: !useChunkHash,
contentHashType: "javascript",
chunk
},
identifier: `chunk${chunk.id}`,
hash: useChunkHash ? chunk.hash : fullHash
});
复制代码
createChunkAssets(){
//...
const manifest = template.getRenderManifest(...)//获取渲染列表
//...
for (const fileManifest of manifest) {
//...
source = fileManifest.render();
//...
}
//...
} 复制代码

准备工作做完之后就要开始渲染了,调用了fileManifest的render函数,其实就是mainTemplate.rendermainTemplate.render触发了hooks.render这个钩子,返回了一个ConcatSource的资源。其中有固定的模板,也有调用的模块。

//...
this.hooks.render.tap("MainTemplate",(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
const source = new ConcatSource();
source.add("/******/ (function(modules) { // webpackBootstrap\n");
//...
source.add(
this.hooks.modules.call(//获取模块的资源
new RawSource(""),
chunk,
hash,
moduleTemplate,
dependencyTemplates
)
);
source.add(")");
return source;
}
);
//..
render(hash, chunk, moduleTemplate, dependencyTemplates) {
//...
let source = this.hooks.render.call(
new OriginalSource(
Template.prefix(buf, " \t") + "\n",
"webpack/bootstrap"
),
chunk,
hash,
moduleTemplate,
dependencyTemplates
);
//...
return new ConcatSource(source, ";");
}
复制代码

各个模块的模板替换MainTemplate将任务分配给了Template,让他去处理模块们的问题,于是调用了Template.renderChunkModules这个方法。这个方法首先是获取所有模块的替换资源。

static renderChunkModules(chunk,filterFn,moduleTemplate,dependencyTemplates,prefix = ""){
const source = new ConcatSource();
const modules = chunk.getModules().filter(filterFn);
//...
const allModules = modules.map(module => {
return {
id: module.id,
source: moduleTemplate.render(module, dependencyTemplates, {
chunk
})
};
});
//...
//...
}
复制代码

然后ModuleTemplate再去请求NormalModule.source这个方法。这里的module便使用了Factory给他配备的generator,生成了替换代码,generate阶段的时候会请求RuntimeTemplate,根据名字可以得知,是用于替换成运行时的代码。

source(dependencyTemplates, runtimeTemplate, type = "javascript") {
//...
const source = this.generator.generate(
this,
dependencyTemplates,
runtimeTemplate,
type
);
const cachedSource = new CachedSource(source);
//..
return cachedSource;
}
复制代码

然后丢入NormalModule将此变为cachedSource,返回给ModuleTemplate进一步处理。ModuleTemplate在对这个模块进行打包,最后出来的效果是这样的:

我们再回到Template,继续处理,经过ModuleTemplate的处理之后,我们返回的数据长这样。

革命尚未结束!替换仍在进行!我们回到Template.renderChunkModules,继续替换。

static renderChunkModules(chunk,filterFn,moduleTemplate,dependencyTemplates,prefix = ""){
const source = new ConcatSource();
const modules = chunk.getModules().filter(filterFn);
//...如果没有模块,则返回"[]"
source.add("[]");
return source;
//...如果有模块则获取所有模块
const allModules = modules.map(//...);
//...开始添加模块
source.add("[\n");
//...
source.add(`/* ${idx} */`);
source.add("\n");
source.add(module.source);
source.add("\n" + prefix + "]");
//...
return source;
}
复制代码

我们将ConcatSource返回至MainTemplate.render(),再加个;,然后组合返回至Compliation.createChunkAssets

到此seal中template就告一段落啦。至于生成文件,那就是通过webpack-source这个包,将我们的饿数组变成字符串然后拼接,最后输出。

所有图片素材均出自笔者之手,欢迎大家转载,请标明出处。毕竟捣鼓了一个多月,感觉自己都要秃了。

在酝酿下一篇研究什么了。感觉loader还需要多扒扒。(笑~)

从Webpack源码探究打包流程,萌新也能看懂~的更多相关文章

  1. .17-浅析webpack源码之compile流程-入口函数run

    本节流程如图: 现在正式进入打包流程,起步方法为run: Compiler.prototype.run = (callback) => { const startTime = Date.now( ...

  2. .20-浅析webpack源码之compile流程-Template模块

    这里的编译前指的是开始触发主要的事件流this-compilaiton.compilation之前,由于还有一些准备代码,这一节全部弄出来. 模块基本上只走构造函数,具体的方法调用的时候再具体讲解. ...

  3. .2-浅析webpack源码之打包后文件

    先不进源码,分析一下打包后的文件,来一张图: 首先创建两个JS文件,内容如下: // config.js module.exports = { entry: './input.js', output: ...

  4. .18-浅析webpack源码之compile流程-rules参数处理(1)

    Tips:写到这里,需要对当初的规则进行修改.在必要的地方,会在webpack.config.js中设置特殊的参数来跑源码,例如本例会使用module:{rules:[...]}来测试,基本上测试参数 ...

  5. .19-浅析webpack源码之compile流程-rules参数处理(2)

    第一步处理rule为字符串,直接返回一个包装类,很简单看注释就好了. test/include/exclude 然后处理test.include.exclude,如下: if (rule.test | ...

  6. 萌新也能看懂的KMP算法

    前言 算法是什么?算法就是数学规律.怎么去总结和发现这个规律,就是理解算法的过程. KMP算法的本质是穷举法,而并不是去创造新的匹配逻辑. 以下将搜寻的字符串称为子串(part),以P表示.被搜寻的字 ...

  7. .30-浅析webpack源码之doResolve事件流(1)

    这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...

  8. .34-浅析webpack源码之事件流make(3)

    新年好呀~过个年光打游戏,function都写不顺溜了. 上一节的代码到这里了: // NormalModuleFactory的resolver事件流 this.plugin("resolv ...

  9. .30-浅析webpack源码之doResolve事件流(2)

    这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...

随机推荐

  1. C语言中static extern的使用

    10:30:22 2019-08-20 基础不牢 瞬间爆炸 参考资料:https://blog.csdn.net/ts_54eagle/article/details/4418627 https:// ...

  2. ubuntu16.04下安装java8

    从Oracle官网下载jdk,jdk-8u231-linux-x64.tar.gz (1)复制到/opt 目录内 sudo cp jdk-8u231-linux-x64.tar.gz /opt (3) ...

  3. PHP程序员的能力水平层次(一)

    前言 之前看过很多篇关于服务端工程师和PHP开发者的能力模型介绍,每篇都对能力有侧重点. 下面我们来详细谈谈以开发能力为基准点的PHP程序员的能力水平层次. 层层递进 1.功能开发 这个水平的程序员一 ...

  4. Go语言 可变参数

    最近与同事讨论时,提到Go语言的可变参数,之前没有总结过相关知识点,今天我们介绍一下Go语言的可变参数. 可变参数(Variable Parameters):参数数量可变的函数称之为可变参数函数,主要 ...

  5. redis 浅谈事务

    写在前面的话 之前在某个网站上看到一个问题:redis在什么情况下出现事务不会滚的情况,以此为由并结合redis官方文档整理这边笔记.不足之处,请指出,谢谢. 事务 redis支持事务,提供两条重要的 ...

  6. svg整体缩放至指定大小

    一.问题 svg画面跑在分辨率低的电脑上,导致不能完全显示. 二.要求 svg要能够根据电脑的屏幕大小自动缩放至适配电脑的尺寸. 三.实现 1.获取本机窗口高度.宽度 let clientWidth ...

  7. 干货:python面对对象类继承简介

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:python视觉算法 PS:如有需要Python学习资料的小伙伴可以加 ...

  8. Python程序设计 实验 1 熟悉 IDLE 和在线编程平台

    ------------恢复内容开始------------ 安徽工程大学 Python程序设计 实验报告 班级   物流191   姓名  姚彩琴  学号3190505129 成绩 日期     2 ...

  9. 多线程高并发编程(5) -- CountDownLatch、CyclicBarrier源码分析

    一.CountDownLatch 1.概念 public CountDownLatch(int count) {//初始化 if (count < 0) throw new IllegalArg ...

  10. G. 蚂蚁的镜像串

    单点时限: 1.0 sec 内存限制: 512 MB 一只聪明的蚂蚁在学习了回文串之后,一直觉得回文串不够优美,所以它决定自己定义一种新的字符串——镜像串 所谓镜像串,就是对一个字符串进行一整个完全的 ...