一. webpack 介绍
webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
二. webpack 编译流程解析
-
init - 初始化阶段
-
将
process.args + webpack.config.js合并成用户配置 -
调用
validateSchema校验配置 -
调用
getNormalizedWebpackOptions(获取标准化的配置项) + applyWebpackOptionsBaseDefaults(获取默认配置项) 合并出最终配置 -
创建
compiler对象 -
遍历用户定义的
plugins集合,执行插件的apply方法 -
调用定义在
lib/WebpackOptionsApply.js文件中的WebpackOptionsApply.process方法,内部根据entry配置决定注入entry相关的插件 -
实例化
Compiler触发lib/Compiler.js的compile函数,compile函数内部定义了**this.hooks.make.callAsync** 用于监听编译模块时的异步回调 -
在 调用
WebpackOptionsApply.process触发EntryPlugin(监听compiler.make钩子) 的make回调时,在回调中执行compilation.addEntry函数 -
compilation.addEntry函数内部经过一坨与主流程无关的hook之后,再调用handleModuleCreate函数,正式开始构建内容 -
Compiler 和 Compilation 的区别
webpack打包离不开Compiler和Compilation,它们两个分工明确,理解它们是我们理清webpack构建流程重要的一步。Compiler负责监听文件和启动编译它可以读取到webpack的 config 信息,整个Webpack从启动到关闭的生命周期,一般只有一个 Compiler 实例,整个生命周期里暴露了很多方法,常见的run,make,compile,finish,seal,emit等,我们写的插件就是作用在这些暴露方法的 hook 上。Compilation负责构建编译。每一次编译(文件只要发生变化,)就会生成一个Compilation实例,Compilation可以读取到当前的模块资源,编译生成资源,变化的文件,以及依赖跟踪等状态信息。同时也提供很多事件回调给插件进行拓展。
-
-
build - 构建阶段
-
启动编译(
run/watch阶段)如果是监听文件(如:
--watch)的模式,则会传递监听的watchOptions,生成Watching实例,每次变化都重新触发回调。如果不是监视模式就调用
Compiler对象的run方法,befornRun->beforeCompile->compile->thisCompilation->compilation开始构建整个应用。 -
编译模块:(
make阶段)-
从 entry 入口配置文件出发, 调用所有配置的
Loader对模块进行处理, -
再找出该模块依赖的模块, 通过
acorn库生成模块代码的AST语法树,形成依赖关系树(每个模块被处理后的最终内容以及它们之间的依赖关系) -
根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环每个依赖;再递归本步骤直到所有入口依赖的文件都经过了对应的 loader 处理。
-
解析结束后,
webpack会把所有模块封装在一个函数里,并放入一个名为modules的数组里。 -
将
modules传入一个自执行函数中,自执行函数包含一个installedModules对象,已经执行的代码模块会保存在此对象中。 -
最后自执行函数中加载函数(
webpack__require)载入模块。
-
-
build - 构建阶段 Webpack 问题反思:
-
Webpack 编译过程会将源码解析为 AST 吗?webpack 与 babel 分别实现了什么?
- 构建阶段会读取源码,解析为 AST 集合。
- Webpack 读出 AST 之后仅遍历 AST 集合;babel 则对源码做等价转换
-
Webpack 编译过程中,如何识别资源对其他资源的依赖?
- Webpack 遍历 AST 集合过程中,识别
require/ import之类的导入语句,确定模块对其他资源的依赖关系
- Webpack 遍历 AST 集合过程中,识别
-
相对于 grunt、gulp 等流式构建工具,为什么 webpack 会被认为是新一代的构建工具?
- Grant、Gulp 仅执行开发者预定义的任务流;而 webpack 则深入处理资源的内容,功能上更强大
-
-
generate - 生成阶段
-
输出资源:(seal 阶段)
在编译完成后,调用
compilation.seal方法封闭,生成资源,这些资源保存在compilation.assets,compilation.chunk-
遍历
compilation.modules,记录下模块与chunk关系 -
触发各种模块优化钩子,这一步优化的主要是模块依赖关系
-
遍历
module构建 chunk 集合 -
触发各种优化钩子,其中触发的
optimizeChunks钩子,这个时候已经跑完主流程的逻辑,得到chunks集合,SplitChunksPlugin正是使用这个钩子,分析chunks集合的内容,按配置规则增加一些通用的 chunk然后便会调用
emit钩子,根据webpack config文件的output配置的path属性,将文件输出到指定的path.
-
-
输出完成:
done/failed阶段done成功完成一次完成的编译和输出流程。failed编译失败,可以在本事件中获取到具体的错误原因在确定好输出内容后, 根据配置确定输出的路径和文件名, 把文件内容写入到文件系统。
```emitAssets(compilation, callback) { const emitFiles = (err) => { //...省略一系列代码 // afterEmit:文件已经写入磁盘完成 this.hooks.afterEmit.callAsync(compilation, (err) => { if (err) return callback(err) return callback() }) } // emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(这是最后一次修改最终文件的机会) this.hooks.emit.callAsync(compilation, (err) => { if (err) return callback(err) outputPath = compilation.getPath(this.outputPath, {}) mkdirp(this.outputFileSystem, outputPath, emitFiles) }) } ``` -
-
webpack流程总结
compiler.make 阶段:
-
entry文件以dependence对象形式加入compilation的依赖列表,dependence对象记录有entry的类型、路径等信息 。 -
根据
dependence调用对应的工厂函数创建module对象,之后读入module对应的文件内容,调用loader-runner对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为module。 -
compilation.seal阶段: 遍历module集合,根据entry配置及引入资源的方式,将module分配到不同的chunk遍历chunk集合,调用compilation.emitAsset方法标记chunk的输出规则,即转化为assets集合 。
compiler.emitAssets 阶段: 将 assets 写入文件系统
-
webpack输出文件代码分析(function (modules) { // webpackBootstrap // 缓存 __webpack_require__ 函数加载过的模块,提升性能, var installedModules = {} /** * Webpack 加载函数,用来加载 webpack 定义的模块 * @param {String} moduleId 模块 ID,一般为模块的源码路径,如 "./src/index.js" * @returns {Object} exports 导出对象 */ function __webpack_require__(moduleId) { // 重复加载则利用缓存,有则直接从缓存中取得 if (installedModules[moduleId]) { return installedModules[moduleId].exports } // 如果是第一次加载,则初始化模块对象,并缓存 var module = (installedModules[moduleId] = { i: moduleId, l: false, exports: {}, }) // 把要加载的模块内容,挂载到module.exports上 modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ) module.l = true // 标记为已加载 // 返回加载的模块,直接调用即可 return module.exports } // 在 __webpack_require__ 函数对象上挂载一些变量及函数 ... __webpack_require__.m = modules __webpack_require__.c = installedModules __webpack_require__.d = function (exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter, }) } } __webpack_require__.n = function (module) { var getter = module && module.__esModule ? function getDefault() { return module['default'] } : function getModuleExports() { return module } __webpack_require__.d(getter, 'a', getter) return getter } // __webpack_require__对象下的r函数 // 在module.exports上定义__esModule为true,表明是一个模块对象 __webpack_require__.r = function (exports) { if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module', }) } Object.defineProperty(exports, '__esModule', { value: true, }) } __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property) } // 从入口文件开始执行 return __webpack_require__((__webpack_require__.s = './src/index.js')) })({ './src/index.js': function ( module, __webpack_exports__, __webpack_require__ ) { 'use strict' eval( '__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ "./src/moduleA.js");\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_moduleA__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./moduleB */ "./src/moduleB.js");\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_moduleB__WEBPACK_IMPORTED_MODULE_1__);\n\n\n\n//# sourceURL=webpack:///./src/index.js?' ) }, './src/moduleA.js': function (module, exports) { eval('console.log("moduleA")\n\n//# sourceURL=webpack:///./src/moduleA.js?') }, './src/moduleB.js': function (module, exports) { // 代码字符串可以通过eval 函数运行 eval('console.log("moduleB")\n\n//# sourceURL=webpack:///./src/moduleB.js?') }, })上述代码的实现了⼀个
webpack_require来实现⾃⼰的模块化把代码都缓存在installedModules⾥,代码⽂件以对象传递进来,key是路径,value是包裹的代码字符串,并且代码内部的require,都被替换成了webpack_require,代码字符串可以通过 eval 函数去执行。bundle.js能直接运行在浏览器中的原因在于输出的文件中通过 webpack_require 函数定义了一个可以在浏览器中执行的加载函数来模拟Node.js中的require语句。总结一下,生成的
bundle.js只包含一个立即调用函数(IIFE),这个函数会接受一个对象为参数,它其实主要做了两件事:- 定义一个模块加载函数
webpack_require。 - 使用加载函数加载入口模块
"./src/index.js",从入口文件开始递归解析依赖,在解析的过程中,分别对不同的模块进行处理,返回模块的exports。
- 定义一个模块加载函数