Javascript

从 boundle.js 源码学习 Webpack

本文主要是介绍从 boundle.js 源码学习 Webpack,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

作者/Youhe(前端时空)

文章已同步发表于

微信公众号「前端时空」

用逆向思维解决问题

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

从boundle文件开始

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

  1. 初始化 webpack.config.js,得到最后的配置结果。
  2. 初始化compiler对象,注册所有配置的插件。
  3. 根据入口文件,分析模块依赖。
  4. 使用对应loader处理对应文件。
  5. 得到每个文件结果,包含每个模块以及他们之间的依赖关系,生成chunk。webpack将所有的模块打包成一个函数。
  6. 生成boundle.js文件。

在生成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__函数,这个函数用来安装模块和获取已安装模块。我们详细看下这个函数的内容。

__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将导入内容进行拷贝,防止如原内容的引用地址发生改变发生的错误。

尾声

至此,一个简单的boundle.js就分析完毕了。我们对webpack生成boundle文件有了解之后,会更加有利学习打包过程以及原理。

精彩文章

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


喜欢我们的小伙伴
请点击点赞、分享、评论哟
关注我们的公众号,学习知识不迷路

这篇关于从 boundle.js 源码学习 Webpack的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!