模块化的理解
什么是模块化
- 将一个复杂的程序,按一定的规则(规范)封装成几个块(文件),并进行组合在一起
- 块的内部数据与实现是私有的,只是向外暴露一些接口(方法)与外部其它模块通信
模块化有什么好处?(为什么要使用模块化)
-
避免命名冲突(减少命名空间污染)
-
更好的分离,按需加载
-
更高复用性,高可维护性
-
解决引入多个
<script>后容易出现的问题 -
请求过多:依赖多个模块,那样就会发送多个请求,导致请求过多
- 依赖模糊:不知道依赖之间的具体依赖关系是什么,容易导致加载先后顺序出错。
- 难以维护:以上两种原因必然导致难以维护
模块化发展史
早期 JavaScript 开发很容易存在全局污染和依赖管理混乱问题,这些问题在多人开发前端应用的情况下变得更加棘手。举个🌰:
<body>
<script src="./index.js"></script>
<script src="./home.js"></script>
<script src="./list.js"></script>
</body>
没有模块化,那么 script 内部的变量是可以相互污染的。比如上述代码中 ./index.js 文件和 ./list.js 文件为小 A 开发的,./home.js 为小 B 开发的。index.js中定义了变量name是一个string,home.js中定义的name变量是一个function,name变量这时候就被互相污染了
在规范出现前的解决方案
1. 全局function模式 : 将不同的功能封装成不同的全局函数
function m1(){
//...
}
function m2(){
//...
}
问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
2. namespace模式 : 简单对象封装
作用: 减少了全局变量,解决命名冲突
问题: 数据不安全(暴露所有模块成员,外部可以直接修改模块内部的数据)
let myModule = {
data: 'www.baidu.com',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data
3. IIFE模式:匿名函数自调用(闭包)
作用: 数据是私有的, 外部只能通过暴露的方法操作
编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
问题: 如果当前这个模块依赖另一个模块怎么办?
// module.js文件
(function(window) {
let data = 'www.baidu.com'
//操作数据的函数
function foo() {
console.log(`foo() ${data}`)
}
function bar() {
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar }
})(window)
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
myModule.bar()
console.log(myModule.data) //undefined 不能访问模块内部数据
myModule.data = 'xxxx' //无法修改的模块内部的data
myModule.foo() //没有改变
</script>
4. **IIFE模式增强 : 引入依赖 -----**这也是现代模块实现的基石
// module.js文件
(function(window, $) {
let data = 'www.baidu.com'
//操作数据的函数
function foo() {
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar }
})(window, jQuery)
// index.html文件
<!-- 引入的js必须有一定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
上面的例子需要在myModule.js中使用jQuery,所以必须先引入jQuery库,就把这个库当作参数传入,。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
模块化规范
在ES6出来之前:
- CommonJs--就是require这种,node里面使用的,是node的模块规范。
- AMD--是Require.js在推广的过程中对模块定义的规范化产出。
- CMD--是淘宝Sea.js在推广的过程中对模块定义的规范化产出
ES6:
- ES6Module--就是import、export这类的,我们对这个应该很熟。
CommonJS
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。( Node 是 CommonJS在服务器端一个具有代表性的实现。 )
特点
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。(也就是说,输入的是被输出的值的拷贝。 )
- 模块加载的顺序,按照其在代码中出现的顺序。
具体使用
暴露模块:
module.exports = value 或 exports.xxx = value
引入模块:
require(module)
如果是第三方模块,module为模块名;如果是自定义模块,module为模块文件路径
// 举个🌰:example.js
const x = 5;
const add = function (a) {
return a + x;
};
// 暴露变量
module.exports.x = x;
module.exports.add = add;
// 引入变量
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.add(1)); // 6
CommonJS暴露与引入的是什么?
暴露: CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
引入(加载): require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
CommonJS加载原理
CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
{
id: 'xxx',
exports: { ... },
loaded: true,
...
}
上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。
也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
补充一个容易纠结的点
// a.js
let counter = 0;
module.exports = {
counter,
increment() {
counter++;
},
getCounter() {
return counter;
}
};
// b.js
const moduleA = require('./a');
console.dir(moduleA.getCounter());
moduleA.increment();
console.dir(moduleA.getCounter());
const moduleA2 = require('./a');
console.dir(moduleA2.getCounter());
打印的是0,1,1而不是0,1,0
虽然 b.js 中导入了 a.js 两次,但由于 CommonJS 模块系统会对已加载的模块进行缓存,所以第二次导入实际上是使用了之前已经加载过的模块。这意味着同一个模块的多次导入将共享相同的模块对象和状态。
AMD
概念与特点
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。
但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来的早。
优点:AMD模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。
基本用法
AMD是"Asynchronous Module Definition"的缩写,即“异步模块定义”。
AMD规范很简单,只有一个API,即define函数:
define([module-name?], [array-of-dependencies?], [module-factory-or-object])
其中:
- module-name: 模块标识,可以省略。
- array-of-dependencies: 所依赖的模块,可以省略。
- module-factory-or-object: 模块的实现,或者一个JavaScript对象。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
- 第一个参数[module],是一个数组,里面的成员就是要加载的模块;
- 第二个参数callback,则是加载成功之后的回调函数。
//定义没有依赖的模块
define(function(){
return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return 模块
})
// 引入使用模块
require(['module1', 'module2'], function(m1, m2){
使用m1/m2
})
CMD
CMD(Common Module Definition)是国内大牛玉伯在开发SeaJS的时候提出来的,属于CommonJS的一种规范规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。
CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
基本用法
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
// 引入使用模块
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
UMD (Universal Module Definition)
UMD 是 JavaScript 模块的通用模块定义模式。这些模块能够在任何地方工作,无论是在客户端、服务器还是其他地方。
// 通常用来加载不同的规范
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS之类的
module.exports = factory(require('jquery'), require('underscore'));
} else {
// 浏览器全局变量(root 即 window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// 方法
function a(){}; // 私有方法,因为它没被返回 (见下面)
function b(){}; // 公共方法,因为被返回了
function c(){}; // 公共方法,因为被返回了
// 暴露公共方法
return {
b: b,
c: c
}
}));
ES Module(ES6 模块化)
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
特点
1、静态语法: ES6 module 的引入和导出是静态的,import 会自动提升到代码的顶层 ,import , export 不能放在块级作用域或条件语句中。
2、执行特性: ES6 模块提前加载并执行模块文件,ES6 模块在预处理阶段分析模块依赖,在执行阶段执行模块,两个阶段都采用深度优先遍历,执行顺序是子 -> 父。
3、导出绑定: 不能修改import导入的属性,直接修改会报错
注:如果导出的是对象,是可以成功改写他的属性的,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。
基本用法
import、export
import会自动提升到代码的顶层,并且import、export 只能出现在代码的顶层
export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。
为什么?
引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。
所以,import和export命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。
这样设计的不足?
这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。所以import命令无法取代 Node 的require方法,因为require是运行时加载模块,import命令无法取代require的动态加载功能。
const path = './' + fileName;
const myModual = require(path);
上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import命令做不到这一点。
所以 ===> ES2020提案 引入import()函数,支持动态加载模块。
导入、导出 示例
// 导出info.js export && export default
const name = "张三";
export const age = "18";
export default name;
// 导入
import name, { age } from "./info";
console.log(name, "name");
console.log(age, "age");
// 执行 module 不导出值 多次调用 module 只运行一次。
import 'module'
// 用星号( * )指定一个对象,所有输出值都加载在这个对象上面
import * as circle from './circle';
// 导出,这样导出的模块 import时可以指定任意的名字
export default {xxx}
export 与 import 的复合写法
注:写成一行以后,foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。
// 先输入后输出同一个模块
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
模块的接口改名和整体输出
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
默认接口
export { default } from 'foo';
// 具名接口改为默认接口
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
// 默认接口改具名接口
export { default as es6 } from './someModule';
import * as someIdentifier from "someModule";对应的符合写法:
export * as ns from "mod";
// 等同于
import * as ns from "mod";
export {ns};
import() :
import()与import的参数一致,并且可以动态使用,加载模块。返回一个 Promise 对象(可以用在async函数中), 返回的 Promise 的 then 成功回调中,可以获取模块的加载成功信息。
import()应用
- 按需加载
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling *
})
});
- 条件加载
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
- 动态的模块路径
import(f())
.then(...);
注:import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
作用
- webpack借助 ES Module 的静态导入导出的优势,实现了 tree shaking。
- ES Module 还可以 import() 懒加载方式实现代码分割。
- 引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处,ES6 模块还有以下好处。
- 不再需要
UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。 - 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者
navigator对象的属性。 - 不再需要对象作为命名空间(比如
Math对象),未来这些功能可以通过模块提供。
浏览器 & Node.js 加载ES6模块
浏览器
默认
浏览器对于带有 type="module" 的 <script> ,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。并且如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。
<script type="module" src="./foo.js"></script>
async
<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。
<script type="module" src="./foo.js" async></script>
一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
一些注意点
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict。 - 模块之中,可以使用
import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。 - 模块之中,顶层的
this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次。
利用顶层的this === undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。示例:
import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
Nodejs
Node.js 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
在node中
.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。type为module时可以加载ES6模块
注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import。
举个🌰,package.json文件中:
以下代码会被解析为ES6模块,但如果去掉type,默认解释为CommonJS模块
{
"type": "module",
"main": "./src/index.js"
}
循环加载
CommonJS 中的循环加载
CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
// 举个🌰,a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
如果先执行a.js,先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。b.js 执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,也就是exports.done = false,然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。
// 执行脚本
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// 运行结果
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。
ES6 module 中的循环加载
ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
上面代码中,a.mjs加载b.mjs,b.mjs又加载a.mjs,构成循环加载。执行a.mjs,结果如下。
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
执行过程
首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。
如何解决
让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.mjs`
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
这时再执行a.mjs,就可以得到预期结果,这是因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。
CommonJS与ESModule
因为现代的构建工具(如Webpack)和运行时环境(如Node.js)通常会支持跨模块化系统的互操作性。当使用 import countObj from './countObj.js'; 语法时,构建工具会自动将其转换为适合目标环境的模块化规范(例如babel-loader)。
否则模块化方案不同是不能直接互相引的!!!!!
使用CommonJS导出ESModule
// 导出 name.js
export const age = 18;
export default "这是一个默认导出";
// 引入
let info = require("./name");
console.log(info);
使用ESModule导出CommonJS
// 导出 countObj.js
const x = 5;
const add = function (a) {
return a + x;
};
module.exports.x = x;
module.exports.add = add;
// 引入
import countObj from './countObj.js';
console.log(countObj);
注:通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。混用的话容易导致问题
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
实际应用场景与问题
使用CommonJS规范打包一个SDK(第三方库),常用与node服务端或者小程序场景
// 使用webpack打包 webpack.config.js配置
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/dist/',
filename: 'lodashshsh.js',
libraryTarget: 'lib'
},
devtool: 'source-map'
};
// 引入该sdk
const lodashshsh = require('./dist/lodashshsh.js');
console.log(lodashshsh);
// rollup打包 rollup.config.js配置
export default {
input: './src/main.js',
output: {
file: './dist/lodashshsh.js',
format: 'cjs'
}
};
项目分别打包ES Module文件、CommonJS文件、UMD文件(经典例如ant-design)
简单写下配置代码,在使用babel的情况下,仅供参考,实际问题实际解决
// CommonJS
{
"presets": [
["@babel/env", {
"loose": true,
"modules": "cjs"
}], "@babel/preset-react"
],
"plugins": [
["@babel/plugin-transform-runtime", {
"useESModules": false
}],
]
}
// ES Modules
{
"presets": [
["@babel/env", {
"loose": true,
"modules": false
}], "@babel/preset-react"
],
"plugins": [
["@babel/plugin-transform-runtime", {
"useESModules": true
}]
]
}
// UMD
{
"presets": [
["@babel/preset-env", {
"modules": false,
}], "@babel/preset-react"
],
"plugins": [
["@babel/plugin-transform-runtime", {
"useESModules": false
}]
]
}
ant的除了 es modules,还打包生成了 commonjs 也就是它的 lib 文件夹,还有最落后的 umd 也就是 dist 文件夹,也就是浏览器可以直接引用使用的
注:这里面有个容易纠结的点,例如日常开发中的Vue、React单页面应用(网页),它的打包后产物,是能够在浏览器上直接打开(打不开一般都是路径的问题)或者服务器上直接部署的静态资源。对于这个资源来说,因为不作为模块被引用,虽然它是UMD规范,也可以不用纠结它的规范(因为没意义)。
而ant-design中除了 es modules,还打包生成了 commonjs 也就是它的 lib 文件夹,还有 umd 也就是 dist 文件夹,也就是浏览器可以直接引用使用的。
常见问题
ES Module和CommonJS的差异
CommonJS:
CommonJS 模块是运行时加载
CommonJS 模块输出的是一个值的拷贝
CommonJs 是单个值导出,本质上导出的就是 exports 属性。
CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题。
CommonJS 模块同步加载并执行模块文件。
ESModule:
ES6 Module 静态的,不能放在块级作用域内,代码发生在编译时。
ES6 Module 输出的是值的动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。可以通过导出方法修改,可以直接访问修改结果。
ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出。
ES6 模块提前加载并执行模块文件,
ES6 Module 导入模块在严格模式下。
ES6 Module 的特性可以很容易实现 Tree Shaking 和 Code Splitting。