前言
在写浅谈webpack2–学会配置(一)的时候,就想把plugins单独拿出来说。但是webpack使用了tapable来实现插件对事件订阅执行,使得插件和webpack打包流程密不可分,所以这篇文章就不得不提webpack的整个打包流程。
webpack打包流程
流程图
了解tapable和webpack核心概念之后,我们来看一张webpack流程图,我们将根据这张流程图进行源码分析。
(这张图是来自网络)
配置和shell解析
这是webpack打包流程的第一步。这一步分为2种不一样的情况:
- 通过命令执行输入
webpack
中执行,程序会执行bin/webpack.js脚本文件。 - 通过nodejs中
require('webpack')
执行,程序会去执行lib/webpack.js文件。
(如果不懂为什么的同学,可以去先去了解package.json的配置具体含义。)
bin/webpack.js源代码解析:
yargs
yargs和commander,optimist一样都是命令行工具。执行webpack --config webpack.config.js
,
通过var argv = yargs.argv;var config = argv.config
就能获取命令行输入。
convert-argv.js
请求配置文件,将配置信息加入options对象中。再根据判断命令行配置信息,对options对象添加Loader,plugins,output,entry等配置属性。
convert-argv.js源码解析:
注意:源码已经改变。比如:loader使用的preLoaders,postLoaders明显已经不是新版的webpack配置了。新webpack使用rule.enforce
等于post
或pre
代替。
模块创建与构建
compilation 对象
当options配置信息设置完成后,执行了compiler.run()
,开始编译和构建。run首先触发before-run
和run
事件,提供一个增强型的文件系统,然后执行compile方法。compile方法首先获取compilation构造函数的入参对象(包含两种类型的模块工厂和一个依赖数组)。然后执行before-compile
和compile
事件,再生成compilation对象。然后触发make
事件来调用SingleEntryPlugin或者MultiEntryPlugin两个插件中的compilation.addEntry
方法。
compilation.addEntry调用源码解析:
在前面一章讲解compiler的时候说过,执行WebpackOptionsApply对象的process方法,根据配置加载插件。就是这个时候EntryOptionPlugin插件被加载到程序中,该插件根据入口文件配置类型加载不同的插件,我这里只写了SingleEntryPlugin。当触发make
事件时,调用compilation.addEntry
方法。
模块创建和构建主流程
compilation.addEntry最主要调用了私有方法_addModuleChain。_addModuleChain根据不同的入口依赖类型获取不同的模块工厂函数进行模块创建,再执行buildModule函数对模块进行构建。
_addModuleChain源码解析:
模块详细构建过程分析
buildModule方法会调用每个模块都具有的build方法,进行模块的构建。然而每种类型的模块构建方式都不一样。例如,因为MultiModule类型模块实质是由多个依赖模块构成的混合模块,所以MultiModule的build方法只是去标明该模块已经被构建,而它的依赖模块会在后面的依赖处理中被构建。NormalModule类型模块的构建,先会被不同loader处理,然后将处理完成的源文件用acorn解析生成AST(抽象语法树)。最后被依赖解析插件遍历AST,将依赖加入dependencies数组。
MultiModule类型构建源码解析:
NormalModule类型构建过程源码解析:
模块loader处理流程
1234567891011121314151617181920//options 配置信息//compilation compilation//resolver 根据options.resolve配置,生成的解析工具//fs inputFileSystemdoBuild(options, compilation, resolver, fs, callback) {this.cacheable = false;//loaderContext对象包含各种编译信息,resolve,resolveSync等const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);//执行//this.loaders 由NormalModuleFactory中触发resolver事件后返回的函数,根据信息判断,返回的不同loadersrunLoaders({resource: this.resource,loaders: this.loaders,context: loaderContext,readResource: fs.readFile.bind(fs)}, (err, result) => {... //将loader处理后的资源赋值给模块属性});}loader处理后,acorn解析生成AST
123456789101112131415161718192021222324252627build(options, compilation, resolver, fs, callback) {... //添加module信息//执行doBuild,对模块进行loader处理。return this.doBuild(options, compilation, resolver, fs, (err) => {...//noParse为ture,将不会生成AST和依赖查找,也就不会构建依赖了const noParseRule = options.module && options.module.noParse;if (this.shouldPreventParsing(noParseRule, this.request)) {return callback();}// parse中,用acorn解析生成ASTtry {this.parser.parse(this._source.source(), {current: this,module: this,compilation: compilation,options: options});}...return callback();});}遍历AST,收集依赖
1234567891011121314151617181920212223//this.parser.parse函数parse(source, initialState){...try {comments.length = 0;// 关于需要解析的类型,解析插件配置POSSIBLE_AST_OPTIONS[i].onComment = comments;// 用acorn解析生成ASTast = acorn.parse(source, POSSIBLE_AST_OPTIONS[i]);} catch (e) {}...//触发AMDPlugin,DefinePlugin等解析器插件的"expression "+outerExpr事件//然后根据AST抽象语法树,对依赖进行添加if (this.applyPluginsBailResult("program", ast, comments) === undefined) {this.prewalkStatements(ast.body);this.walkStatements(ast.body);}}loader处理前后资源变化:
123456789//这里使用了babel-loader,设置 "presets": ["es2015"]//前import React from 'react';//后;var _react = require('react');var _react2 = _interopRequireDefault(_react);function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
模块依赖构建过程分析
当前模块构建完成后,对其依赖处理。processModuleDependencies将模块依赖数组传入addModuleDependencies函数。该函数对依赖进行遍历,异步构建每一个模块。
addModuleDependencies源码解析:
整个模块的创建和构建完成后,触发finish-modules
事件。
模块封装与输出
模块封装
seal方法
模块和依赖构建完成后,调用compilation的seal方法触发seal
事件。遍历preparedChunk,生成chunk。处理模块依赖,当模块属性blocks为true时(也就是异步加载依赖模块时),生成新的chunk。然后调用各种优化事件对chunk和模块等各种优化处理,生成hash。最后通过调用createModuleAssets
和createChunkAssets
,进行模块封装,生成source。赋值给this.assets[file]
生成最终asset。
seal源码解析:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950seal(callback){const self = this;self.applyPlugins0("seal");self.nextFreeModuleIndex = 0;self.nextFreeModuleIndex2 = 0;// 遍历preparedChunks,preparedChunks中包含了入口文件构建后的模块self.preparedChunks.forEach(preparedChunk => {const module = preparedChunk.module;//创建chunk, compilation中添加chunkconst chunk = self.addChunk(preparedChunk.name, module);//模块封装时,会根据entrypoint选择不同的模板对象进行封装const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name);//entrypoint添加chunk,chunk添加entrypointentrypoint.unshiftChunk(chunk);//chunk装载模块chunk.addModule(module);module.addChunk(chunk);chunk.entryModule = module;self.assignIndex(module);self.assignDepth(module);//block模块添加chunk,但是没有entrypoint//当module.blocks为true,也就是异步加载的依赖时,会新生成并添加chunkself.processDependenciesBlockForChunk(module, chunk);});//优化编译,树的异步优化,模块的优化,chunk的优化等等...//hash生成self.createHash();...//modules中assets赋值给compilation.assets数组//module.assets: 某些loader执行this.emitFile后,生成 module.assets ,所以开发在自定义loader时,可以使用this.emitFile来最终输出文件//file-loader中就要使用this.emitFileself.createModuleAssets();//根据chunk.hasRuntime()调用不同模板,生成不同的source// chunk.hasRuntime()源码:if(this.entrypoints.length === 0) return false;return this.entrypoints[0].chunks[0] === this;self.createChunkAssets();...//生成最终assetsthis.assets[file] = source;chunk.files.push(file);...createChunkAssets调用Template生成source(结果代码)
createChunkAssets中遍历chunk,根据判断是否为入口文件,还是异步加载文件,调用mainTemplate和chunkTemplate两种不同的模板,生成source。
createChunkAssets源码解析:1234567891011121314for (let i = 0; i < this.chunks.length; i++) {...//缓存if (this.cache && this.cache[cacheName] && this.cache[cacheName].hash === usedHash) {source = this.cache[cacheName].source;} else {//生成项目入口文件if (chunk.hasRuntime()) {source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);//生成异步加载文件} else {source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);}...Template详细生成source
mainTemplate和chunkTemplate对模块进行封装,其实除了这两种Template还有ModuleTemplate和HotUpdateChunkTemplate。ModuleTemplate会对依赖模块进行逐一的render,HotUpdateChunkTemplate热替换模块。
mainTemplate和chunkTemplate会遍历chunk中装载的模块,调用ModuleTemplate最后生成source。
mainTemplate源码解析:123456789101112131415//模块封装render(hash, chunk, moduleTemplate, dependencyTemplates){const buf = [];buf.push(this.applyPluginsWaterfall("bootstrap", "", chunk, hash, moduleTemplate, dependencyTemplates));buf.push(this.applyPluginsWaterfall("local-vars", "", chunk, hash));buf.push("");buf.push("// The require function");buf.push(`function ${this.requireFn}(moduleId) {`);...//调用ModuleTemplate类型模板的render方法,依次遍历chunk中每一个装载的模块。let source = this.applyPluginsWaterfall("render", new OriginalSource(this.prefix(buf, " \t") + "\n", `webpack/bootstrap ${hash}`), chunk, hash, moduleTemplate, dependencyTemplates);...return new ConcatSource(source, ";");}
chunkTemplate源码解析:
|
|
遍历,调用ModuleTemplate源码:
模块输出
从模块创建到asset生成可以说是webpack编译的整个过程,最后一步模块输出其实已经不涉及到编译。而是执行compiler.emitAssets方法,对模块进行输出。在输出之前,会触发emit事件,这也是开发者最后修改输出文件的机会。然后遍历compilation.assets异步的输出文件到outputPath的输出地址。
主要事件
- entry-option 该事件入参为content作用域和entry入口文件信息。webpack中使用该事件获取不同的插件,在最终生成不同类型的模块。
- run 入参compiler。
- compile 开始编译,compilation创建前触发,入参params。
- make 根据入口信息开始创建并构建模块和他依赖的模块时触发,入参compilation。
- build-module 模块开始构建时触发,入参为module。
- “expression “+outerExpr 入参expr webpack中根据AST抽象语法树,对依赖进行添加。
- seal 模块构建完成后,开始封装模块(创建chunk,为chunk装载module,根据依赖创建chunk,优化,生成hash)时触发。
- emit 将根据chunk封装后的assert输出成文件前触发。