作者/Youhe(前端时空)
公众号「前端时空」每日一题活动 回复「1」看面试题 | 回复「2」看答案
文章已同步发表于
微信公众号「前端时空」
用逆向思维解决问题
一道典型的场景面试题。一共有140g盐,如何用一个天平和两个2g,7g的砝码分三次成90g、50g。这道题用常规思路想可能会很麻烦,但是如果用逆向思维就容易的多了。首先如果要凑成50g,最后一步一定是拿两份25g的盐,25g又可以用砝码和盐来凑,用2g和7g凑成9g盐,再称出7g盐,把所有砝码和这两堆盐凑在一起,9 + 9 + 7 = 25g。 这样三次就可以称出来50g的盐。
从bundle文件开始
我们在学习前端、学习webpack的时候,也不妨利用逆向思维分析问题。按常规来看,学习webpack最好的方式是知晓其背后的原理。事实上,webpack是一个将一切资源都当成模块的模块化打包工具。其打包步骤为:
- 初始化 webpack.config.js,得到最后的配置结果。
- 初始化compiler对象,注册所有配置的插件。
- 根据入口文件,分析模块依赖。
- 使用对应loader处理对应文件。
- 得到每个文件结果,包含每个模块以及他们之间的依赖关系,生成chunk。webpack将所有的模块打包成一个函数。
- 生成bundle.js文件。
在生成bundle.js文件后,html页面就可以利用script标签的src去引入该文件。
我们用一个未使用plugin、loader的简单Demo去就去扒一扒生成bundle.js文件源码,看看有哪些值得我们学习的地方,同时从这个角度去思考webpack。
自执行函数
首先在主体上看,bundle.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__) {}),
});
这就是bundle的主体,十分简洁明了。是一个自执行的匿名函数,接收一个对象作为参数,这个对象键值分别为「模块路径」与一个「匿名函数」。函数体内,有一个「installedModules」对象,从名称上可以推断出是用来存放已安装模块的。之后是十分重要的__webpack_require__函数,这个函数用来安装模块和获取已安装模块。我们详细看下这个函数的内容。
__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结构。
- m属性,用modules为其赋值,即所有模块的集合。
- c属性,用之前介绍过的installedModules为其赋值,存放已安装的模块。
- o属性,作为一个函数,利用Object.prototype.hasOwnProperty.call方法。用来判断参数一(object)上是否存在参数二(property)属性。
- d属性,判断是否符合o属性的方法,如果不是,也就是说参数二name不在参数一exports上,就将getter赋值给exports.name。为什么这么做?下面会提到这里,请继续。
- r属性 将exports属性Symbol.toStringTag赋值为true,将exports的__esModule属性赋值为true。(这样对exports使用toString()方法时将显示‘[Object Module]’)
至此之后自执行函数会执行__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**,
对应__webpack_require__函数中
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);三个参数。
- 内部都是严格模式
- 都执行「webpack_require」.r方法。
对比完毕后,然后开始执行,首先是入口"./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将导入内容进行拷贝,防止如原内容的引用地址发生改变发生的错误。
尾声
至此,一个简单的bundle.js就分析完毕了。我们对webpack生成bundle文件有了解之后,会更加有利学习打包过程以及原理。