js模块规范

# 前言

# JS之AMD、CMD、CommonJS、ES6、UMD的使用

# 图示对比

image-20200820091303580

# 总括

  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重;
  • CommonJS规范主要用于服务端编程**,加载模块是同步的**,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案;
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

# AMD 和 CMD 的区别

  • 对于依赖的模块,**AMD 是提前执行,CMD 是延迟执行。**不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
  • AMD 推崇依赖前置, CMD 推崇依赖就近;
模块化 代表应用 特点
AMD require.js 1、AMD的api默认一个当多个用 2、依赖前置,异步执行
CMD sea.js 1、CMD的api严格区分,推崇职责单一 2、依赖就近,按需加载,异步执行

# Commonjs 和 ES6 Module的区别

取自阿里巴巴淘系技术前端团队的回答:

  • Commonjs是拷贝输出,ES6模块化是引用输出
  • Commonjs是运行时加载,ES6模块化是编译时输出接口
  • Commonjs是动态语法可写在函数体中,ES6模块化静态语法只能写在顶层;
  • Commonjs是单个值导出,ES6模块化可以多个值导出
  • Commonjs的this是当前模块化,ES6模块化的this是undefined

# 为什么Commonjs不适用于浏览器

var math = require('math');
math.add(2, 3);
1
2

第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

# AMD(requirejs)

AMD规范可参考地址:https://github.com/amdjs/amdjs-api/wiki/AMD

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。

AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。

根据AMD规范,我们可以使用define定义模块,使用require调用模块,语法:

 define(id?, dependencies?, factory);
1
  • id: 定义中模块的名字;可选;如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字;
  • dependencies:依赖的模块;
  • factory:工厂方法,返回定义模块的输出值。

总结一段话:声明模块的时候指定所有的依赖dependencies,并且还要当做形参传到factory中,对于依赖的模块提前执行,依赖前置

例子1:

define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
  exports.verb = function() {
    return beta.verb();
    //Or:
    return require("beta").verb();
  }
});
1
2
3
4
5
6
7

例子2:

 define(["alpha"], function (alpha) {
       return {
         verb: function(){
           return alpha.verb() + 2;
         }
       };
   });
1
2
3
4
5
6
7

例子3:

define({
  add: function(x, y){
    return x + y;
  }
});
1
2
3
4
5

例子4:

define(function (require, exports, module) {
  var a = require('a'),
      b = require('b');
  exports.action = function () {};
});
1
2
3
4
5

使用require函数加载模块:

require([dependencies],function(){});
1
  • 第一个参数是一个数组,表示所依赖的模块
  • 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用.加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块

require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题

require(['alpha'],function(alpha){
    alpha.verb ();
})
1
2
3

# CMD(seajs)

CMD规范参考地址:https://github.com/seajs/seajs/issues/242 CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。**CMD规范整合了AMD和CommonJS规范的特点。**在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

# 语法

# define Function

define 是一个全局函数,用来定义模块。

define define(factory)

define 接受 factory 参数,factory 可以是一个函数,也可以是一个对象或字符串。

factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以如下定义一个 JSON 数据模块:

define({ "foo": "bar" });
1

也可以通过字符串定义模板模块:

define('I am a template. My name is {{name}}.');
1

factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:requireexportsmodule

define(function(require, exports, module) {
  // 模块代码
});
1
2
3

define define(id?, deps?, factory)

define 也可以接受两个以上参数。字符串 id 表示模块标识,数组 deps 是模块依赖。比如:

define('hello', ['jquery'], function(require, exports, module) {
  // 模块代码
});
1
2
3

iddeps 参数可以省略。省略时,可以通过构建工具自动生成。

注意:带 iddeps 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport (opens new window) 规范。

define.cmd Object

一个空对象,可用来判定当前页面是否有 CMD 模块加载器:

if (typeof define === "function" && define.cmd) {
  // 有 Sea.js 等 CMD 模块加载器存在
}
1
2
3

# require Function

requirefactory 函数的第一个参数。

require require(id)

require 是一个方法,接受 模块标识 (opens new window) 作为唯一参数,用来获取其他模块提供的接口。

define(function(require, exports) {
  var a = require('./a');// 获取模块 a 的接口
  a.doSomething();// 调用模块 a 的方法
});
1
2
3
4

require.async require.async(id, callback?)

require.async 方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback 参数可选。

define(function(require, exports, module) {
  require.async('./b', function(b) {// 异步加载一个模块,在加载完成时,执行回调
    b.doSomething();
  });
  require.async(['./c', './d'], function(c, d) {// 异步加载多个模块,在加载完成时,执行回调
    c.doSomething();
    d.doSomething();
  });
});
1
2
3
4
5
6
7
8
9

注意:require 是同步往下执行,require.async 则是异步回调执行。require.async 一般用来加载可延迟异步加载的模块。

# exports Object

exports 是一个对象,用来向外提供模块接口。

define(function(require, exports) {
  // 对外提供 foo 属性
  exports.foo = 'bar';
  // 对外提供 doSomething 方法
  exports.doSomething = function() {};
});
1
2
3
4
5
6

除了给 exports 对象增加成员,还可以使用 return 直接向外提供接口。

define(function(require) {
  // 通过 return 直接提供接口
  return {
    foo: 'bar',
    doSomething: function() {}
  };
});
1
2
3
4
5
6
7

# module Object

module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

module.id String

模块的唯一标识。

define('id', [], function(require, exports, module) {
  // 模块代码
});
1
2
3

上面代码中,define 的第一个参数就是模块标识。

module.exports Object

当前模块对外提供的接口。

传给 factory 构造方法的 exports 参数是 module.exports 对象的一个引用。只通过 exports 参数来提供接口,有时无法满足开发者的所有需求。 比如当模块的接口是某个类的实例时,需要通过 module.exports 来实现:

define(function(require, exports, module) {
  // exports 是 module.exports 的一个引用
  console.log(module.exports === exports); // true
  // 重新给 module.exports 赋值
  module.exports = new SomeClass();
  // exports 不再等于 module.exports
  console.log(module.exports === exports); // false
});
1
2
3
4
5
6
7
8

# AMD与CMD的主要区别

  • 对于依赖的模块,**AMD 是提前执行,CMD 是延迟执行。**不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
  • AMD 推崇依赖前置, CMD 推崇依赖就近;
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好  
    a.doSomething()
    b.doSomething()
})
// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    var b = require('./b')
    b.doSomething()
})
1
2
3
4
5
6
7
8
9
10
11
12

AMD与CMD的其它区别可参考地址:https://www.zhihu.com/question/20351507

# CommonJS 【要点】

CommonJS是服务器端模块的规范,Node.js采用了这个规范.Node.JS首先采用了js模块化的概念.

CommonJS定义的模块分为:模块引用(require)/ 模块定义(exports)/模块标识(module)

  • 所有代码都运行在模块作用域,不会污染全局作用域。

  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

  • 模块加载的顺序,按照其在代码中出现的顺序。

//common.js
module.exports = function(a, b) {
  return a-b
}
let minus = require('./common.js')  //文件相对路径
console.log(minus(5,4)) // 结果: 1
1
2
3
4
5
6

# module对象

Node中提供了一个Module构建函数,所有模块都是Module的实例

img

# Node 9下import/export使用简单须知

  • Node 环境必须在 9.0以上
  • 不加loader时候,使用import/export的文件后缀名必须为*.mjs(下面会讲利用Loader Hooks兼容*.js后缀文件)
  • 启动必须加上flag --experimental-modules
  • 文件的importexport必须严格按照ECMAScript Modules语法
  • ECMAScript Modulesrequire()的cache机制不一样

# exports 与 module.exports

根据AMD规范:

  • 每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
  • 每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
    • exports是module.exports一种简写形式,不能直接给exports赋值
    • 当直接给module.exports赋值时,exports会失效

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

const exports = module.exports;
1

于是我们可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样。注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。

// a.js
exports = function a() {};
// b.js
const a = require('./a.js') // a 是一个空对象
1
2
3
4

# 示例

//加载示例:commonjs与e6模块加载 test.js
var x = 5;
var add = function(a, b){return a + b;}
module.exports = {x, add}
//module.exports = {
//  msg: 'module1',
//  foo() { console.log(this.msg) }
//}

var test = require('./test.js');
console.log(test.x); // 5
console.log(test.add(1, 2)); // 3

//使用import语法加载commonjs模块
import { x, add } from './test'
console.log(x); // 5
console.log(add(1, 2)); // 3

//导出的两种不同方式
exports.bcompare = (str, hash) => {
  return bcrypt.compareSync(str, hash)
}
module.exports = {
    bcompare
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# ES6 module【要点】

es6 module内部自动采用严格模式

import和export只能出现在模块的最外层(顶层)结构中,否则报错

由于es6模块是静态加载的,因此import和export不能出现在判断等动态语句中

采用import获取的是模块接口的引用,当模块内部发生改变是,import出的接口也会对应改变

【与commonjs规范不同,commonjs中获得的是接口运行结果的缓存】

# 导出

ES6两种导出方式:

  1. 命名导出
  2. 默认导出

# 命名导出

// 写法1
export const name = 'calculator';
export const add = function (a,b) {
  return a + b;
}
// 写法2
const name = 'calculator';
const add = function (a,b) {
  return a + b;
}
export {name, add};
1
2
3
4
5
6
7
8
9
10
11

在使用命名导出时,可以通过as关键字对变量重命名。如:

const name = 'calculator';
const add = function (a,b) {
  return a + b;
}
export {name, add as getSum};  // 在导入时即为name 和 getSum
1
2
3
4
5

# 默认导出

export default {
  name: 'calculator';, 
  add: function (a,b) {
    return a + b;
  }
}; 
1
2
3
4
5
6

# 导入

针对命名导出的模块,导入方式如下:

// calculator.js
const name = 'calculator';
const add = function (a,b) {
  return a + b;
}
export {name, add};

// 一般导入方式
import {name, add} from './calculator.js'
add(2,3);

// 通过as关键字对导入的变量重命名
import {name, add as calculateSum} from './calculator.js'
calculateSum(2,3);

// 使用import * as <myModule>可以把所有导入的变量作为属性值添加到<myModule>对象中,从而减少了对当前作用域的影响
import * as calculateObj from './calculator.js'
calculateObj.add(2,3);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

对于默认导出来说,import后面直接跟变量名,并且这个名字可以自由指定,它指代了calculator.js中默认导出的值。从原理上可以这样理解:

import {default as myCalculator} from './calculator.js'
1

还有两种导入方式混合起来使用的例子,如下:

import React, {Component} from 'react';
1

注:这里的React必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误

# 复合写法

在工程中,有时候需要把一个模块导入后立即导出,比如专门用来集合所有页面或组件的入口文件。此时可以采用复合形式的写法:

export {name, add } from './calculator.js'
1

复合写法只支持通过命名导出的方式暴露出来的变量,默认导出则没有对应的复合形式,只能将导入和导出拆分开

import calculator from './calculator.js'
export default calculator;
1
2

跨文件引入,比如组件库导出;

export * from 'react-router-dom' // 和下面两个一样的意思;
// import * as router from 'react-router-dom';
// export router
1
2
3

# exoprt和export default

将exoprt和export default放在一起,因为它们关联性很大

简单说:export是导出,export default是默认导出

一个模块可以有多个export,但是只能有一个export default,export default可以和多个export共存

export default 为默认导出,导出的是用{}包裹的一个对象,以键值对的形式存在

导出的方式不同,导入的方式也就不同, export default解构以后就是export

所以建议同一个项目下使用同一的导入导出方式,方便开发

# ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

1:CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

2:CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

**CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值;**这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

// 另外,export语句输出的接口,与其对应的值是动态绑定关系; 即通过该接口,可以取到模块内部实时的值
export var a = 1;
setTimeout(() => a = 2, 1000);  // 1s之后a变为2
1
2
3

# 示例

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {return a + b;};
export { basicNum, add };// 导出多个
//注意:export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系
export var m = 1; // 等价于 var m = 1;export { m }// 导出一个
var a = 1;export a;// 报错,因为通过变量a,直接输出的是1,1只是一个值,不是接口
export const student = {// 导出对象
  name: 'Megan',
  age: 18
}
export function add(a, b) {// 导出函数
  return a + b;
}

// 使用as关键字重命名: 导出接口别名,在导出时重命名,导入时也可以;
export { person as boy }
import { year as y } from './lib';// 将输入的变量重命名
import * as lib from './lib'; //整体加载模块 全局总重命名

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

//一个文件即模块中只能存在一个export default语句,导出一个当前模块的默认对外接口
//使用import命令的时候,为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
export default var i = 0;
// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# UMD【要点】

UMD并不能说是一种模块标准,不如说它是一组模块形式的集合更准确。UMD的全称是Universal Module Definition,也就是通用模块标准。它的目标是使一个模块能运行在各种环境下,不论是CommonJS、AMD,还是非模块化的环境(当时ES6 Module还未被提出)

(function(root, factory) {
  if (typeof define === 'function' && define.amd) {// AMD
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {// Node, CommonJS之类的
    module.exports = factory(require('jquery'));
  } else {// 浏览器全局变量(root 即 window)
    root.returnExports = factory(root.jQuery);
  }
}(this, function($) {
  function myFunc() {};// 方法
  return myFunc;// 暴露公共方法
}));


;(function(){
    function MyModule() {
        // ...
    }
    var moduleName = MyModule;
    if (typeof module !== 'undefined' && typeof exports === 'object') {
        module.exports = moduleName;
    } else if (typeof define === 'function' && (define.amd || define.cmd)) {
        define(function() { return moduleName; });
    } else {
        this.moduleName = moduleName;
    }
}).call(function() {
    return this || (typeof window !== 'undefined' ? window : global);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 引入UMD

# babel中设置

babel中设置modules 该参数的含义是:启用将ES6模块语法转换为另一种模块类型。将该设置为false就不会转换模块。默认为 'commonjs'. 该值可以有如下: 'amd' | 'umd' | 'systemjs' | 'commonjs' | false

我们在项目中一般会看到如下配置,设置modules: false, 如下代码配置:

"presets": [
   'env',
   {
     'modules': false
   }
]
1
2
3
4
5
6

这样做的目的是:

  • 以前我们需要使用babel来将ES6的模块语法转换为AMD, CommonJS,UMD之类的模块化标准语法,
  • 但是现在webpack都帮我做了这件事了,所以我们不需要babel来做
  • 因此需要在babel配置项中设置modules为false,因为它默认值是commonjs, 否则的话,会产生冲突。

# webpack设置

umd方式,他支持浏览器端和node端(require方式),webpack同样也是支持该打包方式的。 首先修改webpack打包脚本,在webpack.config.js中,增加output下的参数:

{
    libraryExport: "default",
    library: "Test",
    libraryTarget: "umd"
}
1
2
3
4
5

告诉webpack导出的方式和导出库名,详细见官方文档output (opens new window)library (opens new window)

外部 library 可能是以下任何一种形式:

  • root:可以通过一个全局变量访问 library(例如,通过 script 标签)。
  • commonjs:可以将 library 作为一个 CommonJS 模块访问。
  • commonjs2:和上面的类似,但导出的是 module.exports.default.
  • amd:类似于 commonjs,但使用 AMD 模块系统。
externals : {
  lodash : {
    commonjs: "lodash",
    amd: "lodash",
    root: "_" // 指向全局变量
  }
}
1
2
3
4
5
6
7

# rollup的设置

import babel from "rollup-plugin-babel";
import serve from "rollup-plugin-serve";

export default {
  input: "./src/index.js",
  output: {
    format: "umd", // amd commonjs规范  默认将打包后的结果挂载到window上
    file: "dist/vue.js", // 打包出的vue.js 文件  new Vue
    name: "Vue", // 打包后的全局变量的名字
    sourcemap: true,
  },
  plugins: [
    // 解析es6 -》 es5
    babel({
      exclude: "node_modules/**", // 排除文件的操作 glob
    }),
    // 开启本地服务
    serve({
      open: true,
      openPage: "/public/index.html",
      port: 3000,
      contentBase: "",
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 相关链接

上次更新: 2022/04/15, 05:41:26
×