做者/Youhe(前端时空)html
公众号「前端时空」每日一题活动 回复「1」看面试题 | 回复「2」看答案前端
文章已同步发表于webpack
微信公众号「前端时空」web
一道典型的场景面试题。一共有140g盐,如何用一个天平和两个2g,7g的砝码分三次成90g、50g。这道题用常规思路想可能会很麻烦,可是若是用逆向思惟就容易的多了。首先若是要凑成50g,最后一步必定是拿两份25g的盐,25g又能够用砝码和盐来凑,用2g和7g凑成9g盐,再称出7g盐,把全部砝码和这两堆盐凑在一块儿,9 + 9 + 7 = 25g。 这样三次就能够称出来50g的盐。面试
咱们在学习前端、学习webpack的时候,也不妨利用逆向思惟分析问题。按常规来看,学习webpack最好的方式是知晓其背后的原理。事实上,webpack是一个将一切资源都当成模块的模块化打包工具。其打包步骤为:数组
在生成bundle.js文件后,html页面就能够利用script标签的src去引入该文件。
咱们用一个未使用plugin、loader的简单Demo去就去扒一扒生成bundle.js文件源码,看看有哪些值得咱们学习的地方,同时从这个角度去思考webpack。微信
首先在主体上看,bundle.js是一个自执行匿名函数,经过传入一个对象参数(版本v4.0.0+,旧版本是一个数组)。下面是将无关代码去掉的精简部分。入口文件是一个index.js文件,在index.js中使用import引入了test.js。markdown
(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__) {}),
});
复制代码
这就是bundle的主体,十分简洁明了。是一个自执行的匿名函数,接收一个对象做为参数,这个对象键值分别为「模块路径」与一个「匿名函数」。函数体内,有一个「installedModules」对象,从名称上能够推断出是用来存放已安装模块的。以后是十分重要的__webpack_require__函数,这个函数用来安装模块和获取已安装模块。咱们详细看下这个函数的内容。app
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**,函数内部须要用到,参数2、三分别是属性名和一个函数。这时候,若是未指定导出的名字(如 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将导入内容进行拷贝,防止如原内容的引用地址发生改变发生的错误。
至此,一个简单的bundle.js就分析完毕了。咱们对webpack生成bundle文件有了解以后,会更加有利学习打包过程以及原理。