webpack源码分析

webpack作为一种流行的打包工具被广泛应用在web项目的前端工程化构建中.单配置文件,各种插件形式的loader能很方便的把各种源码构建成浏览器环境能运行的js代码。webpack的构建流程是如何的?一个个独立的依赖文件怎么被整合成目标js的?源码是何时被转义成浏览器可运行标准的?我自己要定制打包过程从何入手?这篇文章就来解答这几个问题。

 

0 Tapable.js与webpack核心路径的实现

在我们写好webpack.config.js文件后,terminal输入webpack命令时,nodejs会调用node_moudule/webpack/webpack.js文件里的webpack方法,一切的构建从这里开始。基本概念:webpack生成的最终js文件叫chunk,代码块的概念,单独文件对应chunk里包含若干的小逻辑模块叫module。主要的步骤如下:

compile

开始编译 根据配置文件不同区分单入口或多入口

 

make

从入口点分析模块,建立module对象,他依赖的其他module都写入对应数组保存

 

build 构建模块

(make中的一个步骤)build时调用runloader找到加载每种不同资源的loader进行处理,最后转换成js后用arcon.js把源码转换成AST语法树,再分析语法树,提取依赖关系存入供下一步使用

 

seal 封装构建结果

根据配置和之前分析的依赖关系分配代码,把module粒度的代码加入到对应的chunk(代码块)里,每一个chunk最后就对应一个输出的目标js (提取公共js在这里完成)

 

emit 输出文件

把各个chunk输出到结果文件,根据chunk类型的不同,使用不同的模版生成目标js

MainTemplate.js , 主文件模版,包括__webpack_require__在浏览器的实现

ChunkTemplate.js ,chunk文件模版 对拆分出来的chunk加包装代码

ModuleTemplate.js,module文件模版,对所有module加包装代码

HotUpdateChunkTemplate.js 热替换模版,对所有热替换模块加包装代码

 

简叙的流程逻辑就是这些,其中有许多步骤是使用订阅/发布模型实现的。这种模型可以把像webpack这种细分子流程

非常多且繁琐的程序拆分成一个个相对简单的任务来实现。

Tapable.js

webpack的订阅/发布实现是基于Tapable.js

Tapable这里不再赘述,具体细节可以搜github,简单说两个基本方法:

Tapable.plugin('name',function(){ 实现代码 })
void plugin(names: string|string[], handler: Function)

plugin方法可以给观察者系统注册一个订阅者函数

 

Tapable.applyPlugins('name',参数...)
void applyPlugins(name: string, args: any...)  

applyPlugins方法可以向观察者系统发布一个通知,来调用对应的订阅者来进行处理。

 

通过Tapable.js的拆分,我们看到webpack源码中充满了各种的订阅和发布,这也十分便于我们理解和下手修改。

1 Compile过程 webpack参数传入和启动编译

查看node_moudule/webpack/webpack.js源码,

  1. webapck对options的参数进行了格式验证处理,并摘出
  2. options中的plugin在订阅者管理器中进行了注册,以便后续流程调用。把验证后的options传给Compiler.js 调用run方法开始编译。
  3. Compiler.js开始工作后,调用Complation.js中的实现来进行:建立模块--loader处理--封装结果--输出文件几个步骤

2 Make过程

Make是代码分析的核心流程。他包括了创建模块,构建模块的工作。

而构建模块步骤又包含了

  1. loader来处理资源
  2. 处理后的js进行AST转换并分析语句
  3. 语句中发现的依赖关系再处理拆分成dep,即依赖module,形成依赖网

Make步骤的入口在订阅者"make"的实现中,我们举单入口构建的例子来看

compiler.plugin("make", (compilation, callback) => {
		//module工厂函数
	    const dep = 
            SingleEntryPlugin.createDependency(this.entry, this.name); 
		//开始构建!
	compilation.addEntry(this.context, dep, this.name, callback);
});

在这里传入了单入口构建型模块的工厂函数给compilation的addEntry方法开始构建模块。

进入compilation.addEntry方法后,核心步骤是通过

_addModuleChain() 来把处理的代码加入module链中,以便给后续的封装步骤使用

//根据addEntry传入的工厂函数类型得到对应的工厂函数实体
const moduleFactory = 
this.dependencyFactories.get(dependency.constructor);	
if(!moduleFactory) {
 throw new Error(`No dependency factory available 
 for this dependency type: ${dependency.constructor.name}`);
}

//找到对应的modue构建工厂创建module
moduleFactory.create({},(err, module) => {
   //核心流程 处理源码
	this.buildModule();
   //处理module建的依赖关系
	this.processModuleDependencies();
  })

3 buildModule过程解析

在make过程中,最核心的就是buildModule流程,这个步骤主要有3个核心任务:

1、通过runLoaders函数找到对应的loader(css-loader,vue-loader,babel-loader...)处理源码

//NormalModule.js里dobuild的实现
doBuild(options, compilation, resolver, fs, callback) {
   ...
   //构建loader运行的上下文环境
   const loaderContext = 
   this.createLoaderContext(resolver, options, compilation, fs);
	
  
  //环境,loader列表,源码传给runLoaders
   runLoaders({
      resource: this.resource,
      loaders: this.loaders,
      context: loaderContext,
      readResource: fs.readFile.bind(fs)
   }  

2 & 3、通过parser.parse方法,把源码解析成AST树,并且记录源码依赖关系

在上一步doBuild后的回调里,对已经转化成js的文件进行了如下处理

this.parser.parse(this._source.source(), {
					current: this,
					module: this,
					compilation: compilation,
					options: options
				});
//解析源文件的内容

文件转换AST以及语句的遍历处理都在webpack/lib/parser.js中实现,简单介绍几个点,其他处理

读者可以自行查看对应代码

ast = acorn.parse(source, {
				ranges: true,
				locations: true,
				ecmaVersion: 2017,
				sourceType: "module",
				plugins: {
					dynamicImport: true
				},
				onComment: comments
			});
//使用acorn.js将源码转化


this.walkStatements(ast.body);
//遍历ast树上的所有语句


parser.plugin("import", (statement, source) => {
const dep =
new HarmonyImportDependency(source, 
HarmonyModulesHelpers.getNewModuleVar(parser.state, source), 
statement.range);
			dep.loc = statement.loc;
			parser.state.current.addDependency(dep);
			parser.state.lastHarmonyImport = dep;
			return true;
		});
//遇到import ,require等语句时根据commonJS,HarmonyImport,AMD,UMD等不同语法对依赖关系进行解析记录 

4 seal过程 封装代码

当冗长耗时loader处理源码,遍历module依赖关系后,我们得到了一个巨大的AST树结构的module map,

我们就要回到Complation对象中的seal方法来把代码封成浏览器里运行的模块了。

seal(){
	self.preparedChunks.forEach(preparedChunk => {
			const module = preparedChunk.module;
			const chunk = self.addChunk(preparedChunk.name, module);
			const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name);
			entrypoint.unshiftChunk(chunk);

			chunk.addModule(module);
			//把module加入对应的chunk里,准备最后输出成一个文件
			module.addChunk(chunk);
			//记录最后含有module的chunk列表在一个数组中
			chunk.entryModule = module;
			self.assignIndex(module); 
			//给module赋值int型的moduleID 这就是最终源码里的webpackJsonp([1,2]) 依赖数字的由来
			self.assignDepth(module);
			self.processDependenciesBlockForChunk(module, chunk);
		}

        this.createChunkAssets(); // 生成最终assets		
}

再经历了seal的步骤后,我们的一个个module块已经被分配到各自的chunk中去了,准备最后写入文件。但是依赖关系都是ES6的,要想最后浏览器运行还需要加个pollify,webpack针对几种类型的文件有对应的模版来处理

createChunkAssets(){
	//这个方法调用不同的模版来处理源文件用于输出,我们用MainTemplate.js举例,最终浏览器运行顶部的代码模版就在这里定义
	if(chunk.hasRuntime()) {
						source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
					} else {
						source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
					}
}

//这段代码输出了runtime函数,既浏览器环境实现的webpack模块加载器
	
this.plugin("render", (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
			const source = new ConcatSource();
			source.add("/******/ (function(modules) { // webpackBootstrap\n");
			source.add(new PrefixSource("/******/", bootstrapSource));
			source.add("/******/ })\n");
			source.add("/************************************************************************/\n");
			source.add("/******/ (");
			const modules = this.renderChunkModules(chunk, moduleTemplate, dependencyTemplates, "/******/ ");
			source.add(this.applyPluginsWaterfall("modules", modules, chunk, hash, moduleTemplate, dependencyTemplates));
			source.add(")");
			return source;
		});

5 emitAssets 最后输出我们处理好的结果到本地文件中

经历了这么多步骤,终于在Compiler.compile方法完成后的回调中,我们要调用emitAssets方法进行最后输出工作,把代码写到结果文件中去。

Compiler.prototype.emitAssets = function(compilation, callback) {
   this.outputFileSystem.writeFile(targetPath, content, callback);
}

 

Webpack工作原理总结

基于订阅/发布模型建立的Webpack打包工具把一个个繁杂耦合的前端源代码处理工作拆分成了很多个细小的任务。通过Tapable.plugin来注册一个个订阅器就可以在webpack工作中的某个具体步骤插入你的处理逻辑。这种插片式的计方便我们低耦合的对前端打包流程进行自定义。

我们研究webpack的目的是因为他在目前的前端项目中比较流行,为了能更好的使用他,通过改进打包流程的过程而不是业务代码来实现整站的优化。前端工程工具日新月异,但是基本的思想都是在通过对js语言的预处理来实现其他语言/开发模式中的优秀方式,达到规范前端开发提升开发效率的目的。在对webpack优化中,我们也尽量使用plugin系统来实现流程优化而不是再发明新的api,节约开发成本的同时也更好的兼容业务项目向未来构建工具的迁移。

posted @ 2020-03-16 16:57  DAVENEE  阅读(449)  评论(0编辑  收藏  举报