初看源码
当在运行 webpack 打包命令的时候,会去执行 webpack-cli 中的 runWebpack()
然后 => 执行 createCompiler()
, 内部会获取 options 和 config => 然后去调用 webpack。
webpack 调用自身 createCompiler()
, createCompiler()
处理 options 后 new Compiler
生成 compiler 对象。然后注入 plugins 插件,后挂载一些 hooks(hooks 概念下面再讲,这里先提一下)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 if (Array .isArray (options.plugins )) { for (const plugin of options.plugins ) { if (typeof plugin === "function" ) { plugin.call (compiler, compiler); } else if (plugin) { plugin.apply (compiler); } } } compiler.hooks .environment .call (); compiler.hooks .afterEnvironment .call ();
生成的 compiler 执行自身 run()
方法 这里又会挂载一些 hooks,然后执行自身 compile()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 this .hooks .beforeRun .callAsync (this , err => { if (err) return finalCallback (err); this .hooks .run .callAsync (this , err => { if (err) return finalCallback (err); this .readRecords (err => { if (err) return finalCallback (err); this .compile (onCompiled); }); }); });
compile()
中会生成 compilation 对象,其次同样是挂载 hooks,这里其实对应了单次执行编译的生命周期:beforeCompile
-> make
-> finishMake
-> finish
-> seal
-> afterCompile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 compile (callback ) { const params = this .newCompilationParams (); this .hooks .beforeCompile .callAsync (params, err => { if (err) return callback (err); this .hooks .compile .call (params); const compilation = this .newCompilation (params); const logger = compilation.getLogger ("webpack.Compiler" ); logger.time ("make hook" ); this .hooks .make .callAsync (compilation, err => { logger.timeEnd ("make hook" ); if (err) return callback (err); logger.time ("finish make hook" ); this .hooks .finishMake .callAsync (compilation, err => { logger.timeEnd ("finish make hook" ); if (err) return callback (err); process.nextTick (() => { logger.time ("finish compilation" ); compilation.finish (err => { logger.timeEnd ("finish compilation" ); if (err) return callback (err); logger.time ("seal compilation" ); compilation.seal (err => { logger.timeEnd ("seal compilation" ); if (err) return callback (err); logger.time ("afterCompile hook" ); this .hooks .afterCompile .callAsync (compilation, err => { logger.timeEnd ("afterCompile hook" ); if (err) return callback (err); return callback (null , compilation); }); }); }); }); }); }); }); }
到这里 webpack 整体上的执行流程和生命周期就结束了,看上去似乎很简单,并没有做什么编译上的事情,只是不停的在挂载 hooks 。
这也就是 webpack 的设计,将所有单一的职责明确的事情都交给了 plugins,webpack 通过加载 plugins 挂载 hooks ,在特定的执行阶段去执行对应的 hooks 来完成所有的任务。
plugins & Tapable 一个内置 plugin 的示例:EntryPlugin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 class EntryPlugin { constructor (context, entry, options ) { this .context = context; this .entry = entry; this .options = options || "" ; } apply (compiler ) { compiler.hooks .compilation .tap ( "EntryPlugin" , (compilation, { normalModuleFactory } ) => { compilation.dependencyFactories .set ( EntryDependency , normalModuleFactory ); } ); const { entry, options, context } = this ; const dep = EntryPlugin .createDependency (entry, options); compiler.hooks .make .tapAsync ("EntryPlugin" , (compilation, callback ) => { compilation.addEntry (context, dep, options, err => { callback (err); }); }); } static createDependency (entry, options ) { const dep = new EntryDependency (entry); dep.loc = { name : typeof options === "object" ? options.name : options }; return dep; } }
从形态上看,插件通常是一个带有 apply 函数的类。
从逻辑上看,插件获取到全局的编译器 compiler 对象,compiler 提供的生命周期 hooks 上添加执行逻辑来对 webpack 处理流程进行干预。
plugin 也能获取到单次编译的 compilation 对象,通过调用对象上的方法,对单次编译进行干预。
从 plugin 的源码里可以看出 webpack plugin 是基于 hooks 去实现的。
查看 Compiler
类的实现,可以看到 hooks 的定义:
1 2 3 4 5 6 7 8 9 10 this .hooks = Object .freeze ({ initialize : new SyncHook ([]), shouldEmit : new SyncBailHook (["compilation" ]), done : new AsyncSeriesHook (["stats" ]), })
而其中的 SyncHook 等,都是来自 Tapable
这个 npm 包。
Tapable ,这个包的主要逻辑就是实现一个订阅/发布模式:用 tap 方法注册回调,用 call 调用回调。
Tapable 定义许多 Hook ,从分类上来主要分为异步、同步;异步中又分为并行、串行;
具体的 Hook 不进行展开,一个是记不住,二是不影响对 webpack 整体的理解。可以参考 文章 。
构建流程 至此,webpack 的大致轮廓已经有了:提供一套具有生命周期的流水线,用 Tapable 实现钩子,plugins 注册钩子函数, webpack 在执行到对应的生命周期时调用 plugin 注册的逻辑。
hooks 对应了整个 webpack 执行的生命周期,可以通过 hooks 观察 webpack 的生命周期,这部分在 webpack 官网上有列举:compiler-hooks 、 compilation-hooks 。
总体的构建流程:
初始化参数 :从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数
创建编译器对象 :用上一步得到的参数创建 Compiler
对象
初始化编译环境 :包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
开始编译 :执行 compiler
对象的 run
方法
确定入口 :根据配置中的 entry
找出所有的入口文件,调用 compilition.addEntry
将入口文件转换为 dependency
对象
**编译模块 (make)**:根据 entry
对应的 dependency
创建 module
对象,调用 loader
将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
完成模块编译 :上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图
**输出资源 (seal)**:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
,再把每个 Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
**写入文件系统 (emitAssets)**:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
可以看到 14 与开头第一章节是基本对应的,后面的 59 则是散落到各个 plugins 中。
entry & dependency webpack 官网简单介绍了 Dependency Graph :
任何时候一个文件依赖于另一个文件,webpack 都会将其视为 dependency 。这允许 webpack 获取非代码资源,例如图像或 Web 字体,并将它们作为应用程序的依赖项提供。
当 webpack 处理你的应用程序时,它从命令行或其配置文件中定义的模块列表开始。从这些 entry points 开始,webpack 递归地构建一个 Dependency Graph ,其中包含应用程序所需的每个模块,然后将所有这些模块捆绑到少量的 bundles 中(通常只有一个)以供浏览器加载。
从源码上看,compilation 中的 addEntry
-> _addEntryItem
-> addModuleTree
-> handleModuleCreation
是在负责这一职责。
handleModuleCreation
函数中, 会对 moduleGraph
对象进行操作,最终存储一个依赖关系的无环有向图。
make 编译 依赖图构建完成后,开始进行编译,这里每个需要参与构建的文件都被视作一个 module,webpack 中有个 Module 的抽象类对其进行了定义。Module 下的 build 方法则是进行编译的入口方法。
编译中,webpack 运行 loaders 将各种资源文件 module 转译成 js。
loader
loader 是一个带有副作用的内容转译器。应为 webpack 只接受符合 js 规范的文本,因此其他类型的 module 时,需要将此种 module 转换成 js module。
loader 通常是一个函数:
source
:资源输入,对于第一个执行的 loader 为资源文件的内容;后续执行的 loader 则为前一个 loader 的执行结果
sourceMap
: 可选参数,代码的 sourcemap 结构
data
: 可选参数,其它需要在 Loader 链中传递的信息
1 2 3 4 5 6 function loader (source, sourceMap?, data? ) { return source; }; module .exports = loader;
类型 loader
主要有以下几种类型:
同步 loader
: return
或调用 this.callback
都是同步返回值
异步 loader
:是用 this.async()
获取异步函数,是用 this.callback()
返回值
raw loader
:默认情况下接受 utf-8
类型的字符串作为入参,若标记 raw
属性为 true
,则入参的类型为二进制数据
pitch loader
: loader
总是从右到左被调用。有些情况下,loader
只关心 request 后面的 元数据 (metadata
),并且忽略前一个 loader
的结果。在实际(从右到左)执行 loader
之前,会先从左到右调用 loader
上的 pitch
方法。
pitch loader 上面的示例代码是个Normal Loader ,而 Pitching Loader 则是在 loader 函数上设置的 pitch 函数
1 2 3 loader.pitch = function (remainingRequest, precedingRequest, data ) { };
Pitching Loader 的执行顺序是 从左到右 ,而 Normal Loader 的执行顺序是 从右到左 。当 Loader.pitch
方法返回非 undefined
值时,跳过了剩下的 loader。
loader 实现示例
1 2 3 4 5 6 7 module .exports = function (source ) { const reg = /console\.log\(["'].*?["']\)/ if (reg.test (source)) { return source.replace (reg, '' ) } return source }
模块加载原理 比如这样的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 const hello = require ('./modules/module1' )function main ( ) { console .log ('main' ) } hello ()main ()module .exports = function hello ( ) { console .log ('hello' ) }
构建出如下的产物:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 (() => { var __webpack_modules__ = ({ "./src/modules/module1.js" : ((module ) => { module .exports = function hello ( ) { console .log ('hello' ) } }) }); var __webpack_module_cache__ = {}; function __webpack_require__ (moduleId ) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined ) { return cachedModule.exports ; } var module = __webpack_module_cache__[moduleId] = { exports : {} }; __webpack_modules__[moduleId](module , module .exports , __webpack_require__); return module .exports ; } var __webpack_exports__ = {};(() => { const hello = __webpack_require__ ( "./src/modules/module1.js" )function main ( ) { console .log ('main' ) } hello ()main ()})(); })() ;
首先这整个是个 IIFE,防止全局作用域污染
定义 __webpack_modules__
,形成模块 map,模块作为值保存
定义 __webpack_module_cache__
,缓存已加载的模块
定义 __webpack_require__
,用来加载模块并缓存,从 __webpack_modules__
加载模块并保存到 __webpack_module_cache__
执行 entry 模块,其中的 引入模块 (import/require) 会被 __webpack_require__
替代
懒加载
将异步模块分割成单独 chunk
异步引入模块的时机会去通过 jsonp 的方式加载模块,返回 promise
webpack 新建一个全局队列 self["webpackChunk"]
,其 push 方法为 webpackJsonpCallback
, 该方法会去合并模块到 __webpack_modules__
,并去执行上一步中 promise 的 resolve
异步模块中会添加执行 self["webpackChunk"].push
的方法,执行该方法,即去将异步模块中的导出注册到 __webpack_modules__
上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 (() => { var installedChunks = { "main" : 0 }; __webpack_require__.f .j = (chunkId, promises ) => { var installedChunkData = __webpack_require__.o (installedChunks, chunkId) ? installedChunks[chunkId] : undefined ; if (installedChunkData !== 0 ) { if (installedChunkData) { promises.push (installedChunkData[2 ]); } else { if (true ) { var promise = new Promise ((resolve, reject ) => (installedChunkData = installedChunks[chunkId] = [resolve, reject])); promises.push (installedChunkData[2 ] = promise); var url = __webpack_require__.p + __webpack_require__.u (chunkId); var error = new Error (); var loadingEnded = (event ) => { if (__webpack_require__.o (installedChunks, chunkId)) { installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0 ) installedChunks[chunkId] = undefined ; if (installedChunkData) { var errorType = event && (event.type === 'load' ? 'missing' : event.type ); var realSrc = event && event.target && event.target .src ; error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')' ; error.name = 'ChunkLoadError' ; error.type = errorType; error.request = realSrc; installedChunkData[1 ](error); } } }; __webpack_require__.l (url, loadingEnded, "chunk-" + chunkId, chunkId); } } } }; var webpackJsonpCallback = (parentChunkLoadingFunction, data ) => { var [chunkIds, moreModules, runtime] = data; var moduleId, chunkId, i = 0 ; if (chunkIds.some ((id ) => (installedChunks[id] !== 0 ))) { for (moduleId in moreModules) { if (__webpack_require__.o (moreModules, moduleId)) { __webpack_require__.m [moduleId] = moreModules[moduleId]; } } if (runtime) var result = runtime (__webpack_require__); } if (parentChunkLoadingFunction) parentChunkLoadingFunction (data); for (;i < chunkIds.length ; i++) { chunkId = chunkIds[i]; if (__webpack_require__.o (installedChunks, chunkId) && installedChunks[chunkId]) { installedChunks[chunkId][0 ](); } installedChunks[chunkId] = 0 ; } } var chunkLoadingGlobal = self["webpackChunk" ] = self["webpackChunk" ] || []; chunkLoadingGlobal.forEach (webpackJsonpCallback.bind (null , 0 )); chunkLoadingGlobal.push = webpackJsonpCallback.bind (null , chunkLoadingGlobal.push .bind (chunkLoadingGlobal)); })();
HMR
使用 webpack-dev-server
托管静态资源,同时以 Runtime 方式注入 HMR 客户端代码
浏览器加载页面后,与 WDS 建立 WebSocket 连接
Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送 hash
事件
浏览器接收到 hash
事件后,请求 manifest
资源文件,确认增量变更范围
浏览器加载发生变更的增量模块
Webpack 运行时触发变更模块的 module.hot.accept
回调,执行代码变更逻辑