随着前端工程化,各种自动化构建工具也越来越多,像早期的 gulp,再到后来的 webpack,以及 Vue 新的 vite 等等,虽然 webpack 用了很长时间,但是还没研究过内部原理,今天就来尝试研究一下原理并且手动实现一个简易的 webpack 并且对代码进行打包。
首先来看一个最基本的代码,我们在项目中,一般最常使用的就是 es6 的 import 语法,比如现在有一个 add.js,还有一个 types.js,以及入口文件 main.js 我们一步一步来拆解分析
add.js
1 2 3 4 5 // add.js export default function add(a, b) { return a + b }
types.js
1 2 3 4 // types.js export const PARAMS_ONE = 1 export const PARAMS_TWO = 2
c.js
1 2 let c = 20 export default c
main.js
1 2 3 4 import { PARAMS_ONE, PARAMS_TWO } from './types' import add from './add' console.log(add(PARAMS_ONE, PARAMS_TWO))
webpack 配置如下,这里需要注意,需要 mode 为 development,才能看到打包之后的 bundle 源码,这里的 webpack 版本为 5.24.4
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 var webpack = require('webpack') var path = require('path') module.exports = { // dirname是当前webpack.config.js的文件路径 entry: path.join(__dirname, 'src/main.js'), mode: 'development', output: { filename: 'bundle.js', path: path.join(__dirname, 'dist'), }, }
在 package.json 的 scripts 添加命令
package.json
1 "build": "webpack --config webpack.config.js"
当我们执行npm run build命令时候,在 dist 目录下会生成一个 bundle.js 的文件,里面的内容大概如下
what?这里面得一大坨都是什么?不用着急,当我们要去实现一个 webpack 时候,我们自然要先去分析 webpack 打包出来 bundle.js 的代码是什么,是如何运行的,理解原理,我们才能有思路去实现一个 webpack
bundle.js 当我们把打包出来的 bundle.js 代码折叠之后,就会发现,发现其实就是一个匿名自执行函数
1 2 3 4 /******/ ;(() => { // code.... })()
可以看到,在匿名自执行函数内部,首先申明了一个__webpack_modules__的对象字面量,里面的key是以我们文件的路径作为字段,值是一个箭头函数,里面有三个参数,函数内部是一个 eval 用来处理 webpack 编译之后的代码
1 2 3 4 5 6 7 var __webpack_modules__ = ({ "./src/add.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ add)\n/* harmony export */ });\n/* harmony import */ var _c__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./c */ \"./src/c.js\");\n\nfunction add(a, b) {\n return a + b + _c__WEBPACK_IMPORTED_MODULE_0__.default\n}\n\n\n//# sourceURL=webpack://webpack-demo/./src/add.js?");}), })
下面还声明了一个函数__webpack_require__,以及三个自执行函数,这三个自执行的匿名函数,执行完之后会在__webpack_require__函数身上添加三个静态方法,分别是d,o以及r
1 2 3 4 5 6 7 8 9 10 11 var __webpack_module_cache__ = {}; function __webpack_require__(moduleId) { if(__webpack_module_cache__[moduleId]) { return __webpack_module_cache__[moduleId].exports; } var module = __webpack_module_cache__[moduleId] = { exports: {} }; __webpack_modules__[moduleId](module, module.exports, __webpack_require__); return module.exports; }
给__webpack_require__挂载静态方法的自执行函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 (() => { __webpack_require__.d = (exports, definition) => { for(var key in definition) { ... } }; })(); (() => { __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) })(); (() => { // define __esModule on exports __webpack_require__.r = (exports) => { ... }; })();
在 bundle.js 最下方,就是我们的启动函数__webpack_require__,这里调用了__webpack_require__,并且传入了./src/main.js作为函数的参数,也就是在 webpack.config.js 中指定的 entry
1 2 3 4 // startup // Load entry module and return exports // This entry module can't be inlined because the eval devtool is used. var __webpack_exports__ = __webpack_require__("./src/main.js");
现在让我们来尝试分析一下调用这里的函数之后发生的事情。
当我们调用__webpack_require__函数之后,__webpack_require__函数接受一个参数moduleId,第一次调用时候为"./src/main.js",首先对模块做了缓存判断,如果缓存存在,则立即返回当前的exports,否则,把当前模块添加到__webpack_module_cache__中,并且添加一个属性exports,值为空对象,在exports中,会把我们在各个 js 文件中导出的内容挂载在exports上。再往下则通过__webpack_modules__,也就是上面以文件路径作为 key 值的对象字面量,找到对应的moduleId的函数执行,将当前module,module.exports就是导出的内容,以及__webpack_require__函数自身作为参数传入,在这里第一次对应执行的函数是main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var __webpack_module_cache__ = {}; function (moduleId) { <!--缓存判断--> if(__webpack_module_cache__[moduleId]) { return __webpack_module_cache__[moduleId].exports; } <!--创建cache缓存--> var module = __webpack_module_cache__[moduleId] = { exports: {} }; <!--找到对应的模块函数执行--> __webpack_modules__[moduleId](module, module.exports, __webpack_require__); // Return the exports of the module return module.exports; }
来看一下__webpack_modules__对应的main.js函数,可以看到,这里是通过eval函数包裹了一个字符串代码来执行的,为什么会通过eval函数包裹来执行呢?这就涉及到了webpack中的devTool,在默认情况下,如果不配置devTool选项,就是默认的eval来执行代码,具体信息可以参见理解 webpack 中的 devTool 的配置项
1 2 3 4 5 "./src/main.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _types__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./types */ \"./src/types.js\");\n/* harmony import */ var _add__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./add */ \"./src/add.js\");\n\n\nconsole.log((0,_add__WEBPACK_IMPORTED_MODULE_1__.default)(_types__WEBPACK_IMPORTED_MODULE_0__.PARAMS_ONE, _types__WEBPACK_IMPORTED_MODULE_0__.PARAMS_TWO))\n\n\n//# sourceURL=webpack://webpack-demo/./src/main.js?"); }),
我们把重要的代码提炼出来,可以看到,开始执行的代码是__webpack_require__.r函数,传入了__webpack_exports__,对应就是上面的module.exports
1 2 3 4 5 6 7 __webpack_require__.r(__webpack_exports__) var _types__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/types.js") var _add__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__( "./src/add.js") console.log((0,_add__WEBPACK_IMPORTED_MODULE_1__.default)(_types__WEBPACK_IMPORTED_MODULE_0__.PARAMS_ONE, _types__WEBPACK_IMPORTED_MODULE_0__.PARAMS_TWO))
__webpack_require__.r函数作用是什么呢?可以看到,最主要的作用其实就是在当前的 exports 中添加了一个__esModule值为true的属性,而这个属性就表示,标识module为es模块
1 2 3 4 5 6 7 8 // define __esModule on exports __webpack_require__.r = (exports) => { if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }); };
之后继续执行下面的代码,继续通过__webpack_require__函数引入了types.js和add.js,此时再次执行上面创建module,添加缓存的逻辑,执行types.js对应的eval函数,标记为es模块的逻辑
1 2 3 4 var _types__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/types.js") var _add__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__( "./src/add.js")
在执行完标记es模块之后,这里原来的types.js导出了两个常量,此时被打包为如下
1 2 3 4 5 6 /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "PARAMS_ONE": () => (/* binding */ PARAMS_ONE), /* harmony export */ "PARAMS_TWO": () => (/* binding */ PARAMS_TWO) /* harmony export */ }); const PARAMS_ONE = 1 const PARAMS_TWO = 2
通过__webpack_require__.d函数,将上方标记的 exports 模块,以及一个对象作为参数传入,此时的第一个参数就是我们的标记过的es模块
1 2 3 4 5 //__webpack_exports__ { __esModule: true }
来看一下__webpack_require__.d函数
1 2 3 4 5 6 7 __webpack_require__.d = (exports, definition) => { for(var key in definition) { if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); } }
首先对我们传入的对象遍历,通过__webpack_require__.o函数判断,属性只能来源与其自身而不是来自于exports,就会通过Object.defineProperty将当前exports中的key设置为我们导出的常量名,值为上面传入的函数
1 2 3 4 5 6 7 8 9 10 __webpack_require__.d = (exports, definition) => { for(var key in definition) { if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); } } }; // __webpack_require__.o __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
遍历完成types.js的exports,最后入口函数返回这个epxorts给_types__WEBPACK_IMPORTED_MODULE_0__变量
1 2 3 4 5 6 7 var _types__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./types */ "./src/types.js"); // 此时的_types__WEBPACK_IMPORTED_MODULE_0__就是 { __esModule: true, PARAMS_ONE:1, PARAMS_TWO:2, }
同样,在add.js执行完上述逻辑之后,因为是使用的export default,所以执行完成是
1 2 3 4 5 6 7 8 9 var _add__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./add */ "./src/add.js"); // 此时的_add__WEBPACK_IMPORTED_MODULE_1__就是 { __esModule: true, default:function(a,b){ <!--这里的_c__WEBPACK_IMPORTED_MODULE_0__.default就是引入的c.js--> return a+b+_c__WEBPACK_IMPORTED_MODULE_0__.default } }
执行完上面的逻辑之后,最后一步就是执行main.js的最后一个函数
1 console.log((0,_add__WEBPACK_IMPORTED_MODULE_1__.default)(_types__WEBPACK_IMPORTED_MODULE_0__.PARAMS_ONE, _types__WEBPACK_IMPORTED_MODULE_0__.PARAMS_TWO))
这里是通过(0,function(a,b){a+b+c})(1,2)这种方法执行的,这里通过逗号操作符来执行函数,最后就会执行出打印结果。
这里最后来梳理一下:
通过__webpack_require__ 加载入口文件,有缓存使用缓存,没有创建缓存模块,并且执行对应的eval函数
发现里面有依赖其他模块,继续通过 __webpack_require__引用,回到第一步,直到没有下一级
将处理好的exports返回,里面是被标记过的es模块和我们在文件中导出的属性
执行模块函数,运行结束
深度优先遍历(Depth-First Search) 可以看到,模块文件形成了一颗树状依赖,并且按照优先纵深查找,这个就叫做(DFS),其实我们可以把文件结构更改一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [{ name: 'main.js', code: 'eval(xx)', children: [{ name: 'add.js', code: 'eval(xx)', children: [{ name: 'c.js', code: 'eval(xx)' }] },{ name:'types.js', code:'eval(xx)' }] }]
然后通过递归实现深度遍历
1 2 3 4 5 6 7 8 9 10 let nodeLists = [] function depth1(node) { if (!node) return nodeLists.push(node) node.children && node.children.forEach((v) => { depth1(v) }) return nodeLists }