作者/Youhe(前端时空)
文章已同步发表于
微信公众号「前端时空」
一道典型的场景面试题。一共有140g盐,如何用一个天平和两个2g,7g的砝码分三次成90g、50g。这道题用常规思路想可能会很麻烦,但是如果用逆向思维就容易的多了。首先如果要凑成50g,最后一步一定是拿两份25g的盐,25g又可以用砝码和盐来凑,用2g和7g凑成9g盐,再称出7g盐,把所有砝码和这两堆盐凑在一起,9 + 9 + 7 = 25g。 这样三次就可以称出来50g的盐。
我们在学习前端、学习webpack的时候,也不妨利用逆向思维分析问题。按常规来看,学习webpack最好的方式是知晓其背后的原理。事实上,webpack是一个将一切资源都当成模块的模块化打包工具。其打包步骤为:
在生成boundle.js文件后,html页面就可以利用script标签的src去引入该文件。
我们用一个未使用plugin、loader的简单Demo去就去扒一扒生成boundle.js文件源码,看看有哪些值得我们学习的地方,同时从这个角度去思考webpack。
首先在主体上看,boundle.js是一个自执行匿名函数,通过传入一个对象参数(版本v4.0.0+,旧版本是一个数组)。下面是将无关代码去掉的精简部分。入口文件是一个index.js文件,在index.js中使用import引入了test.js。
(function (modules) { // 已安装模块 var installedModules = {} // __webpack_require__函数 function __webpack_require__(moduleId) { //代码 } /* 主体内容 ... __webpack_require__.m = modules; __webpack_require__.c = installedModules; ... */ return __webpack_require__(__webpack_require__.s = "./js/index.js"); }) ({ "./js/index.js": (function (module, __webpack_exports__, __webpack_require__) {}), "./js/test.js": (function (module, __webpack_exports__, __webpack_require__) {}), }); 复制代码
这就是boundle的主体,十分简洁明了。是一个自执行的匿名函数,接收一个对象作为参数,这个对象键值分别为「模块路径」与一个「匿名函数」。函数体内,有一个「installedModules」对象,从名称上可以推断出是用来存放已安装模块的。之后是十分重要的__webpack_require__函数,这个函数用来安装模块和获取已安装模块。我们详细看下这个函数的内容。
function __webpack_require__(moduleId) { //已安装模块,返回模块得exports if (installedModules[moduleId]) { return installedModules[moduleId].exports; } //未安装,安装模块 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // 调用参数modules中的键值函数,将this指向module.exports modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 表示安装完成 module.l = true; // 返回模块得exports return module.exports; } 复制代码
函数接收一个「moduleId」作为参数,首先是一个if语句判断是否installedModules安装了相应模块,如果安装了则直接返回该模块的「exports属性」。如果不存在,将installedModules[moduleId] 赋值一个对象,其中键「i」为模块的ID即moduleId,「l」为一个布尔型标识符,代表是否安装完毕,初值为false,「exports为一个空对象」。接下来去调用modules(传进来的对象参数),根据moduleId执行相应的函数。将this指向了module.exports,也就是刚才的那个空对象,并传入三个参数 module、module.exports、 「webpack_require」。
完成后,将module的i置为true,表示安装完成。最后返回module的「exports」。
在__webpack_require__函数之后的代码,姑且叫它主体内容。下面是精简后的部分。请硬着头皮看完这里,脑海里留下印象即可。
// modules __webpack_require__.m = modules; // installedModules __webpack_require__.c = installedModules; // 判断__webpack_require__.o是否为flase __webpack_require__.d = function (exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, {enumerable: true, get: getter}); } }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // 将exports的toStringTag值变成‘[Module Object]’ __webpack_require__.r = function (exports) { if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'}); } Object.defineProperty(exports, '__esModule', {value: true}); }; 复制代码
在JavaScript中,函数的本质也是对象。这里将一些属性存放在__webpack_require__上。
如m、c、d、o、r。(这里只讲叙这几种),这种写法的好处是可以将单个元素既作为可以执行的函数,又能作为一个具有存储功能的hash结构。
至此之后自执行函数会执行__webpack_require__函数,并传入入口文件ID。
return __webpack_require__(__webpack_require__.s = "./js/index.js"); //调用\_\_webpack_require__函数,将__webpack_require__.s赋值为"./js/index.js"后作为参数传入执行。 复制代码
执行__webpack_require__函数后,我们重新进入到函数内部。到这条语句。
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 复制代码
这里将根据moduleId找到对应的函数。贴参数部分代码。
(function (modules) {}) ({ "./js/index.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ "./js/test.js"); const textNode = document.createTextNode('my name is wyh') document.querySelector('#test').appendChild(textNode) Object(_test__WEBPACK_IMPORTED_MODULE_0__["printA"])() }), "./js/test.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "printA", function () { return printA; }); __webpack_require__.d(__webpack_exports__, "a", function () { return a; }); function printA() { console.log('A'); } let a = {} a.name = 'A' }) }); 复制代码
我们对比下两个函数的相同点,其中:
module
、`__webpack_exports**、__webpack_require**,modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
三个参数。
对比完毕后,然后开始执行,首先是入口"./js/index.js"。这里声明了一个_test__WEBPACK_IMPORTED_MODULE_0__变量,事实上,如果含有多个依赖,那么变量名就会从0开始递增。
如_test__WEBPACK_IMPORTED_MODULE_1__、_test__WEBPACK_IMPORTED_MODULE_2__...
调用__webpack_require__方法并传入所有依赖文件路径ID,返回值就是对应的Module。在调用该函数的时候,又会调用modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
,调用"./js/test.js"函数。
该函数内部除了我们自己写的代码,还会调用__webpack_require**.d 函数将导出的内容作为参数传入,作为属性放在其modules上。其中有三个参数。参数一是\ __webpack_exports**,函数内部需要用到,参数二、三分别是属性名和一个函数。这时候,如果未指定导出的名字(如 export default),那么在__webpack_require**.o找不到module的defualt属性,就会返回false,__webpack_require**.d函数就会将defualt属性存放该函数。
最后返回该module的exports。
Module a: (...) printA: (...) Symbol(Symbol.toStringTag): "Module" __esModule: true get a: ƒ () get printA: ƒ () __proto__: Object 复制代码
之后,回到"./js/index.js",将module赋值给_test__WEBPACK_IMPORTED_MODULE_0__变量。在执行导入的方法时,将其替换成变量的属性调用。
import {printA} from './test1' import add from './test2' printA() add(1,2) //替换后 Object(_test__WEBPACK_IMPORTED_MODULE_0__["printA"])() Object(_test1__WEBPACK_IMPORTED_MODULE_1__["default"])(1, 2) 复制代码
这里的Object将导入内容进行拷贝,防止如原内容的引用地址发生改变发生的错误。
至此,一个简单的boundle.js就分析完毕了。我们对webpack生成boundle文件有了解之后,会更加有利学习打包过程以及原理。