CommonJS,AMD,CMD和ES6的对比(import、export、exports、require、define)

ES6

在ES6中,我们可以使用 import 关键字引入模块,通过 exprot 关键字导出模块,功能较之于前几个方案更为强大,也是我们所推崇的。

变量/对象/方法/类-导出/导入

导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const A = 42;

export const api = {};

export function myfunc() {};

export class MyClass {};

var myapi = {}; export {myapi};

var m1 = 1; var m2 = 2; export {m1, m2};

// 输出指定变量,并重命名,则外部引入时得到的是as后的名称。
var n = 1; export {n as m};

导入

导入变量/方法的时候变量和方法名必须和导出时一致

1
2
3
import { A,api,myfunc,myapi,MyClass } from './A'
// 导入设置别名
import { A as AA, myfunc as afunc } from './A'

import导入的变量都是只读的,加载后不能修改

1
2
import { m } from 'my_module';
m = 1; // SyntaxError: "m" is read-only

默认-导出/导入

注意default一个文件只能有一个

导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default "http://www.psvmc.cn";

export default {}; //Vue组件化常用方式

let myapi = {}; export { myapi as default };
// 这种和上面的方式一样
export default myapi;

// 导出方法的时候写不写方法名都可以
export default function() {};
export default function MyFunc() {};

// 导出类的时候写不写类名都可以
export default class { }
export default class MyClass{ }
class Person{}; export default Person;

导入
导入默认时变量和方法名可自定义

1
2
3
import A from './A'
// 导入设置别名
import { default as myApi } from './A';

全部导入

1
2
let m1 = 1; let m2 = 2; export {m1, m2};
export default 3;

导入

1
2
3
4
import * as mynum from '@/assets/js/Test'
console.info(mynum.m1);
console.info(mynum.m2);
console.info(mynum.default);

注意

导出的默认不能直接通过console.info(mynum);打印。

直接打印的化结果如下:

image-20220726163942294

全部导出

1
export * from "module"

export * from "module"不包含default导出,但export * as ns from "module"(在ns上)是包含default导出的。

页面中使用

报错

Uncaught SyntaxError: Cannot use import statement outside a module

方式1(不建议 学习的时候可以这样)

HTML中直接用要添加type="module"

1
2
3
4
<script type="module">
import { zj } from "./index.js";
console.info(zj);
</script>

方式2 通过webpack打包

但是由于ES6目前无法在浏览器中执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require

解决方法:在项目中配置webpack即可

Babel转换ES6

ES6 的导出模块写法有

1
2
3
4
5
6
7
export default 123;

export const a = 123;

const b = 3;
const c = 4;
export { b, c };

Babel 会将这些统统转换成 CommonJS 的 exports

1
2
3
4
5
exports.default = 123;
exports.a = 123;
exports.b = 3;
exports.c = 4;
exports.__esModule = true;

Babel 转换 ES6 的模块输出逻辑非常简单,即将所有输出都赋值给 exports,并带上一个标志 __esModule 表明这是个由 es6 转换来的 CommonJS 输出。

调用

1
2
3
4
5
6
const { a } = require("./a.js");
const d = require("./a.js").default;

// 或者
const obj = require("./a.js");
const a = obj.a;

如果我们使用了 ES6 的模块系统,如果借助 babel 的转换,ES6 的模块系统最终还是会转换成 CommonJS 的规范。

所以我们如果是使用 Babel 转换 ES6 模块,混合使用 ES6 的模块和 CommonJS 的规范是没有问题的,因为最终都会转换成 CommonJS 。

推荐一篇文章写的比较详细

https://blog.csdn.net/gwdgwd123/article/details/104626274

CommonJS(NodeJS)

前端浏览器不支持,用于服务器,Nodejs中使用的是这个规范

1
2
3
exports.area = function(r) {
return Math.PI * r * r;
}

CommonJS的核心思想就是通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports 或者 module.exports 来导出需要暴露的接口。

使用

1
const { area } = require("area");

AMD

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

AMD是RequireJS在推广过程中对模块定义的规范化产出。
AMD规范则是非同步加载模块,允许指定回调函数。

AMD标准中,定义了下面两个API:

  • require([module], callback)
  • define(id, [depends], callback)
    即通过define来定义一个模块,然后使用require来加载一个模块。

require还支持CommonJS的模块导出方式。

test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
define(['package/lib',...], function(lib) {
function foo () {
lib.log('hello world');
}

return {
foo: foo
}
});

require(['test'], function(test) {
test.foo()
})

CMD

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

CMD是同步模块定义。

1
2
3
4
5
6
7
8
//所有模块都通过define来定义
define(function(require, exports, module) {
// 通过require引入依赖
var $ = require('jquery');
var Spinning = require('./spinning');
exports.doSomething = ...
module.exports = ...
})

二者的区别是前者是对于依赖的模块提前执行,而后者是延迟执行。 前者推崇依赖前置,而后者推崇依赖就近,即只在需要用到某个模块的时候再require。

AMD和CMD区别

规范

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产s出。
类似的还有 CommonJS Modules/2.0 规范,是 BravoJS 在推广过程中对模块定义的规范化产出。

这些规范的目的都是为了 JavaScript 的模块化开发,特别是在浏览器端的。
目前这些规范的实现都能达成浏览器端模块化开发的目的

区别:

  1. 定位有差异
    RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。
    Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。

  2. 遵循的规范不同
    RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。
    Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
    CMD 推崇依赖就近,AMD 推崇依赖前置
    看代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // CMD
    define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    // 此处略去 100 行
    var b = require('./b') // 依赖可以就近书写
    b.doSomething()
    // ...
    })

    // AMD 默认推荐的是
    define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething()
    // 此处略去 100 行
    b.doSomething()
    //...
    })

    虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。

  3. 推广理念有差异
    RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。
    Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。

  4. 对开发调试的支持有差异
    Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。
    RequireJS 无这方面的明显支持。

  5. 插件机制不同
    RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。
    Sea.js 采取的是通用事件机制,插件类型更丰富。

  6. 执行机制不同
    对于依赖的模块,AMD 是提前执行,CMD 是延迟执行
    不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。
    CMD 推崇 as lazy as possible.

  7. API差异
    AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一
    比如 AMD 里,require 分全局 require 和局部 require,都叫 require。
    CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。
    CMD 里,每个 API 都简单纯粹

require和import

require 和 import 分别是不同模块化规范下引入模块的语句,下文将介绍这两种方式的不同之处。

1.出现的时间、地点不同

年份 出处
require/exports 2009 CommonJS
import/export 2015 ECMAScript2015(ES6)

2.不同端(客户端/服务器)的使用限制

require/exports import/export
Node.js 所有版本 Node 9.0+(启动需加上 flag –experimental-modules) Node 13.2+(直接启动)
Chrome 不支持 61+
Firefox 不支持 60+
Safari 不支持 10.1+
Edge 不支持 16+

CommonJS 模块化方案 require/exports 是为服务器端开发设计的。服务器模块系统同步读取模块文件内容,编译执行后得到模块接口。(Node.js 是 CommonJS 规范的实现)。

在浏览器端,因为其异步加载脚本文件的特性,CommonJS 规范无法正常加载。所以出现了 RequireJS、SeaJS 等(兼容 CommonJS )为浏览器设计的模块化方案。

两种方案各有各的限制,需要注意以下几点:

  • 原生浏览器不支持 require/exports,可使用支持 CommonJS 模块规范的 Browsersify、webpack 等打包工具,它们会将 require/exports 转换成能在浏览器使用的代码。

  • import/export 在浏览器中无法直接使用,我们需要在引入模块的 <script> 元素上添加type="module属性。

  • 即使 Node.js 13.2+ 已经支持 import/export,Node.js官方不建议在正式环境使用。.

    目前可以使用 babel 将 ES6 的模块系统编译成 CommonJS 规范(注意:语法一样,但具体实现还 是require/exports)。

3.require/exports 是运行时动态加载,import/export 是静态编译

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

4.require/exports 输出的是一个值的拷贝,import/export 模块输出的是值的引用

require/exports 输出的是值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

import/export 模块输出的是值的引用。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

若文件引用的模块值改变,require 引入的模块值不会改变,而 import 引入的模块值会改变。

5.用法不一致

(1). require/exports 的用法

1
2
3
const fs = require('fs')
exports.fs = fs
module.exports = fs

exports 是对 module.exports 的引用,相当于

1
exports = module.exports = {};

在不改变 exports 指向的情况下,使用 exports 和 module.exports 没有区别;如果将 exports 指向了其他对象,exports 改变不会改变模块输出值。示例如下:

1
2
3
4
5
6
7
8
9
10
11
//utils.js
let a = 100;

exports.a = 200;
console.log(module.exports) //{a : 200}
exports = {a:300}; //exports 指向其他内存区

//test.js

var a = require('./utils');
console.log(a) // 打印为 {a : 200}

(2). import/export 的写法

1
2
3
4
5
6
7
8
9
10
11
import fs from 'fs'
import {readFile} from 'fs' //从 fs 导入 readFile 模块
import {default as fs} from 'fs' //从 fs 中导入使用 export default 导出的模块
import * as fileSystem from 'fs' //从 fs 导入所有模块,引用对象名为 fileSystem
import {readFile as read} from 'fs' //从 fs 导入 readFile 模块,引用对象名为 read

export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'

建议:

建议明确列出我们要引用的内容。

使用 * 虽然很方便,但是不利于现代的构建工具检测未被使用的函数,影响代码优化。

同时需要注意

  1. 引入 export default 导出的模块不用加 {},引入非 export default 导出的模块需要加 {}。
1
import fileSystem, {readFile} from 'fs'
  1. 一个文件只能导出一个 default 模块。

  2. 在验证代码的时候遇到如下报错

access to script from origin ‘null’ has been blocked by CORS policy

前面提到过,浏览器引入模块的 <script> 元素要添加 type="module 属性,但 module 不支持 FTP 文件协议(file://),只支持 HTTP 协议,所以本地需要使用 http-server 等本地网络服务器打开网页文件。

(3). import/export 不能对引入模块重新赋值/定义

当我尝试给 import 的模块重新赋值时

1
2
import {e1} from './webUtils.js';
e1=234;

浏览器显示

Uncaught TypeError: Assignment to constant variable.

当我重新定义引用的模块

1
2
import {e1} from './webUtils.js';
var e1=1;

浏览器显示

(index):17 Uncaught SyntaxError: Identifier ‘e1’ has already been declared

(4). ES6 模块可以在 import 引用语句前使用模块,CommonJS 则需要先引用后使用

ES6 模块

1
2
3
4
5
//webUtils.js
export var e='export';
console.log(e) //export
import {e} from './webUtils.js';
console.log(e) //export

CommonJS

1
2
3
4
5
//utils.js
exports.e = 'export';
console.log(a)
a = require('./utils');
console.log(a)

程序报错

ReferenceError: a is not defined

(5) import/export 只能在模块顶层使用,不能在函数、判断语句等代码块之中引用;require/exports 可以。

1
2
3
4
5
6
7
import fs from  './webUtils.js';
function a(){
import {e1} from './webUtils.js';
console.log(e1)
}
a();
console.log(fs())

程序报错

Uncaught SyntaxError: Unexpected token ‘{‘

前面提到过 import/export 在代码静态解析阶段就会生成,不会去分析代码块里面的 import/export,所以程序报语法错误,而不是运行时错误。

6.是否采用严格模式

严格模式是采用具有限制性JavaScript变体的一种方式

import/export 导出的模块默认调用严格模式。

1
2
3
4
var fun=()=>{
mistypedVaraible = 17; //报错,mistypedVaraible is not defined
};
export default fun;

require/exports 默认不使用严格模式,可以自定义是否使用严格模式。 例如

1
2
3
exports.fun = ()=>{
mistypedVaraible = 17; //没有调用严格模式,不会报错
};

7.其他模块化方法

动态导入

import(modulePath) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。

我们可以在代码中的任意位置动态地使用它。例如:

1
2
3
4
import('/modules/my-module.js') //动态导入
.then((module) => {
// Do something with the module.
});

建议: 请不要滥用动态导入 import()(只有在必要情况下采用)。静态框架能更好的初始化依赖,而且更有利于静态分析工具和 tree shaking 发挥作用