现在Javascript模块规范有这样几种:CommonJSAMD ,CMD,UMD ES6 模块规范

模块化原始写法

在没有CommonJSES6的时候,我们想要达到模块化的效果可能有这么三种:

1、一个函数就是一个模块

2、一个对象就是一个模块

3、 立即执行函数为一个模块

CommonJS规范

1、暴露定义模块

// 1. 正确  
module.exports = {  
    name: 'lindaidai',  
    sex: 'boy'  
}  
  
// 2. 正确  
exports.name = 'lindaidai';  
exports.sex = 'boy'  
  
// 3. 正确  
module.exports.name = 'lindaidai';  
module.exports.sex = 'boy'  
  
// 4. 无效  
exports = {  
    name: 'lindaidai',  
    sex: 'boy'  
}  

2、引用(引入)模块

对于模块的引用使用全局方法 require() 。这个全局方法是 node 中的方法,不是 window 下面的。 require() 它是 Node.js 中的一个全局方法,并不是CommonJS独有的,CommonJS只是众多规范中的其中一种.

  • 使用 module.exports = {} 或者 exports.name = xxx 导出模块
  • 使用 const m1 = require('./m1') 引入模块

require() 的参数是一个表达式。

var m1Url = './m1.js';  
var m1 = require(m1Url);  
  
// 甚至做一些字符串拼接:  
var m1 = require('./m' + '1.js'); 

3、模块标识符(标识)

模块标识符其实就是你在引入模块时调用 require() 函数的参数。

// 直接导入  
const path = require('path');  
// 相对路径  
const m1 = require('./m1.js');  
// 直接导入  
const lodash = require('lodash');  

我们引入的模块会有不同的分类,像path这种它是Node.js就自带的模块,m1是路径模块,lodash是我们使用npm i lodash下载到node_modules里的模块。

引入的模块分为以下三类:

  • 核心模块(Node.js自带的模块)
  • 路径模块(相对或绝对定位开始的模块)
  • 自定义模块(node_modules里的模块)

三种模块的查找方式:

  • 核心模块,直接跳过路径分析和文件定位
  • 路径模块,直接得出相对路径就好了
  • 自定义模块,先在当前目录的node_modules里找这个模块,如果没有,它会往上一级目录查找,查找上一级的node_modules,依次往上,直到根目录下都没有, 就抛出错误。

自定义模块的查找过程:

这个过程其实也叫做路径分析

文件定位:

导入的模块它的后缀(扩展名)是可以省略的啊,那Node怎么知道我们是导入了一个js还是一个json呢?这其实就涉及到了文件定位。

NodeJS中, 省略了扩展名的文件, 会依次补充上.js, .node, .json来尝试, 如果传入的是一个目录, 那么NodeJS会把它当成一个包来看待, 会采用以下方式确定文件名

第一步, 找出目录下的package.json, 用JSON.parse()解析出main字段

第二步, 如果main字段指定的文件还是省略了扩展, 那么会依次补充.js, .node, .json尝试.

第三步, 如果main字段制定的文件不存在, 或者根本就不存在package.json, 那么会默认加载这个目录下的index.js, index.node, index.json文件.

4、CommonJS 规范特点

  • 所有代码都运行在模块作用域,不会污染全局作用域;
  • 模块是同步加载的,即只有加载完成,才能执行后面的操作;
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;
  • CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。

AMD 规范

模块化这种概念不仅仅适用于服务器端,客户端同样也适用。而CommonJS规范就不太适合用在客户端(浏览器)环境。AMD它的产生很大一部分原因就是为了能让我们采用异步的方式加载模块。AMDAsynchronous Module Definition的缩写,也就是“异步模块定义”

1、 定义并暴漏模块

它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

此时就需要另一个重要的方法来定义我们的模块:define()

它其实是会有三个参数:

define(id?, dependencies?, factory)
  • id: 一个字符串,表示模块的名称,但是是可选的
  • dependencies: 一个数组,是我们当前定义的模块要依赖于哪些模块,数组中的每一项表示的是要依赖模块的相对路径,且这个参数也是可选的
  • factory: 工厂方法,一个函数,这里面就是具体的模块内容了

define并不是Node.js自带的方法啊,它只是名义上规定的这样一个方法,但是你真的想要去用还是得使用对应的JavaScript库,也就是我们常常听到的:

目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js

nodejs环境下使用,在项目根目录下执行

npm i requirejs

示例模块文件写成( 补充注意:对象同名属性简写是ES6才出来的):

define(function () {  
  var add = function (a, b) {  
    return a + b;  
  }  
  return {  
    add: add  
  }  
})  

2、引用模块

var requirejs = require("requirejs"); //引入requirejs模块  
  
requirejs(['math'],function(math) {  
  console.log(math)  
  console.log(math.add(1, 2));  
})  

3、依赖其他模块的define

math.js文件内代码依赖m1.js模块

define(['m1'], function (m1) {  
  console.log('我是math, 我被加载了...')  
  var add = function (a, b) {  
    return a + b;  
  }  
  var print = function () {  
    console.log(m1.name)  
  }  
  return {  
    add: add,  
    print: print  
  }  
})  

既然是使用AMD的规范,那我们肯定是要一统到底了,m1.js中用的还是CommonJS的规范,当然不行了。也应该改写成AMD规范

define(function () {  
  console.log('我是m1, 我被加载了...')  
  return {  
    name: 'lindaidai',  
    sex: 'boy'  
  }  
})  

CMD规范

CMD (Common Module Definition)被seajs推崇,依赖就近,用的时候再require

define(function(require, exports, module) {  
  var math = require('./math');  
  math.print()  
})  

AMD类似,其中define()参数相同,都是define(id?, dependencies?, factory),区别在最后一个factory参数

factory函数中是会接收三个参数:

  • require

  • exports

  • module

    这三个很好理解,对应着之前的CommonJS那不就是:

  • require:引入某个模块

  • exports:当前模块的exports,也就是module.exports的简写

  • module:当前这个模块

现在再来说说AMDCMD的区别。

虽然它们的define()方法的参数都相同,但是:

  • AMD中会把当前模块的依赖模块放到dependencies中加载,并在factory回调中拿到加载成功的依赖
  • CMD一般不在dependencies中加载,而是写在factory中,使用require加载某个依赖模块

因此才有了我们常常看到的一句话:

AMD和CMD最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块。

// 所有模块都通过 define 来定义  
define(function(require, exports, module) {  
  
  // 通过 require 引入依赖  
  var $ = require('jquery');  
  var Spinning = require('./spinning');  
  
  // 通过 exports 对外提供接口  
  exports.doSomething = ...  
  
  // 或者通过 module.exports 提供整个接口  
  module.exports = ...  
  
});  

AMDCMD的区别

1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块

2、CMD推崇就近依赖,只有在用到某个模块的时候再去require

AMDCMD最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块。

UMD

Universal Module Definition 即通用模块定义。UMDAMDCommonJS的糅合.

AMD 模块以浏览器第一的原则发展,异步加载模块。 CommonJS 模块以服务器第一原则发展,选择同步加载。它的模块无需包装(unwrapped modules)。 这迫使人们又想出另一个更通用的模式 UMD(Universal Module Definition),实现跨平台的解决方案。

  • UMD 先判断是否支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。再判断是否支持 AMDdefine 是否存在),存在则使用 AMD 方式加载模块。
(function (window, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define(factory);
    } else {     
        window.eventUtil = factory();
    }
})(this, function () {
    //module ...
});

ES6 Modules规范

ES6标准出来后,ES6 Modules规范算是成为了前端的主流吧,以import引入模块,export导出接口被越来越多的人使用。

1、export 导出模块

命名式导出
// 以下两种为错误  
// 1.  
export 1;  
// 2.  
const a = 1;  
export a;  
  
// 以下为正确  
// 3.  
const a = 1;  
export { a };  
  
// 4. 接口名与模块内部变量之间,建立了一一对应的关系  
export const a = 1, b = 2;  
  
// 5. 接口名与模块内部变量之间,建立了一一对应的关系  
export const a = 1;  
export const b = 2;  
  
// 或者用 as 来命名  
const a = 1;  
export { a as outA };  
  
const a = 1;  
const b = 2;  
export { a as outA, b as outB };  
默认导出
// 1.  
const a = 1;  
export default a;  
  
// 2.  
const a = 1;  
export default { a };  
  
// 3.  
export default function() {}; // 可以导出一个函数  
export default class(){}; // 也可以出一个类  

2、import 导入模块

// 某个模块的导出 moudule.js  
export const a = 1;  
  
// 模块导入  
// 1. 这里的a得和被加载的模块输出的接口名对应  
import { a } from './module'  
  
// 2. 使用 as 换名  
import { a as myA } from './module'  
  
// 3. 若是只想要运行被加载的模块可以这样写,但是即使加载2次也只是运行一次  
import './module'  
  
// 4. 整体加载  
import * as module from './module'  
// 第四种写法会获取到module中所有导出的东西,并且赋值到module这个变量下,这样我们就可以用module.a这种方式来引用a了
// Cesium的源码方式加载
  
// 5. default接口和具名接口  
import module, { a } from './module'  

3、exportfrom

我有三个模块a、b、cc模块现在想要引入a模块,但是它不直接引用a,而是通过b模块来引用,b模块可以写成如下:

export { someVariable } from './a';

需要注意:

这样的方式不会将数据添加到该聚合模块的作用域, 也就是说, 你无法在该模块(也就是b)中使用someVariable

4、 ES6 Modules 规范特点

  • 输出使用export
  • 输入使用import
  • 可以使用export...from...这种写法来达到一个"中转"的效果
  • 输入的模块变量是不可重新赋值的,它只是个可读引用,不过却可以改写属性
  • export命令和import命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。
  • import命令具有提升效果,会提升到整个模块的头部,首先执行。

5、Bable下的ES6模块转换

使用一些ES6Babel时,你会发现当使用export/import的时候,Babel也会把它转换为exports/require的形式。

因为这种转换关系,才能让我们把exportsimport结合起来用

// 输出模块 m1.js  
exports.count = 0;  
// index.js中引入  
import {count} from './m1.js'  
console.log(count)  

CommonJSES6 Modules规范的区别

  • CommonJS模块是运行时加载,ES6 Modules是编译时输出接口
  • CommonJS输出是值的浅拷贝ES6 Modules输出的是值的引用,被输出模块的内部的改变会影响引用的改变
  • CommonJs导入的模块路径可以是一个表达式,因为它使用的是require()方法;而ES6 Modules只能是字符串
  • CommonJSthis指向当前模块,ES6 Modules this指向undefined
  • ES6 Modules中没有这些顶层变量:argumentsrequiremoduleexports__filename__dirname