阅读 238

从 bundle.js 源码学习 Webpack

作者/Youhe(前端时空)

公众号「前端时空」每日一题活动 回复「1」看面试题 | 回复「2」看答案

文章已同步发表于

微信公众号「前端时空」

用逆向思维解决问题

一道典型的场景面试题。一共有140g盐,如何用一个天平和两个2g,7g的砝码分三次成90g、50g。这道题用常规思路想可能会很麻烦,但是如果用逆向思维就容易的多了。首先如果要凑成50g,最后一步一定是拿两份25g的盐,25g又可以用砝码和盐来凑,用2g和7g凑成9g盐,再称出7g盐,把所有砝码和这两堆盐凑在一起,9 + 9 + 7 = 25g。 这样三次就可以称出来50g的盐。

从bundle文件开始

我们在学习前端、学习webpack的时候,也不妨利用逆向思维分析问题。按常规来看,学习webpack最好的方式是知晓其背后的原理。事实上,webpack是一个将一切资源都当成模块的模块化打包工具。其打包步骤为:

  1. 初始化 webpack.config.js,得到最后的配置结果。
  2. 初始化compiler对象,注册所有配置的插件。
  3. 根据入口文件,分析模块依赖。
  4. 使用对应loader处理对应文件。
  5. 得到每个文件结果,包含每个模块以及他们之间的依赖关系,生成chunk。webpack将所有的模块打包成一个函数。
  6. 生成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文件有了解之后,会更加有利学习打包过程以及原理。

精彩文章

理想主义团队的开源作品之Chameleon跨端框架 React 中必会的 10 个概念 一道面试题引发关于 js 隐式转换的思考 前端首屏耗时测量方法 一分钟理解 JavaScript 发布订阅模式 前端响应式你了解多少?

关注我们