JavaScript之无题之让人烦躁的模块化

时间:2022-10-07 07:08:49

  我怎么记得我好像写过相关类型的文章,但是我找遍了我的博客没有~那就再写一遍吧,其实模块化的核心内容也算不上是复杂,只不过需要整理一下,规划一下罢了。嘻嘻。

  开始写标题的时候我就在纠结一件事情,就是,先吃喜欢吃的,还是后吃喜欢吃的,翻译过来就是我应该先写CommonJS和ES6 Module,还是先写CMD和AMD。嗯,我决定了,谁先做好了我就先吃谁

  其实模块化的缘由很简单,就一句话,不对,就一个词,两个字,分类。如果一定让我在加一点,那应该是“隔离”。没了~~但是这么少不太好,我举个可能不那么恰当的例子吧。

  刚开始这个世界上只有三个人,起名字的时候会“刻意”的避开彼此已经叫过的名字,他们在一起生活,日子欣欣向荣,一片美好,对未来充满了期待。

  时间飞逝,三个人变成了三十人,他们勉强还是住在一起,起名字的时候虽然费事一点,但是也还能不重复,日子还是欣欣向荣,一片美好,对未来充满了期待。

  时间又飞逝,三十人变成了三百人,那这不太好管理了,于是三位首领就说,你们有领导能力的几个人站出来,组成各自的部落,去吧,我相信你们可以的。于是每个部落住在一起,部落与部落之间的人可以重名,叫名字的时候再加上一个部落的名称呗,嗯~又一片欣欣向荣。

  时间继续飞逝,三百人变成了三千人,这三千人住在几个大部落里也很不方便,你拿我的苹果,我偷了你得猪,这肯定不行,有碍于社会的稳定发展,于是三个创始者叫上部落的组长说,我们给每个人分一块地,盖一个房子,把三五个人分割成一个家庭,家庭之间由部落作为纽带,关联彼此,在形式上又相互独立,不可以随便拿别家的苹果。很完美~

  时间飞飞飞飞逝,三千人变成了三百万人……我们需要法律了。

  OK,上面的小例子,人,就是函数,部落就是命名空间,房子就是IIFE,法律就是后续发展的模块化规范。那么我们依照上面的描述,如何转换成代码?

一、社会的起源与法律的雏形

  最开始的时候,浏览器只需要简单的图文展示就是可以了,没什么复杂的交互和逻辑的处理,所以,当我们只有三个人的时候,我们可以很*,很随意:

function a(){}

function b(){}

  随着Web的发展,交互的增多,项目的扩大,很容易有人也声明了同样名称的函数,于是纷争开始了,那咋解决纷争呢?嗯,命名空间也就是拆分部落,就像这样:

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

  但是这样并不能真正的解决问题,因为虽然从形式上区分了部落,但是部落之间没有任何的隔离,部落内部也是混乱的,所以各个首领就制定了一个方案,IIFE,利用闭包的特性,来实现数据的隔离,暴露出对外的入口:

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 };
})("xiaowangba");
module.getName();

  传个参数呗,这就是依赖注入。在这个阶段,最有代表性的就是jQuery了,它的封闭性的核心实现,跟上面的代码几乎无异,我们可以看下jQuery的模块的实现:

(function (global, factory) {
  factory(global);
})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
  if (typeof noGlobal === "undefined") {
    window.jQuery = window.$ = jQuery;
  }
  return jQuery;
});

  当然我这里略了很多,你看它,无非就是一个闭包,传入了window和jQuery本身,然后再绑定到window上,这样,我们就只能访问到暴露出来的$以及$上的方法和属性,我们根本无法修改内部的数据。

  OK,到了这个阶段,其实算是一个转折点,我们有了初步的法律,还需要后续针对法律的完善,

二、法律的初现与CommonJs

  随着社会的发展,出现一种规则已成必然,于是commonJs统领举起模块化的大旗,让JavaScript迈向了另一个阶段。commonJs最初由 JavaScript 社区中的 Mozilla 的工程师Kevin Dangoor在Google Groups中创建了一个ServerJs小组。该组织的目标是为web服务器、桌面和命令行应用程序以及浏览器构建JavaScript生态系统。嗯,它的野心很大~,后来,他就就把ServerJs改成了commonJs,毕竟ServerJs的范围有点小,commonJs更符合他们的初衷。

  而后,在同一年的年底,NodeJs出现了,Javascript不仅仅可以用于浏览器,在服务器端也开始攻城略地。NodeJs的初衷是基于commonJs社区的模块化规范,但是NodeJs并没有完全遵循于社区的一些腐朽过时的约束,它实现了自己的想法。

  commonJs规范的写法,如果大家写过NodeJs一定都有所了解,大概是这样的:

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

  看起来挺简单的,但是这里隐藏了一些不那么容易被理解的特性。

  在NodeJs中,一个文件就是一个模块,有自己的作用域,在一个文件里面定义的函数、对象都是私有的,对其他文件不可见。并且,当第一次加载某个模块的时候,NodeJ会缓存该模块,待再次加载的时候,会直接从模块中取出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

  诶?为啥你写的是“exports.”,不是module.exports?NodeJs在实现CommonJs规范的时候为了方便,给每个模块都提供了一个exports私有变量,指向module.exports。有一点要尤其注意,exports 是模块内的私有局部变量,它只是指向了 module.exports,所以直接对 exports 赋值是无效的,这样只是让 exports 不再指向 module.exports了而已。

  我们回到上面的代码,按理来说,我第二次引入的b的name应该是“zaking”啊。但是实际上,在第一次引入之后的引入,并不会再次执行模块的内容,只是返回了缓存的结果。

  另外一个核心的点是,我们导入的是导出值的拷贝,也就是说一旦引入之后,模块内部关于该值的变化并不会被影响。

// a.js
var name = "zaking";
function changeName() {
  name = "xiaowangba";
}
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

  嗯,一切看起来都很不错。

三、争奇斗艳,百家争鸣

  在上一小节,我们简单介绍了模块化的始祖也就是CommonJs以及实现了该规范的NodeJs的一些核心内容。但是NodeJs的实现的一个关键的点是,它在读取或者说加载模块的时候是同步的,这在服务器没什么问题,但是对于浏览器来说,这个问题就很严重,因为大量的同步模块加载意味着大量的白屏等待时间。

  基于这样的问题,从CommonJs中独立出了AMD规范。

1、AMD规范与RequireJs

  AMD,即Asynchronous Module Definition,翻译过来就是异步模块化规范,它的主要目的就是解决CommonJs不能在浏览器中使用的问题。但是RequireJs在实现上,希望可以通吃,也就是可以在任何宿主环境下使用。

  我们先来看个例子:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script>
  <body></body>
  <script>
    require(["./a"]);
  </script>
</html>

  然后,我们的a.js是这样的:

define(function () {
  function fun1() {
    alert("it works");
  }

  fun1();
});

  define用来声明一个模块,require导入。我们还可以这样:

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

  导入前置依赖的模块,在第二个参数也就是回调中执行。RequireJs会在所有的模块解析完成后执行回调函数。就算你倒入了一个没有使用的模块,RequireJs也一样会加载:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script>
  <body></body>
  <script>
    require(["./a", "./b"], function (a, b) {
      a.fun1();
    });
  </script>
</html>

  然后分别是a.js和b.js:

// a.js
define(function () {
  function fun1() {
    alert("it works fun1");
  }

  return {
    fun1: fun1,
  };
});

// b.js
define(function () {
  function fun2() {
    alert("it works fun2");
  }

  return {
    fun2: fun2,
  };
});

  结果大家可以试一试~

  以上就是RequireJs的简单用法,我们据此知道了两个核心内容,RequireJs基于AMD规范,RequireJs会加载你引入的所有模块,哪怕你并不会真的用到它。

2、CMD规范与SeaJs

  由于RequireJs的一些问题,又出现了基于CMD规范的SeaJs,SeaJs和RequireJs有一个最大的不同就是RequireJs希望可以通吃,但是SeaJs则更专注于浏览器,哦对了,CMD的全称叫做:Common Module Definition,即通用模块规范。

  SeaJs的简单用法如下:

// 所有模块都通过 define 来定义
define(function(require, exports, module) {

  // 通过 require 引入依赖
  var a = require('xxx')
  var b = require('yyy')

  // 通过 exports 对外提供接口
  exports.doSomething = ...

  // 或者通过 module.exports 提供整个接口
  module.exports = ...

})
// a.js
define(function(require, exports, module){
    var name = 'morrain'
    var age = 18

    exports.name = name
    exports.getAge = () => age
})
// b.js
define(function(require, exports, module){
    var name = 'lilei'
    var age = 15
    var a = require('a.js')

    console.log(a.name) // 'morrain'
    console.log(a.getAge()) //18

    exports.name = name
    exports.getAge = () => age
})

  上面的代码是从网上抄的,大概说明白了基本的使用方法。我们可以看到,SeaJs的导入和导出的方式,跟NodeJs好像~~而SeaJs从书写形式上,更像是CommonJs和AMD的结合。当然,我只是说书写形式上。

  而AMD和CMD,RequireJs和SeaJs,都是由社区发起的,并没有语言层面的规范,包括CommonJs以及NodeJs,所以,这个时代还是一个百花争艳,没有统一的时代,不过在现在,这些都不重要了。如果非要我说些什么,那就是,忘记这两个东西,去学下面的重点。

四、大一统

  百花争艳的时代确实有些烦人,这个那个那个这个,又都不被官方认可,还好,官方终于还是出手了,ES6的出现,在语言层面上就提出了对于模块化的规范,也就是ES6 Module。它太重要了,具体语法我就不多说了,文末的链接附上了阮一峰大神的《ES6入门指南》关于ES6 Module的地址。

  所以到了ES6的时候,你要学习的就是ES6 Module,NodeJs也在逐步实现对ES6 Module的支持。最终,秦始皇会一统天下,这是必然的结果。

  这篇文章到这里就结束了,说实话,模块化的问题和历史由来已久,从萌芽到统一,至少十几年的过程,而市面上也已有大量的文章介绍彼此的区别和各自的特点,我写来写去,也不过是复制一遍,毫无意义。

  但是我又想学一下模块化,以及模块化的历史,额……,请原谅我的无知,所以才有了这篇文章,但是写着写着,发现我能表达出来的东西并不多,因为都是故事,都是历史,并且对于未来的开发好像也没什么实际的意义和价值。

  所以,在如此纠结的心态下有了这篇文章,原谅我无知又想逼逼的心情吧。

  最后的最后,如果你想学习模块化,在现阶段,只需要去深入学习ES6 Module,和学习一下NodeJs的CommonJs,以及了解一下各模块化的区别即可,因为现在是即将统一,还未完全统一的时候。

 

参考资料:

  https://wiki.commonjs.org/wiki/CommonJS

  https://github.com/seajs/seajs/issues/242

  https://es6.ruanyifeng.com/#docs/module