官方文档上是这样说的:At its core, webpack is a static module bundler for modern JavaScript applications.
实际上,webpack就是一个模块打包器(module bundler),将多个模块打包成一个(或者多个)js文件。 模块可以是CommonJs、AMD、ES6模块,也可以是CSS、Images、JSON、LESS...又或者是自定义的模块。
现在的网站都向webapp进化,浏览器能做越来越多的事情,页面中有越来越多的js; 大量的代码需要被组织,即模块化; 网站的脚本数量太多会影响用户体验,通过代码打包可以提高性能;
entry和output很好理解,不作解析
loader就是模块转换器,使webpack能够去处理那些非JavaScript文件,即将文件转换为webpack能够处理的模块。
插件接口功能极其强大,可以用来处理各种各样的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。 除了可以通过plugins配置的插件,webpack内部也有很多插件(根据wepack配置调用各种插件参考 lib/WebpackOptionsApply.js)。
webpack是一个基于事件流的插件合集。 webpack的工作流程非常复杂,细分程度非常高,内部的钩子多达一百多个。 webpack内部各种各样的plugin至少有好几十个,而将这些插件控制在webapck事件流上的运行的就是webpack的Tapable,核心类Compiler和Compilation都继承Tapable。 Tapable作为webpack的核心库,用来处理各种同步异步串行并行的流程。参考第4点。
几个关键对象的相关钩子: compiler、 compilation、 parser
webpack内部的钩子非常多,内部插件与钩子间的关系也非常复杂, 可视化展示webpack内部插件与钩子关系
tapable与node的event类似,先注册(tap),然后在某个时刻触发/调用(call) tapable暴露了很多Hook(钩子)类,为插件提供挂载的钩子。
在 tapable 源码中,触发事件的方法 call、callAsync、promise 都是通过 compile 方法生成的。 而在早期的版本,实现上简单很多,目前我还没看明白为什么做这些改动。
简单地理解,webpack就是一个Compiler!通过配置得到一个Compiler实例,调用run方法开始编译。包含了很多重要的钩子:entry-option, run, compile, make, after-compile, emit, after-emit。主要方法:run, watch, compile, emitAsset等等。
该对象负责组织整个编译过程,包含了每个构建环节所对应的方法,如:addEntry, buildModule, processModuleDependencies, summarizeDependencies, createModuleAssets, createHash等等
代码块,一个 Chunk 由多个模块组合而成。coding split的产物,我们可以对一些代码打包成一个单独的chunk,比如某些公共模块,去重,更好的利用缓存。或者按需加载某些功能模块,优化加载时间。在webpack3及以前我们都利用CommonsChunkPlugin将一些公共代码分割成一个chunk,实现单独加载。在webpack4 中CommonsChunkPlugin被废弃,使用SplitChunksPlugin
在 Webpack 里一切皆模块,一个模块对应着一个文件。子类:RawModule, NormalModule, MultiModule, ContextModule, DelegatedModule, DllModule, ExternalModule 等
每一个依赖(Dependency)的实体都包含一个module字段,指向被依赖的Module. 这样通过Module的dependencies数组成员就能找出该模块所依赖的其它模块。 Dependency各种子类在lib/dependencies里
Template是用来生成结果代码的。webpack中Template有四种: MainTemplate.js 用于生成项目入口文件 ChunkTemplate.js 用于生成异步加载的js代码 ModuleTemplate.js 用于生成某个模块的代码 HotUpdateChunkTemplate.js
Webpack 的构建流程可以分为以下三大阶段:
1.初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
2.编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
3.输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。
以下表格为各个阶段关键的事件及描述:
阶段 | 事件 | 描述 |
---|---|---|
初始化阶段 | 初始化参数 | 从配置文件和 Shell 语句中读取与合并参数 |
加载插件 | 依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。 | |
entry-option | 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin | |
after-resolvers | 根据配置初始化完 resolver,resolver 负责在文件系统中寻找指定路径的文件。 | |
编译阶段 | run | 启动一次新的编译 |
compile | 新的编译将要启动,同时会给插件带上 compiler 对象。 | |
compilation | 新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等 | |
make | 即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析 | |
after-compile | 一次 Compilation 执行完成 | |
输出阶段 | emit | 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容 |
after-emit | 文件输出完毕。 | |
done | 成功完成一次完成的编译和输出流程 | |
failed | 如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤 |
在编译阶段中,最重要的要数 compilation 事件了,因为在 compilation 阶段调用了 Loader 完成了每个模块的转换操作,在 compilation 阶段又包括很多小的事件
事件 | 事件 | 描述 |
---|---|---|
complation | build-module | 使用对应的 Loader 去转换一个模块。 |
normal-module-loader | 在用 Loader 对一个模块转换完后,使用parser解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。 | |
program | 从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析。 | |
seal | 所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk。 |
文件搜索范围
精简第三方包
压缩代码
公共代码
动态链接库
按需加载
删除无用代码
多进程
输出分析
刚开始阅读源代码,代码跟着跟着就乱了,这也很正常。 本来webpack的流程就很复杂,因此,作者特意写了tapable这个库,来处理各种流程,整个webpack大大小小的钩子一百多个,想要完全弄清楚几乎不可能。 其实也没有必要,只要理解一些关键的钩子的顺序和关系,那么整个流程就比较容易理解了。
另外,回调函数本身就不太利于代码跟踪,项目里回调函数也非常多,跟踪起来更加困难,不过这个很难避免。 关于回调这个问题,自己在开发时可以用ES7的新特性async/await来代替。
为什么call方法要动态生成,我也是没想明白。