Module
(模块)是webpack的中的关键实体。Webpack 会从配置的 Entry 开始递归找出全部依赖的模块. 经过Loaders
(模块转换器),用于把模块原内容按照需求转换成新模块内容.css
webpack总体是一个事件驱动架构,全部的功能都以Plugin
(插件)的方式集成在构建流程中,经过发布订阅事件来触发各个插件执行。webpack核心使用tapable来实现Plugin
(插件)的注册和调用,Tapable是一个事件发布(tap)订阅(call)库html
Graph 模块之间的Dependency
(依赖关系)构成的依赖图node
Compiler (Tapable
实例)订阅了webpack最顶层的生命周期事件webpack
Complilation (Tapable
实例)该对象由Compiler
建立, 负责构建Graph,Seal,Render...是整个工做流程的核心生命周期,包含Dep Graph 遍历算法,优化(optimize),tree shaking...git
Compiler 和 Compilation 的区别在于:Compiler 表明了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是表明了一次新的编译。github
Resolver(Tapable
实例)资源路径解析器web
ModuleFactory (Tapable
实例) 被Resolver
成功解析的资源须要被这个工厂类被实例化成Module
redis
Parser (Tapable
实例) 负责将Module
(ModuleFactory
实例化来的)转AST的解析器 (webpack 默认用acorn),并解析出不一样规范的require/import 转成Dependency(依赖)算法
Template 模块化的模板. Chunk,Module,Dependency都有各自的模块模板,来自各自的工厂类的实例mongodb
bundle
和 chunk
区别:https://github.com/webpack/webpack.js.org/issues/970
bundle
:由多个不一样的模块打包生成生成最终的js文件,一个js文件便是1个bundle。
chunk
: Graph的组成部分。通常有n个入口=n个bundle=graph中有n个chunk。但假设因为n个入口有m个公共模块会被重复打包,须要分离,最终=n+m个bundle=graph中有n+m个chunk
有3类chunk:
/******/
的部分 (是bundle)chunk的依赖图算法
https://medium.com/webpack/the-chunk-graph-algorithm-week-26-29-7c88aa5e4b4e
Compiler
读取配置,建立Compilation
Compiler
建立Graph
的过程:
Compilation
读取资源入口Resolver
解析Parser
解析 AST
Loader
(执行loader runner)Compilation
优化Graph
Compilation
渲染Graph
钩子名 | 执行方式 | 要点 |
---|---|---|
SyncHook | 同步串行 | 不关心监听函数的返回值 |
SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值不为null ,则跳过剩下全部的逻辑 |
SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值能够传给下一个监听函数 |
SyncLoopHook | 同步循环 | 当监听函数被触发的时候,若是该监听函数返回true 时则这个监听函数会反复执行,若是返回undefined 则表示退出循环 |
AsyncParallelHook | 异步并发 | 不关心监听函数的返回值 |
AsyncParallelBailHook | 异步并发 | 只要监听函数的返回值不为null ,就会忽略后面的监听函数执行,直接跳跃到callAsync 等触发函数绑定的回调函数,而后执行这个被绑定的回调函数 |
AsyncSeriesHook | 异步串行 | 不关心callback 的参数 |
AsyncSeriesBailHook | 异步串行 | callback() 的参数不为null ,就会直接执行callAsync 等触发函数绑定的回调函数 |
AsyncSeriesWaterfalllHook | 异步串行 | 上一个监听函数中的callback(err,data) 的第二个参数,能够做为下一个监听函数的参数 |
//建立一个发布订阅中心 let Center=new TapableHook() //注册监听事件 Center.tap('eventName',callback) //触发事件 Center.call(...args) //注册拦截器 Center.intercept({ context,//事件回调和拦截器的共享数据 call:()=>{},//钩子触发前 register:()=>{},//添加事件时 tap:()=>{},//执行钩子前 loop:()=>{},//循环钩子 })
更多示例 https://juejin.im/post/5abf33f16fb9a028e46ec352
它有不少子类:RawModule, NormalModule ,MultiModule,ContextModule,DelegatedModule,DllModule,ExternalModule 等
ModuleFactory: 使用工厂模式建立不一样的Module,有四个主要的子类: NormalModuleFactory,ContextModuleFactory , DllModuleFactory,MultiModuleFactory
js if(chunk.entry) { source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates); } else { source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates); }
js MainTemplate.prototype.requireFn = "__webpack_require__"; MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) { var buf = []; // 每个module都有一个moduleId,在最后会替换。 buf.push("function " + this.requireFn + "(moduleId) {"); buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash))); buf.push("}"); buf.push(""); ... // 其他封装操做 };
HotUpdateChunkTemplate 是对热替换模块的一个处理
function __webpack_require__(moduleId) { // 1.首先会检查模块缓存 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 2. 缓存不存在时,建立并缓存一个新的模块对象,相似Node中的new Module操做 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {}, children: [] }; // 3. 执行模块,相似于Node中的: // result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); //须要引入模块时,同步地将模块从暂存区取出来执行,避免使用网络请求致使过长的同步等待时间。 module.l = true; // 4. 返回该module的输出 return module.exports; }
__webpack_require__.e = function requireEnsure(chunkId) { var promises = []; var installedChunkData = installedChunks[chunkId]; // 判断该chunk是否已经被加载,0表示已加载。installChunk中的状态: // undefined:chunk未进行加载, // null:chunk preloaded/prefetched // Promise:chunk正在加载中 // 0:chunk加载完毕 if(installedChunkData !== 0) { // chunk不为null和undefined,则为Promise,表示加载中,继续等待 if(installedChunkData) { promises.push(installedChunkData[2]); } else { // 注意这里installChunk的数据格式 // 从左到右三个元素分别为resolve、reject、promise var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push(installedChunkData[2] = promise); // 下面代码主要是根据chunkId加载对应的script脚本 var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); var onScriptComplete; script.charset = 'utf-8'; script.timeout = 120; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } // jsonpScriptSrc方法会根据传入的chunkId返回对应的文件路径 script.src = jsonpScriptSrc(chunkId); onScriptComplete = function (event) { script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if(chunk !== 0) { if(chunk) { var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'); error.type = errorType; error.request = realSrc; chunk[1](error); } installedChunks[chunkId] = undefined; } }; var timeout = setTimeout(function(){ onScriptComplete({ type: 'timeout', target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; head.appendChild(script); } } return Promise.all(promises); };
// webpack runtime chunk function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var executeModules = data[2]; var moduleId, chunkId, i = 0, resolves = []; // webpack会在installChunks中存储chunk的载入状态,据此判断chunk是否加载完毕 for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } // 注意,这里会进行“注册”,将模块暂存入内存中 // 将module chunk中第二个数组元素包含的 module 方法注册到 modules 对象里 for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(data); //先根据模块注册时的chunkId,取出installedChunks对应的全部loading中的chunk,最后将这些chunk的promise进行resolve操做 while(resolves.length) { resolves.shift()(); } deferredModules.push.apply(deferredModules, executeModules || []); return checkDeferredModules(); };
function checkDeferredModules() { var result; for(var i = 0; i < deferredModules.length; i++) { var deferredModule = deferredModules[i]; var fulfilled = true; // 第一个元素是模块id,后面是其所需的chunk for(var j = 1; j < deferredModule.length; j++) { var depId = deferredModule[j]; // 这里会首先判断模块所需chunk是否已经加载完毕 if(installedChunks[depId] !== 0) fulfilled = false; } // 只有模块所需的chunk都加载完毕,该模块才会被执行(__webpack_require__) if(fulfilled) { deferredModules.splice(i--, 1); result = __webpack_require__(__webpack_require__.s = deferredModule[0]); } } return result; }
//lib/webpack.js options = new WebpackOptionsDefaulter().process(options); compiler = new Compiler(options.context); compiler.options = options; /*options:{ entry: {},//入口配置 output: {}, //输出配置 plugins: [], //插件集合(配置文件 + shell指令) module: { loaders: [ [Object] ] }, //模块配置 context: //工程路径 ... }*/
class Compiler extends Tapable { constructor(context) { super(); this.hooks = { shouldEmit: new SyncBailHook(["compilation"]),//此时返回 true/false。 done: new AsyncSeriesHook(["stats"]),//编译(compilation)完成。 additionalPass: new AsyncSeriesHook([]), beforeRun: new AsyncSeriesHook(["compiler"]),//compiler.run() 执行以前,添加一个钩子。 run: new AsyncSeriesHook(["compiler"]),//开始读取 records 以前,钩入(hook into) compiler。 emit: new AsyncSeriesHook(["compilation"]),//输出到dist目录 afterEmit: new AsyncSeriesHook(["compilation"]),//生成资源到 output 目录以后。 thisCompilation: new SyncHook(["compilation", "params"]),//触发 compilation 事件以前执行(查看下面的 compilation)。 compilation: new SyncHook(["compilation", "params"]),//编译(compilation)建立以后,执行插件。 normalModuleFactory: new SyncHook(["normalModuleFactory"]),//NormalModuleFactory 建立以后,执行插件。 contextModuleFactory: new SyncHook(["contextModulefactory"]),//ContextModuleFactory 建立以后,执行插件。 beforeCompile: new AsyncSeriesHook(["params"]),//编译(compilation)参数建立以后,执行插件。 compile: new SyncHook(["params"]),//一个新的编译(compilation)建立以后,钩入(hook into) compiler。 make: new AsyncParallelHook(["compilation"]),//从入口分析依赖以及间接依赖模块 afterCompile: new AsyncSeriesHook(["compilation"]),//完成构建,缓存数据 watchRun: new AsyncSeriesHook(["compiler"]),//监听模式下,一个新的编译(compilation)触发以后,执行一个插件,可是是在实际编译开始以前。 failed: new SyncHook(["error"]),//编译(compilation)失败。 invalid: new SyncHook(["filename", "changeTime"]),//监听模式下,编译无效时。 watchClose: new SyncHook([]),//监听模式中止。 } } }
this.name /** @type {string=} */ this.parentCompilation /** @type {Compilation=} */ this.outputPath = /** @type {string} */ this.outputFileSystem this.inputFileSystem this.recordsInputPath /** @type {string|null} */ this.recordsOutputPath /** @type {string|null} */ this.records = {}; this.removedFiles //new Set(); this.fileTimestamps /** @type {Map<string, number>} */ this.contextTimestamps /** @type {Map<string, number>} */ this.resolverFactory /** @type {ResolverFactory} */ this.options = /** @type {WebpackOptions} */ this.context = context; this.requestShortener this.running = false;/** @type {boolean} */ this.watchMode = false;/** @type {boolean} */ this._assetEmittingSourceCache /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */ this._assetEmittingWrittenFiles/** @private @type {Map<string, number>} */
compilation = new Compilation(compiler)
compilation.hooks.finish
compilation.hooks.seal
Compilation源码
Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被建立。Compilation 对象也提供了不少事件回调供插件作扩展。经过 Compilation 也能读取到 Compiler 对象。
承接上文的compilation = new Compilation(compiler)
插件能够用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量
plugin
: 一个具备 apply 方法的 JavaScript 对象。apply 方法会被 compiler 调用,而且 compiler 对象可在整个编译生命周期访问。这些插件包一般以某种方式扩展编译功能。
class MyPlugin{ apply(compiler){ compiler.hooks.done.tabAsync("myPlugin",(stats,cb)=>{ const assetsNames=[] for(let assetName in stats.compilation.assets) assetNames.push(assetName) console.log(assetsNames.join("\n")) cb() }) compiler.hooks.compilation.tap("MyPlugin",(compilation,params)=>{ new MyCompilationPlugin().apply(compilation) }) } } class MyCompilationPlugin{ apply(compilation){ compilation.hooks.additionalAssets.tapAsync('MyPlugin', callback => { download('https://img.shields.io/npm/v/webpack.svg', function(resp) { if(resp.status === 200) { compilation.assets['webpack-version.svg'] = toAsset(resp); callback() } else callback(new Error('[webpack-example-plugin] Unable to download the image')) }); }); } } module.exports=MyPlugin
其余声明周期hooks和示例 https://webpack.docschina.org/api/compilation-hooks/
在 NormalModuleFactory.js 的 resolver.resolve
中触发
hooks在 WebpackOptionsApply.js的 compiler.resolverFactory.hooks
中。
能够彻底被替换,好比注入本身的fileSystem
在 CommonJSPulgin.js的new CommonJsRequireDependencyParserPlugin(options).appply(parser)
触发,调用 CommonJsRequireDependencyParserPlugin.js 的apply(parser)
,负责添加Dependency,Template...
hooks在 CommonJsPlugin.js的 normarlModuleFactory.hooks.parser
中
在make阶段build中会调用doBuild去加载资源,doBuild中会传入资源路径和插件资源去调用loader-runner插件的runLoaders方法去加载和执行loader。执行完成后会返回以下图的result结果,根据返回数据把源码和sourceMap存储在module的_source属性上;doBuild的回调函数中调用Parser类生成AST,并根据AST生成依赖后回调buildModule方法返回compilation类。
NormalModuleFactory将loader分为preLoader、postLoader和loader三种
对loader文件的路径解析分为两种:inline loader和config文件中的loader。
require的inline loader路径前面的感叹号做用:
!
禁用preLoaders (代码检查和测试,不生成module)!!
禁用全部Loaders-!
禁用preLoaders和loaders,但不是postLoaders
前面提到NormalModuleFactory中的resolver钩子中会先处理inline loader。
最终loader的顺序:post
、inline
、normal
和pre
然而loader是从右至左执行的,真实的loader执行顺序是倒过来的,所以inlineLoader是总体后于config中normal loader执行的。
js //NormalModuleFactory.js let elements = requestWithoutMatchResource .replace(/^-?!+/, "") .replace(/!!+/g, "!") .split("!");
解析返回结果:
js [ // 第一个元素是一个loader数组 [ { loader: '/workspace/basic-demo/home/node_modules/html-webpack-plugin/lib/loader.js', options: undefined } ], // 第二个元素是模块自己的一些信息 { resourceResolveData: { context: [Object], path: '/workspace/basic-demo/home/public/index.html', request: undefined, query: '', module: false, file: false, descriptionFilePath: '/workspace/basic-demo/home/package.json', descriptionFileData: [Object], descriptionFileRoot: '/workspace/basic-demo/home', relativePath: './public/index.html', __innerRequest_request: undefined, __innerRequest_relativePath: './public/index.html', __innerRequest: './public/index.html' }, resource: '/workspace/basic-demo/home/public/index.html' } ]
ruleSet
的属性,至关于一个规则过滤器,会将resourcePath应用于全部的module.rules规则,它能够根据模块路径名,匹配出模块所需的loader。webpack编译会根据用户配置与默认配置,实例化一个RuleSet,它包含:
normalizeRule()
将配置值转换为标准化的test对象,其上还会存储一个this.references属性exec()
每次建立一个新的NormalModule时都会调用RuleSet实例的.exec()方法,只有当经过了各种测试条件,才会将该loader push到结果数组中。references {map}
key是loader在配置中的类型和位置,例如,ref-2表示loader配置数组中的第三个。
同一匹配(test)资源有多loader的时候:(相似先捕获,再冒泡)
loader.pitch()
(源码里是PitchingLoaders 不妨称为 pitch 阶段)loader()
(源码里是NormalLoaders 不妨称为 normal 阶段).这两个阶段(pitch
和normal
)就是loader-runner中对应的iteratePitchingLoaders()
和iterateNormalLoaders()
两个方法。
若是某个 loader 在 pitch 方法中return结果,会跳过剩下的 loader。那么pitch的递归就此结束,开始从当前位置从后往前执行normal
//webpack.config.js test: /\.pug/, use: [ 'apply-loader', 'pug-loader', ]
先执行pug-loader,获得 Module pug-loader/index.js!./src/index.pug
的js代码:
var pug = __webpack_require__(/*! pug-runtime/index.js */ "pug-runtime/index.js"); function template(locals) {var pug_html = "", pug_mixins = {}, pug_interp;pug_html = pug_html + "\\u003Cdiv class=\"haha\"\\u003Easd\\u003C\\u002Fdiv\\u003E";return pug_html;}; module.exports = template; //# sourceURL=webpack:///./src/index.pug?pug-loader
再执行apply-loader,获得 Module "./src/index.pug"
的js代码:
var req = __webpack_require__(/*! !pug-loader!./src/index.pug */ "pug-loader/index.js!./src/index.pug"); module.exports = (req['default'] || req).apply(req, []) //# sourceURL=webpack:///./src/index.pug?
此时假设在入口文件./src/index.js
引用
var html =__webpack_require__( './index.pug') console.log(html) //<div class="haha">asd</div>
这个入口文件 Module 的js代码:
module.exports = __webpack_require__(/*! ./src/index.js */"./src/index.js"); //# sourceURL=webpack:///multi_./src/index.js?
build 后可看到控制台输出的 1个Chunk,2个Module(1个fs忽略),3个中间Module和一些隐藏Module
Asset Size Chunks Chunk Names main.js 12.9 KiB main [emitted] main Entrypoint main = main.js [0] multi ./src/index.js 28 bytes {main} [built] [1] fs (ignored) 15 bytes {main} [optional] [built] [pug-loader/index.js!./src/index.pug] pug-loader!./src/index.pug 288 bytes {main} [built] [./src/index.js] 51 bytes {main} [built] [./src/index.pug] 222 bytes {main} [built]
pitch:顺序执行loader.pitch,例:
//webpack.config.js test: /\.css/, use: [ 'style-loader', 'css-loader', ]
style-loader(负责添加<style>
到页面)
获得Module ./src/a.css
的js代码:
// Load styles var content = __webpack_require__(/*! !css-loader/dist/cjs.js!./a.css */ "css-loader/dist/cjs.js!./src/a.css"); if(typeof content === 'string') content = [[module.i, content, '']]; // Transform styles var options = {"hmr":true} options.transform = undefined options.insertInto = undefined; // Add styles to the DOM var update = __webpack_require__(/*! style-loader/lib/addStyles.js */ "style-loader/lib/addStyles.js")(content, options); module.exports = content.locals; //# sourceURL=webpack:///./src/a.css?
build 后可看到控制台输出的 1个Chunk,1个最终Module,3个中间Module,和一些隐藏Module
Asset Size Chunks Chunk Names main.js 24.3 KiB main [emitted] main Entrypoint main = main.js [0] multi ./src/index.js 28 bytes {main} [built] [./node_modules/_css-loader@2.1.1@css-loader/dist/cjs.js!./src/a.css] 170 bytes {main} [built] [./src/a.css] 1.12 KiB {main} [built] [./src/index.js] 16 bytes {main} [built] + 3 hidden modules
其余loader解析:bundle loader , style-loader , css-loader , file-loader, url-loader
happypack
loader的内部处理流程:流水线机制,即挨个处理每一个loader,前一个loader的结果会传递给下一个loader。
loader有一些主要的特性:同步&异步; pitch&normal; context
runLoaders方法调用iteratePitchingLoaders去递归查找执行有pich属性的loader;若存在多个pitch属性的loader则依次执行全部带pitch属性的loader,执行完后逆向执行全部带pitch属性的normal的normal loader后返回result,没有pitch属性的loader就不会再执行;若loaders中没有pitch属性的loader则逆向执行loader;执行正常loader是在iterateNormalLoaders方法完成的,处理完全部loader后返回result;
用 loader 编译 Module 的主要步骤
compilation.addEntry()
方法中调用的_addModuleChain()
会执行一系列的模块方法,其中对于未build过的模块,最终会调用到NormalModule.doBuild()
方法。loaderContext
的对象doBuild()
run Loaders后将js代码经过acorn转为AST (源码) Parser中生产AST语法树后调用walkStatements方法分析语法树,根据AST的node的type来递归查找每个node的类型和执行不一样的逻辑,并建立依赖。
runLoaders()
iteratePitchingLoaders()
递归执行,并记录loader的pitch状态;loaderIndex++;当达到最大的loader序号时,处理实际的module(源码)://递归执行每一个loader的pitch函数,并在全部pitch执行完后调用processResource if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback);
processResource()
将目标module当作loaderContext的一个依赖,添加该模块为依赖和读取模块内容iterateNormalLoaders()
递归执行normal,和pitch的流程大同小异,须要注意的是顺序是反过来的,从后往前。,loaderIndex--.addDependency()
触发(不将该模块资源添加进依赖),并且没法读取模块的文件内容。loader会将pitch返回的值做为“文件内容”来处理,并返回给webpack。
runSyncOrAsync()
pitch与normal的实际执行 (源码)
往context
上添加了async
和callback
函数.
当咱们编写loader调用this.async()
或this.callback()
时,会将loader变为一个异步的loader,并返回一个异步回调,还能够直接返回一个Promise。
只有isSync标识为true时,才会在loader function执行完毕后当即(同步)回调callback来继续loader-runner。
version:number 2//版本 emitWarning(warning: Error)//发出一个警告 emitError(error: Error)//发出一个错误 resolve(context: String, request: String, callback: function(err, result: string)),//像 require 表达式同样解析一个 request getResolve(),//? emitFile(name: string, content: Buffer|string, sourceMap: {...}),//产生一个文件 rootContext:'/home/seasonley/workplace/webpack-demo',//从 webpack 4 开始,原先的 this.options.context 被改进为 this.rootContext webpack:true,//若是是由 webpack 编译的,这个布尔值会被设置为真(loader 最初被设计为能够同时当 Babel transform 用) sourceMap:false,//是否生成source map _module:[Object:NormalModule], _compilation:[Object:Compilation], _compiler:[Object:Compiler], fs:[Object:CachedInputFileSystem],//用于访问 compilation 的 inputFileSystem 属性。 target:'web',//编译的目标。从配置选项中传递过来的。示例:"web", "node" loadModule(request: string, callback: function(err, source, sourceMap, module))],//解析给定的 request 到一个模块,应用全部配置的 loader ,而且在回调函数中传入生成的 source 、sourceMap 和 模块实例(一般是 NormalModule 的一个实例)。若是你须要获取其余模块的源代码来生成结果的话,你可使用这个函数。 context: '/home/seasonley/workplace/webpack-demo/src',//模块所在的目录。能够用做解析其余模块路径的上下文。 loaderIndex: 0,//当前 loader 在 loader 数组中的索引。 loaders:Array [ { path: '/home/seasonley/workplace/webpack-demo/src/myloader.js', query: '', options: undefined, ident: undefined, normal: [Function], pitch: undefined, raw: undefined, data: null, pitchExecuted: true, normalExecuted: true, request: [Getter/Setter] } ],//全部 loader 组成的数组。它在 pitch 阶段的时候是能够写入的。 resourcePath: '/home/seasonley/workplace/webpack-demo/src/index.js',//资源文件的路径。 resourceQuery: '',//资源的 query 参数。 async(),//告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。 callback(err,content,sourceMap,meta),/*一个能够同步或者异步调用的能够返回多个结果的函数。若是这个函数被调用的话,你应该返回 undefined 从而避免含糊的 loader 结果。 this.callback( err: Error | null, content: string | Buffer, sourceMap?: SourceMap, meta?: any ); 能够将抽象语法树AST(例如 ESTree)做为第四个参数(meta),若是你想在多个 loader 之间共享通用的 AST,这样作有助于加速编译时间。*/ cacheable(flag),/*设置是否可缓存标志的函数: cacheable(flag = true: boolean) 默认状况下,loader 的处理结果会被标记为可缓存。调用这个方法而后传入 false,能够关闭 loader 的缓存。 一个可缓存的 loader 在输入和相关依赖没有变化时,必须返回相同的结果。这意味着 loader 除了 this.addDependency 里指定的之外,不该该有其它任何外部依赖。*/ addDependency(file),//加入一个文件做为产生 loader 结果的依赖,使它们的任何变化能够被监听到。例如,html-loader 就使用了这个技巧,当它发现 src 和 src-set 属性时,就会把这些属性上的 url 加入到被解析的 html 文件的依赖中。 dependency(file),// addDependency的简写 addContextDependency(directory),//(directory: string)把文件夹做为 loader 结果的依赖加入。 getDependencies(),// getContextDependencies(),// clearDependencies(),//移除 loader 结果的全部依赖。甚至本身和其它 loader 的初始依赖。考虑使用 pitch。 resource: [Getter/Setter],//request 中的资源部分,包括 query 参数。示例:"/abc/resource.js?rrr" request: [Getter],/*被解析出来的 request 字符串。"/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr"*/ remainingRequest: [Getter],// currentRequest: [Getter],// previousRequest: [Getter],// query: [Getter],/** 若是这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象。 若是 loader 中没有 options,而是以 query 字符串做为参数调用时,this.query 就是一个以 ? 开头的字符串。 使用 loader-utils 中提供的 getOptions 方法 来提取给定 loader 的 option。*/ data: [Getter]//在 pitch 阶段和正常阶段之间共享的 data 对象。 /* Object.defineProperty(loaderContext, "data", { enumerable: true, get: function() { return loaderContext.loaders[loaderContext.loaderIndex].data; } }); */
function myLoader(resource) { if(/\.js/.test(this.resource)) return resource+';console.log(`wa js`);'; }; module.exports = myLoader
//webpack.config.js var path = require('path'); module.exports = { mode: 'production', entry: ['./src/index.js'], output: { path: path.resolve(__dirname, './dist'), filename: '[name].js' }, module: { rules: [ { test: /index\.js$/, use: 'bundle-loader' } ] }, resolveLoader: { modules: ['./src/myloader/'], } };
inspect-brk 启动的时候自动在第一行自动加上断点
webpack 经过静态语法分析,找出了不用的 export ,把他们改为 free variable(只是把 exports 关键字删除了,变量的声明并无删除)
Uglify经过静态语法分析,找出了不用的变量声明,直接把他们删了。
当配置了watch时webpack-dev-middleware 将 webpack 本来的 outputFileSystem 替换成了MemoryFileSystem(memory-fs 插件) 实例。
MemoryFileSystem 是个抽象的文件系统库,webpack将该部分解耦,可进一步设置redis或mongodb做为文件系统,在多个webpack实例中共享资源
当执行watch时会实例化一个Watching对象,监控和构建打包都是Watching实例来控制;在Watching构造函数中设置变化延迟通知时间(默认200),而后调用_go方法;webpack首次构建和后续的文件变化从新构建都是_执行_go方法,在__go方法中调用this.compiler.compile启动编译。webpack构建完成后会触发 _done方法,在 _done方法中调用this.watch方法,传入compilation.fileDependencies和compilation.contextDependencies须要监控的文件夹和目录;在watch中调用this.compiler.watchFileSystem.watch方法正式开始建立监听。
在this.compiler.watchFileSystem.watch中每次会从新建立一个Watchpack实例,建立完成后监控aggregated事件和触发this.watcher.watch(files.concat(missing), dirs.concat(missing), startTime)方法,而且关闭旧的Watchpack实例;在watch中会调用WatcherManager为每个文件所在目录建立的文件夹建立一个DirectoryWatcher对象,在DirectoryWatcher对象的watch构造函数中调用chokidar插件进行文件夹监听,而且绑定一堆触发事件并返回watcher;Watchpack会给每个watcher注册一个监听change事件,每当有文件变化时会触发change事件。
在Watchpack插件监听的文件变化后设置一个定时器去延迟触发change事件,解决屡次快速修改时频繁触发问题。
当文件变化时NodeWatchFileStstem中的aggregated监听事件根据watcher获取每个监听文件的最后修改时间,并把该对象存放在this.compiler.fileTimestamps上而后触发 _go方法去构建。
在compile中会把this.fileTimestamps赋值给compilation对象,在make阶段从入口开始,递归构建全部module,和首次构建不一样的是在compilation.addModule方法会首先去缓存中根据资源路径取出module,而后拿module.buildTimestamp(module最后修改时间)和fileTimestamps中的该文件最后修改时间进行比较,若文件修改时间大于buildTimestamp则从新bulid该module,不然递归查找该module的的依赖。
在webpack构建过程当中是文件解析和模块构建比较耗时,因此webpack在build过程当中已经把文件绝对路径和module已经缓存起来,在rebuild时只会操做变化的module,这样能够大大提高webpack的rebuild过程。
https://github.com/lihongxun945/diving-into-webpack/blob/master/7-hmr.md
当完成编译的时候,就经过 websocket 发送给客户端一个消息(一个 hash 和 一个ok)
向client发送一条更新消息 当有文件发生变更的时候,webpack编译文件,并经过 websocket 向client发送一条更新消息
//webpack-dev-server/lib/Server.js compiler.plugin('done', (stats) => { // 当完成编译的时候,就经过 websocket 发送给客户端一个消息(一个 `hash` 和 一个`ok`) this._sendStats(this.sockets, stats.toJson(clientStats)); });
webpack主要是使用Compiler和Compilation类来控制webpack的整个生命周期,定义执行流程;他们都继承了tabpable而且经过tabpable来注册了生命周期中的每个流程须要触发的事件。
webpack内部实现了一堆plugin,这些内部plugin是webpack打包构建过程当中的功能实现,订阅感兴趣的事件,在执行流程中调用不一样的订阅函数就构成了webpack的完整生命周期。
其中:[event-name]
表明 事件名
[---初始化阶段---]
options
Compiler
:存放输入输出配置+编译器Parser对象Watching()
:监听文件变化WebpacOptionsApply()
(根据options)[----构建Graph阶段 1----]
入口文件出发,调用全部配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到全部入口依赖的文件都通过了本步骤的处理
Compiler.readRecords(cb)
Compiler.compile(onCompiled)
(开始构建options中模块)Compiler.newCompilationParams()
NormalModule
工厂函数ContextModule
工厂函数compilation
对象
Compiler.newCompilation(params)
Compiler.createCompilation()
compile
的引用,供plugin使用,并存放全部modules,chunks,assets(对应entry),template。根据test正则找到导入,并分配惟一id[----构建Graph阶段 2----]
SingleEntryDependency
)或者多入口(MultiEntryDependency
)依赖,多个入口时在make事件上注册多个相同的监听,并行执行多个入口DllEntryPlugin
, 就是将入口模块经过调用compilation.addEntry()
方法将全部的入口模块添加到编译构建队列中,开启编译流程。_addModuleChain
开始编译。在_addModuleChain
首先会生成模块,最后构建。在_addModuleChain
中根据依赖查找对应的工厂函数,并调用工厂函数的create来生成一个空的MultModule
对象,而且把MultModule
对象存入compilation的modules中后执行MultModule.build
,由于是入口module,因此在build中没处理任何事直接调用了afterBuild
;在afterBuild
中判断是否有依赖,如果叶子结点直接结束,不然调用processModuleDependencies
方法来查找依赖processModuleDependencies
方法就会被调用;processModuleDependencies
根据当前的module.dependencies
对象查找该module依赖中全部须要加载的资源和对应的工厂类,并把module和须要加载资源的依赖做为参数传给addModuleDependencies
方法;在addModuleDependencies
中异步执行全部的资源依赖,在异步中调用依赖的工厂类的create去查找该资源的绝对路径和该资源所依赖全部loader的绝对路径,而且建立对应的module后返回;而后根据该module的资源路径做为key判断该资源是否被加载过,若加载过直接把该资源引用指向加载过的module返回;不然调用this.buildModule
方法执行module.build
加载资源;build完成就获得了loader处理事后的最终module了,而后递归调用afterBuild
,直到全部的模块都加载完成后make阶段才结束。[---- 优化Graph----]
compilation.seal(cb)
根据以前收集的依赖,决定生成多少文件,每一个文件的内容是什么. 对每一个module和chunk整理,生成编译后的源码,合并,拆分,生成 hash,保存在compilation.assets,compilation.chunk
__webpack__reuqire()
的格式。
[after-optimize-assets]资产已经优化
[after-compile] 一次 Compilation 执行完成。
[---- 渲染Graph----]
Compiler.emitAssets()
按照 output 中的配置项异步将将最终的文件输出到了对应的 path 中
output:plugin结束前,在内存中生成一个compilation对象文件模块tree,枝叶节点就是全部的module(由import或者require为标志,并配备惟一moduleId),主枝干就是全部的assets,也就是咱们最后须要写入到output.path文件夹里的文件内容。
MainTemplate.render()
和ChunkTemplate.render()
处理入口文件的module 和 非首屏需异步加载的moduleMainTemplate.render()
[asset-path]
[after-emit]
[done]
this.emitRecords(cb)
[failed] 若是在编译和输出流程中遇到异常致使 Webpack 退出时,就会直接跳转到本步骤,插件能够在本事件中获取到具体的错误缘由。