JavaScript 模块化规范的使用说明
kecho

本文介绍了常用的 JavaScript 模块化规范的使用说明,包含 ESMA6、CommonJS 等其它规范。

视频链接

ES6 模块规范

ES6 模块规范主要用于客户端,也可用于服务端。

使用方式

在 node.js 服务端有两种使用方式:

  • .js 文件改为 .mjs 文件。
  • 或者在 package.json 中添加定义 'type': 'module'

在浏览器端使用:

  • 在 html 文件中使用 <script type="module" src="index.js"></script> 引入。

直接在本地使用浏览器打开 html 文件浏览器会报错,请启动 web 服务器后通过 IP:端口 访问网页。

导出数据

导出有三种方式:

  • 分别导出
1
2
3
4
5
6
7
8
9
// student.js
export const name = 'zhangsan';
export const age = '18';
export function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}
  • 统一导出
1
2
3
4
5
6
7
8
9
10
11
// student.js
const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}

export { name, age, getTel };
  • 默认导出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// student.js
const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}

// export default {
// name: name,
// age: age,
// getTel: getTel
// };

// 可省略书写
export default { name, age, getTel };

三种导出可以混用:

1
2
3
4
5
6
7
8
9
10
11
12
// student.js
export const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}

export { age };
export default { getTel };

会得到:

1
2
3
4
5
[Module: null prototype] {
age: '18',
default: { getTel: [Function: getTel] },
name: 'zhangsan'
}

导入数据

对于 ES6 模块化来说,使用何种导入方式要根据导出方式决定。

  • 全部导入(通用)
1
2
// index.js
import * as student from './student.js';
  • 命名导入(对应:分别导出、统一导出)
1
2
// index.js
import { name as newName, age, getTel } from './student.js';
  • 默认导入(对应:默认导出)
1
2
3
// index.js
import student from './student.js';
// student 变量名可以自己取

默认导入和命名导入可以混用(当然不能和全部导入混用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// student.js
export const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}

export { age };
export default { getTel };


// index.js
import newFun, { name, age } frome './student.js';
  • 直接导入不接收任何数据
1
2
3
4
5
6
7
// log.js
console.log('hello world!');

// index.js
import './log.js';

// 导入时 console.log() 会执行
  • 动态导入(通用)

导入还可以根据事件来进行动态导入,下面的例子将演示当点击按钮时才导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// student.js
const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}

export { name, age, getTel };

// index.js
const btn = document.getElementById('btn');
btn.onclick = async() => {
const result = await import('./student.js');
console.log(result);
}

CommonJS 规范

CommonJS 规范主要用于服务端,如果想要在浏览器端使用,可以通过 browserify 将原本的 js 文件进行翻译,再将翻译后的文件在 html 中引用。

导出数据

在 CommonJS 规范中有两种导出方式,导出的是一个普通对象,默认为 {}

通过 exports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// student.js
const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}

exports.name = name;
exports.age = age;
exports.getTel = getTel;

// 当然也可以使用 module.exports.name = name 只是书写更麻烦;

通过 module.exports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// student.js
const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}

// module.exports = {
// name: name,
// age: age,
// getTel: getTel
// };

// 可省略书写
module.exports = { name, age, getTel };

导入数据

导入时会执行被导入文件中的代码:

1
2
3
4
5
6
7
8
9
10
// student.js
console.log('hello');

// index.js
const student = require('./student.js');
console.log(student);

// 会输出:
// hello
// {}

导入时也能通过解构赋值:

1
2
// index.js
const { name: newName, age, getTel } = require('./student.js'); // 解构导入并重命名

注意

注意点如下:

  1. 每个模块内部的:thisexportsmodule.exports 在初始时都指向同一个空对象。console.log(this===exports && exports === module.exports); // true
  2. 无论如何修改导出对象,最终导出的都是 module.exports 的值。
  3. exports 是对 module.exports 的初始引用,仅为了方便给导出对象添加属性,所以不能使用 exports = value 的形式导出数据,可以使用 exports.value = valuemodule.exports = value 导出数据。

思考以下代码在导入时获取到的数据是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// student.js
const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}

exports = { a: 1 };
exports.b = 2;
module.exports.c = 3;
module.exports = { d: 4 };
1
2
3
4
// index.js
const student = require('./student.js');
console.log(student);
// 输出:{ d: 4 }

分析:

  1. exports -> {} <- module.exports
  2. exports -> { a: 1 }module.exports -> {}
  3. exports -> { a: 1, b: 2 }module.exports -> {}
  4. exports -> { a: 1, b: 2 }module.exports -> { c: 3 }
  5. exports -> { a: 1, b: 2 };没有东西指向 { c: 3 } module.exports -> { d: 4 }

模块化的好处

模块化能够有效解决:

  • 全局污染问题
  • 数据安全问题
  • 依赖混乱问题

1、全局污染、数据安全问题

在使用模块化以前:

1
2
3
4
5
6
7
8
9
// student.js
const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript" src="student.js"></script>
<title>Document</title>
</head>
<body>

</body>
</html>

此时在浏览器控制台是能够获取到 nameage 等变量的,并且如果引入的多个文件有相同的变量名会产生冲突。

使用模块化以后:

1
2
3
4
5
6
7
8
9
10
11
12
13
// student.js
const name = 'zhangsan';
const age = '18';
function getTel (){
return '13333333333';
}
function getSubject (){
return ['Java', 'C++', '高等数学'];
}
export default { name, age, getTel };

// index.js
import student from './student.js';
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module" src="index.js"></script>
<title>Document</title>
</head>
<body>

</body>
</html>

此时在浏览器控制台是无法获取到 student.name 变量的,而且在导入模块时能够对冲突的变量重命名,解决了数据安全、全局污染问题。

2、依赖混乱问题

在使用模块化以前,直接在 html 文件中引入 js 的顺序至关重要,尤其是需要引入多个 js 文件时,特别需要关注他们的依赖关系来确定引入顺序,十分麻烦。使用模块化后,可以在各个 js 文件中显式导入需要使用到的模块,而不用关心 html 文件中引入 js 的顺序。

数据引入问题

思考同一代码在 CommonJS 规范和 ES6 规范的执行结果:

  • CommonJS 规范
1
2
3
4
5
6
7
8
9
10
11
12
13
// data.js
let sum = 1;
function increment() {
sum += 1;
}
module.exports = { sum, increment };

// index.js
const { sum, increment } = require('./data.js');
console.log(sum);
increment();
increment();
console.log(sum);

输出:

1
data: 2
data: 3
1

  • ES6 规范
1
2
3
4
5
6
7
8
9
10
11
12
13
// data.js
let sum = 1;
function increment() {
sum += 1;
}
export { sum, increment };

// index.js
import { sum, increment } from './data.js';
console.log(sum);
increment();
increment();
console.log(sum);

输出:

1
data: 2
data: 3
3

对比两者发现,ES6 规范中执行完函数后的 sum 的值不一样了,是因为 index.jsdata.js 中的 sum 是同一片内存空间(在 CommonJS 规范中导入是将值进行了复制),为了避免这种情况应该在 data.js 中声明常量而不是变量,即改为 const sum = 1

其它规范

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

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