zoukankan      html  css  js  c++  java
  • 从Webpack源码探究打包流程,萌新也能看懂~

    简介

    上一篇讲述了如何理解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
    ");
    	    //...
    		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, " 	") + "
    ",
    			"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("[
    ");
    	//...
    	    source.add(`/* ${idx} */`);
    		source.add("
    ");
    		source.add(module.source);
    		source.add("
    " + prefix + "]");
    	//...
    	return source;
    }
    复制代码

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

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

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

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

  • 相关阅读:
    Converting PDF to Text in C#
    Working with PDF files in C# using PdfBox and IKVM
    Visualize Code with Visual Studio
    Azure Machine Learning
    Building Forms with PowerShell – Part 1 (The Form)
    ML.NET is an open source and cross-platform machine learning framework
    Microsoft Visual Studio Tools for AI
    Debugging Beyond Visual Studio – WinDbg
    Platform.Uno介绍
    Hawk-数据抓取工具
  • 原文地址:https://www.cnblogs.com/twodog/p/12135523.html
Copyright © 2011-2022 走看看