CommonJS、AMD、CMD、ES Modules

92 阅读8分钟

前言

随着前端的飞速发展,JavaScript代码变得越来越复杂,功能也变得越来越强大。

Ajax的出现,前后端开发分离,意味着后端返回数据后,前端通过JavaScript来进行页面的渲染。

SPA的出现,前端的页面也变得更加的复杂(前端路由、状态管理等)。

后来nodejs的出现,JavaScript也能够开发复杂的后端程序。

所以模块化已经是JavaScript的一个非常迫切的需求, 但是JavaScript本身直到ES6才有了自己的模块化方案,在此之前,为了让JavaScript支持模块化,涌现了很多不同的模块化规范:AMD、CMD、CommonJS

1.没有模块化的问题

没有模块化的时候带来了很多的问题,比如命名冲突的问题。对此我们的解决方案是使用IIFE(立即函数调用表达式),但是解决问题的同时又带来了新的问题:

1、必须记得每一个模块返回的对象名,才能在其他模块正确的使用

2、每个文件的代码都必须包裹在一个匿名函数中编写

3、没有规范的情况下,每个人可能会随意命名导致可能出现模块名称相同的情况

<script>
  var moduleBar = (function () {
    var name = "coderwhh";
    var age = 18;
    return {
      name,
      age
    }
  })()
</script>

<script>
  (function() {
    var name = "coderzbj";
    console.log(name);
  })();
  
  
</script>

<script>
  console.log(moduleBar.name);
  console.log(moduleBar.age);
</script>

2.CommonJS

CommonJS只是一个规范,最初提出来是在浏览器以外的地方使用,且当时命名为ServerJS,后来为了提现他的广泛性,修改为CommonJS,简称cjs。

在commonjs中:exports和module。exports负责对模块中的内容进行导出,require函数帮助我们导入其他模块(自定义模块,系统模块,第三方模块等)

  • exports导出

    • exports是一个对象,在这个对象上添加的属性就会被导出(eg: exports.name = name)

    • 另外一个文件中可以导入(eg: const foo = require('./foo.js'))

    • 经过上面的导出与导入操作,完成了什么呢?意味着foo变量等于exports对象,也就是说require通过各种查找方式,最终找到了exports这个对象且将这个exports对象赋值给了foo变量,所以bar变量就是exports对象了。

    • 实际上就是一个浅层拷贝(foo对象就是exports对象的引用赋值)

  • module.exports和exports有什么区别呢?

    • commonjs中是没有module.exports的概念的,为了实现模块的导出,node中使用的是Module的类每一个模块都是Module的一个实例,也就是module,所以在node中真正用于导出的其实是module.exports而不是exports。然而exports也可以实现导出的原因是module.exports = exports
  • require细节(require(X)) require是一个函数,可以帮我们引入一个文件(模块)中导出的对象,require的导入规则如下

    1.X为核心模块:直接返回核心模块,停止查找

    2.X是以./或../或/(根目录)开头的

    • 第一步:将X当做一个文件在对应的目录下查找,如果有后缀名就按照后缀名的格式查找,如果没有后缀名,会按照如下顺序:直接查找文件X -> X.js -> X.json -> X.json
    • 第二步:没找到对应的文件,将X作为一个目录,查找目录下面的index文件:X/index.js -> X/index.json -> X/index.node
    • 若没找到则报错 3.直接是一个X(无路径),并且X不是核心模块: 会从当前文件所在的目录中的nodule_modules中查找,若无则向上查找,直到找到根目录为止。
  • 模块的加载过程

    • 模块在第一次引入时,模块中的代码会执行一次

    • 模块多次引用时会缓存,最终只加载(运行)一次(如何做到只加载一次?每个模块对象module都有一个loaded属性,false表示未加载,true表示已加载)

    • 如果循环引入,那么加载顺序是? eg:

    image.png 这其实为图结构,在遍历时有深度优先和广度优先搜索,node使用的是深度优先:main -> aaa -> ccc -> ddd -> eee -> bbb(在main中先引入aaa.js再引入bbb.js则为此顺序)

  • CommonJS通过module.exports导出的是一个对象

    • 导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量

    • 他们指向的都是同一个对象,那么一个变量修改了对象的属性,所有的地方都会被修改

  • CommonJS规范的缺点

    • commonjs加载模块是同步的:同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行,在服务器不会有问题,因为服务器加载的js都是本地文件,速度非常快
    • 如果用于浏览器,浏览器需要先将js文件下载下来,之后再运行,那么采用同步就意味着后续的js都无法正常执行
    • 所以在浏览器中,我们通常不使用commonjs规范,如果在webpack中使用commonjs,webpack会将代码转换为浏览器直接执行的代码

3.AMD

AMD(asynchronous module definition:异步模块定义的缩写)主要应用于浏览器的一种模块化规范,它采用的是异步加载模块。另外规范只是定义代码应该如何去编写,只有有了具体的实现才能被应用,AMD常用的实现库有require.js和curl.js。

如何使用?

  1. 下载require.js传送门

  2. 在html文件的script标签中引入require.js以及定义入口文件 <script src="./lib/require.js" data-main="./index.js"></script> data-main的作用是加载完src的文件后执行该文件。

示例:

// index.html
 <script src="./lib/require.js" data-main="./index.js"></script>
// index.js
(function() {
  require.config({
    baseUrl: '',
    paths: {
      "bar": "./modules/bar",
      "foo": "./modules/foo"
    }
  })

  require(['foo'], () => {});
})();
// bar.js
define(function() {
  const name = "coderwhh";
  const sayHello = function(name) {
    console.log("你好" + name);
  }
  return {
    name,
    sayHello
  }
});
// foo.js
define(['bar'], function(bar) {
  console.log(bar.name);
  bar.sayHello("coder");
});

4. CMD

CMD(common module definition:通用模块定义的缩写),他也采用异步加载模块的方式,且他将commonjs的优点吸收了过来(实现方案:SeaJS)

如何使用?

  1. 下载传送门

  2. 引入sea.js和使用主入口文件,使用seajs指定主文件入口

// index.html
  <script src="./lib/sea.js"></script>
  <script>
    seajs.use('./index.js');
  </script>
// index.js
define(function(require, exports, module) {
  const foo = require('./modules/foo');
  console.log(foo.name);
  foo.sayHello("coder");
})
// foo.js
define(function(require, exports, module) {
  const name = "coderwhh";
  const sayHello = function(name) {
    console.log("你好" + name);
  }

  module.exports = {
    name: name,
    sayHello: sayHello
  }
});

5.ES Module

ES Module和CommonJS的模块化有一些不同之处:1.它使用了import和export关键字 2.另一方面它采用编译期的静态分析,并且也加入了动态引用的方式

直接引用:<script src="./index.js" type="module"></script>会报跨域错误,可以使用VS Code的插件Live Serve来运行项目。详细报错原因以及解决方案 传送门

  • export关键字:将一个模块中的变量、函数、类等导出

      1. 在声明语句前直接加export关键字
      1. 将所有需要导出的标识符,放到export后的{}中。(在这的{}里面并不是ES6的对象的字面量的增强写法,{}也不是表示一个对象,错误写法{name: "coder"}
    • 3.导出时可以给标识符起一个别名
  • import关键字:负责从另外一个模块中导入内容

    • import {标识符列表} from "模块"(这儿的{}也不是一个对象,里面只是存放导入的标识符列表内容)
    • 导入时给标识符起别名
    • 通过 * 将模块功能放到一个模块对象(a module object)上
  • export 和 import结合使用

    eg:export { dateFormat as untilDateFormat} from 'util-date.js' 为什么这样做?在封装功能库时,通常我们希望将暴露的所有接口放到一个文件中去,这样方便指定统一接口规范,也方便阅读。

  • default

    默认导出:默认导出在导出时可以不需要指定名字;在导入时不需要使用{},并且可以自己来指定名字。(在一个模块中,只能有一个默认导出)

  • import 函数

    通过import加载一个模块是不可以放置在逻辑代码中的。

    eg: if(true) { import bar from "bar.js" },为什么不可以这样写?是因为ES Module 在被js引擎解析时,就必须得知道他的依赖关系,由于这个时候js代码没有任何的执行,所以无法进行逻辑判断,根据代码的执行情况来获取依赖关系。

    如果确实希望动态加载某个模块,可以使用import()函数来动态加载

    let flag = true;
    if (flag) {
      import('./modules/foo.js').then(res => {
        console.log(res);
      }).catch(err => {
        console.log(err);
      })
    } else {
      import('./modules/bar.js').then(res => {
        console.log(res);
      }).catch(err => {
        console.log(err);
      })
    }
    
  • ES Module加载过程

    ES Module加载js文件的过程是编译(解析)时加载的,并且是异步的

    • 编译(解析)时加载,意味着import不能和运行时相关的内容放在一起使用

    • 比如from后的路径需要动态获取

    • 比如不可以将import放到if语句的代码块中

    • 所以有时候也称ES Module是静态解析的,而不是动态或者运行时解析的 异步: 意味着js引擎在遇到import时会去获取这个js文件,但是获取的过程是异步的,并不会阻塞主线程继续执行。也就是说设置了type = module的代码,相当于在script标签上加了async属性。

    <script src="./index.js" type="module"></script>
    <!-- 这段js代码不会被阻塞执行 -->
    <script>
    console.log("========================>");
    </script>
    
  • ES Module 通过export导出的是变量本身的引用

    • export在导出一个变量时,js引擎会解析这个语法,并且创建模块环境记录

    • 模块环境记录会和变量进行绑定,且这个绑定是实时的,所以如果在导出的模块中修改了变化,那么在导入的地方可以实时获取最新的变量。

    • 注意:导入的地方不可以修改变量,因为他只是被绑定到了这个变量上(其实是一个常量)

  • CommonJS和ES Module

    • 通常情况下,commonjs不能加载esmodule:因为commonjs是同步加载的,但是es module必须通过静态分析等,无法在这个时候执行JavaScript
    • 多数情况下,es module可以加载commonjs:es module在加载commonjs时,会将其module.exports导出的内容作为default导出方式来使用。(需要看具体实现)