购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.1 模块化的目的

在前端领域提及模块化时,人们首先联想到的无疑是ES6 Module。作为ECMAScript颁布的标准模块化方案,ES6 Module因其权威性在前端模块化方案领域占据了举足轻重的地位。那么,为什么模块化对我们来说是必要的?为何在JavaScript诞生之初并未设计出模块化方案,而是直到ES6标准时才推出了ES6 Module呢?带着这些疑问,我们将回顾前端模块化方案的历史演变,并简要探讨其应用场景。

1.1.1 模块化的原始时期

在浏览器早期发展阶段,其功能相对单一,主要集中在“图文展示”。因此,当时的浏览器并不需要复杂的模块化设计。可以说,那时还没有真正意义上的前端和模块化概念,只有基本的脚本语言JavaScript和HTML。仅凭这两者,就能构建出一个功能完善、能够展示图文的浏览器界面。

早期的HTML页面并没有CSS,而CSS的出现正是为了解决HTML在样式处理上的不足。使用HTML同时处理结构和样式既不优雅,也容易导致代码冗余和混乱。因此,能够将结构和样式分离的CSS(层叠样式表)应运而生,并在前端开发中发挥着至关重要的作用。

稍微跑题了,接着回到模块化的讨论。当我们的页面仅包含简单的图文展示,且规模较小时,可以通过这种方式编写代码:


    function a(){};
    function b(){};

即便情况变得极端,我们仍可以使用全局搜索,或者干脆等待页面报错后再逐一纠正命名错误。然而,随着互联网的迅猛发展,项目规模日益庞大,单个页面往往需要引入众多JavaScript文件,命名冲突也层出不穷。在这种情况下,显然没有人愿意逐个文件去检查潜在的命名冲突。那么,我们该如何应对呢?

可以用命名空间来解决这个问题:


    var zaking1 = {
        a:function(){},
        b:function(){}
    }
    var zaking2 = {
        a:function(){},
        b:function(){}
    }

然而,这样的做法似乎也不够理想,因为它只是形式上的隔离,而非真正意义上的隔离。随着项目体积不断增大,调用可能会变得冗长,例如:


    zaking1.zaking2.zaking3.getProduct.xxx.xxx

即使设置了命名空间1和命名空间2,它们之间的方法仍能相互调用,导致混乱程度不言而喻。既然简单的隔离措施难以奏效,为何不利用闭包的特性来真正隔离各个命名空间?通过这种方式,我们可以精确控制哪些内容对外可见,从而实现更为清晰有序的结构。


    var module = (function () {
       var name = "zaking";
       function getName() {
               console.log(name);
       }
       return { getName };
    })();
    module.getName();

至此,我们成功解决了隔离性问题,通过闭包创建了独立的作用域,确保外部只能通过我们提供的接口与模块进行交互。然而,新的挑战随之而来:如果我们想向这个模块传递一些数据或信息,该如何处理呢?由于模块的封闭性,它无法获取外界的任何信息,而这显然不是我们所期望的结果。


    var module = (function (neighbor) {
        var name = "zaking";
        function getName() {
                console.log(name + "和邻居:" + neighbor);
        }
        return { getName };
    })("xiaowang");
    module.getName();

我们可以通过向函数传递参数的方式来解决向模块传递数据的问题。听起来很熟悉,对吧?没错,这正是早期版本的jQuery外层架构所采用的策略。

这种设计构成了早期模块化的高级方案,在jQuery中的应用尤为突出,甚至在当今许多项目中仍被广泛使用。

1.1.2 Node.js与CommonJS

随着社会的发展,模块化规则的出现已成为必然。于是,CommonJS举起了模块化的大旗,引领JavaScript迈向新的发展阶段。CommonJS的诞生源于社区的努力,最初是由JavaScript社区的Mozilla工程师Kevin Dangoor在Google Groups创建了SeverJS小组。该小组的目标是为Web服务器、桌面、命令行应用程序以及浏览器构建一个生态系统。鉴于其宏大的愿景和目标,SeverJS后来更名为CommonJS,因为SeverJS这个名称看起来与浏览器无关。

同年年底,Node.js横空出世。此时,JavaScript的应用范围已突破浏览器的限制,开始在服务器端展现出强大实力。Node.js的初衷是基于CommonJS社区来实现模块化规范,但它并未完全照搬CommonJS的所有内容,而是进行了创新和发展,去除了其中一些过时且不合时宜的元素,最终取得了超越前人的成就。


    // a.js
    module.exports = 'zaking'
    // b.js
    const a = require("./a");
    console.log(a); // zaking

在Node.js环境中,每个JS文件都被视为一个独立的模块。每个模块都有自己的作用域,这意味着在一个JS文件中定义的函数和对象都是私有的,无法被其他JS文件访问。此外,Node.js在首次加载某个模块时,会将该模块缓存起来。当再次加载同一模块时,系统会直接从缓存中取出它的module.exports属性并返回,从而避免了重复加载。例如:


    // a.js
    var name = "zaking";
    exports.name = name;

    // b.js
    var a = require("./a.js");
    console.log(a.name); // zaking
    a.name = "xiaoba";
    var b = require("./a.js");
    console.log(b.name); // xiaoba

这里第二次打印的b.name显示为更改后的name,这正是前面提到的缓存机制。当模块第一次被引入时,Node.js会执行模块内的代码并缓存结果。在后续引入中,Node.js不会重新执行模块代码,而是直接返回之前缓存的module.exports对象。

在前面的示例代码中,虽然没有直接使用module.exports,但使用了exports。为了简化模块的使用,Node.js为每个模块创建了一个私有变量exports,它指向module.exports。

需要注意的是,exports是模块内部的私有变量,仅仅是对module.exports的引用。因此,直接修改exports的值并不会影响module.exports,也就是说,这样的操作是无效的。


    exports = "aaa"

这样的操作只是让exports不再指向module.exports,但不会改变module.exports的内容。

此外,还需要了解一个关键点:当我们导入一个模块时,实际上获得的是导出值的副本。这意味着,一旦模块内部的值被导出,后续对该值的任何更改都不会影响已导出的副本。


    // a.js
    var name = "zaking";
    function changeName() {
         name = "xiaowang";
    }
    exports.name = name;
    exports.changeName = changeName;

    // b.js
    var a = require("./a.js");
    console.log(a.name); // zaking
    a.changeName();
    console.log(a.name); // zaking

1.1.3 AMD与CMD争奇斗艳

AMD和CMD是前端历史上两种颇具影响力的模块化方案,它们从不同的角度出发,为浏览器端的模块化做出了杰出的贡献。可以说,它们共同书写了前端模块化历史中不可磨灭的一页。

1.AMD规范

AMD(Asynchronous Module Definition,异步模块化定义)的主要目标是解决浏览器端缺乏模块化方案的问题,从而鼓励开发者在浏览器页面中采用模块化开发方式。

需要强调的是,AMD是一种模块化方案,而非具体的实现。RequireJS正是这一方案的具体实现者。RequireJS的视野并不局限于浏览器环境,而是希望能够在任何使用JavaScript语言的宿主环境中应用这一模块化方案。

其中,a.js文件是这样的:

这个机制简单易懂:define用于声明一个模块,require用于导入一个模块。在使用require导入模块时,还可以传入一个回调函数作为参数,以便在所有模块都导入后执行:


    require(["./a"], function () {
        alert("load finished");
    });

最后,我们来看一个稍微复杂一点的例子:

打开页面后,可以看到如图1-1所示的结果。

图1-1 AMD使用示例效果

可以看到,这里只执行了fun1。我们审查一下元素,如图1-2所示。

图1-2 AMD模块化示例审查元素

虽然我们仅使用了a.js中的内容,但RequireJS并不关心你是否使用,它会直接将所有内容都导入进来。

2.CMD规范

与AMD规范的RequireJS相比,使用体验似乎不太理想。有时,它会导入一些我们并未使用的模块,让人感觉有些冗余。此外,RequireJS的野心很大,希望成为所有使用JavaScript环境的模块化方案,但这种贪心的策略往往导致在实际应用中难以满足所有需求。

在这样的背景下,基于CMD(Common Module Definition,通用模块化定义)规范的SeaJS应运而生。与RequireJS不同,SeaJS专注于为浏览器端提供模块化解决方案。

它的大致用法如下:

然后JS文件是这样的:

从示例代码中可以看到,SeaJS通过define函数封装整个模块,并在模块内部通过require和exports参数作为模块导入和导出的接口。这种设计使得SeaJS在使用上的负担接近于CommonJS。因此,开发者可以像在Node.js中使用CommonJS一样,无须花费太多时间学习,就能轻松地将SeaJS的基本用法应用于浏览器环境。这也是SeaJS的设计初衷。

1.1.4 ES6 Module一统天下

在前面的内容中,我们详细探讨了3种由社区提供的模块化方案,它们在前端模块化历史进程中都扮演了至关重要的角色,可以说是一个群雄逐鹿的时代。

然而,正如了解中国历史的人所知,当群雄纷争、格局混乱时,往往也预示着统一的曙光即将到来。在这样的背景下,ECMAScript在语言层面提出了模块化的规范,即ES6 Module。

ES6 Module的问世标志着前端模块化方案的混乱时代即将结束,统一的时代即将来临,同时Node.js也在不断加强对它的支持。可以预见,在未来,ES6 Module将成为前端领域唯一的模块化方案。因此,读者一定要加强对ES6 Module的学习,这将是进入前端领域必备的技能。 9VKi5nkC5gpoo99ISaC7HUE8RxkF7YAp4B/quf1ZrO/l9ypW8vf4EZPum7lWlEsO

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开