• JavaScript是如何实现模块化的?


    一. 为什么要有模块化?

    1. 内联的JavaScript语句

    JavaScript诞生之初的应用比较简单,例如用户输入校验。代码量少,一般直接写在html中。

    <script>
    var name="kobe"
    console.log(name)
    script>
    
    • 1
    • 2
    • 3
    • 4

    2. 独立的JavaScript文件

    随着用户体验要求变高,前端承载的功能变多了,代码量也随着膨胀

    例如,axios的出现带来了前后端分离,前端通过后端接口获取数据,动态渲染页面;SPA让页面切换更丝滑,但需要实现前端路由,状态管理等功能。

    为了提高代码的可复用性,人们开始将JavaScript从html解耦,封装成独立的模块

    // index.html
    <script src="moduleA.js">script>
    
    // moduleA.js
    var name="kobe";
    
    • 1
    • 2
    • 3
    • 4
    • 5

    引入的外部JavaScript文件中声明的变量是全局作用域的,当引用的第三方模块多了,势必会造成全局变量冲突

    // index.html
    <script src="moduleA.js">script>
    <script src="moduleB.js">script>
    
    // moduleA.js
    var name="kobe";
    
    // moduleB.js
    var name="iverson";
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3. 命名空间

    解决全局变量冲突的方法之一是给每个模块分配命名空间,进行隔离。

    // index.html
    <script src="moduleA.js">script>
    <script src="moduleB.js">script>
    
    // moduleA.js
    var moduleA = {};
    moduleA.name="kobe";
    
    // moduleB.js
    var moduleB = {};
    moduleB.name="iverson";
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是,JavaScript对象属性默认是公有的,这意味着模块内的变量既能被外部访问let scopeVar = moduleA.name,也可能被外部修改moduleA.name = ‘xxx’。那么,如何保护数据不被外部修改?

    4. 闭包

    为了保护数据不被外部修改,人们将模块封装在函数作用域内。

    // index.html
    <script src="moduleA.js">script>
    <script src="moduleB.js">script>
    
    // moduleA.js
    let moduleA = (function (){
        let name = 'kobe';
        return {
            getName:function(){
                return name;
            }
        }
    })();
    
    // moduleB.js
    let moduleB = (function (){
        let name = 'iverson';
        return {
            getName:function(){
                return name;
            }
        }
    })();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    使用立即调用的函数表达式(IIFE) 创建闭包,将模块封装在函数作用域内,并对外提供可访问模块的公共API。这样,在moduleB中可以通过moduleA.getName()访问数据,但是必须保证moduleA在moduleB之前完成加载。当模块数量多了,如何管理好模块依赖又是一个问题。

    小结

    综上所述,为了提高JavaScript代码的可复用性,开发者尝试利用JavaScript语言特性来模拟实现模块化。分别用命名空间闭包来解决全局变量冲突和实现数据保护。然而,管理模块依赖是比较复杂的问题,因此诞生了CommonJS,AMD,UMD等模块化方案。

    二. 模块模式

    模块化思想就是把代码拆分成独立的模块,逻辑独立,各自实现,然后再把它们连接起来实现完整功能。对应的实现模式就叫模块模式,它是所有模块化系统的基础。

    1. 模块标识符

    每个模块都有一个可用于引用它的标识符。

    import axios form "axios"
    import msgBox from "/src/ui.js"
    
    • 1
    • 2
    • 原生浏览器的模块标识符是绝对文件路径
    • Node.js环境默认会搜索node_modules目录,可省去路径。

    2.模块依赖

    模块化系统的核心是管理依赖,即保证模块正常运行时所需要的外部依赖能够完成加载和初始化。

    // moduleA.js
    import moduleB form "moduleB.js"
    
    console.log(moduleB.getName())
    
    • 1
    • 2
    • 3
    • 4

    3.模块加载

    加载模块的一般步骤是: 加载模块及其依赖的代码,在所有依赖加载和初始化之后,才会执行入口模块。

    a. 同步加载

    浏览器加载JavaScript文件默认是同步的。

    例如moduleA.js依赖了moduleB.js,moduleB.js依赖了moduleC.js,则必需按以下顺序加载依赖。

    <script src="moduleC.js">script>
    <script src="moduleB.js">script>
    <script src="moduleA.js">script>
    
    • 1
    • 2
    • 3

    同步加载的缺点很明显:

    • 性能: 阻塞页面渲染,直到所有依赖按顺序加载完。
    • 复杂性: 需要手动管理依赖的加载顺序。

    b. 异步加载

    异步加载模块,不会阻塞页面,只需要在加载完成后执行回调。

    例如上面同步加载的例子改用异步加载,则不会阻塞页面渲染,只需要在moduleC.js和moduleB.js完成加载和初始化之后,回调执行moduleA.js。

    可使用