随着前端项目越来越庞大,JS 代码交叉引用、相互依赖等问题导致前端开发混乱,因此,对代码模块化是前端工程化的刚需。代码模块化,主要有以下两点好处:
- 避免全局变量污染
- 有效的处理依赖关系
ES2015(即:ES6)引入了模块的概念,提供了 export
和 import
2 个主要的命令(API)。相对于社区实现的 JS 模块管理方案,ES6 模块管理更加强大和灵活,最重要的是,这是 ECMAScript 官方推出的 JS 模块管理方案,是每位前端开发者需要掌握的模块化方案。
1、Export 语句
模块是独立的文件,该文件内部的所有的变量外部都无法获取。如果希望获取某个变量,必须通过export输出,如下面的代码所示:
// profile.js
export var firstName = 'Bill';
export var lastName = 'Gates';
export var year = 1954;
或者,用大括号指定要输出的一组变量:
// profile.js
var firstName = 'Bill';
var lastName = 'Gates';
var year = 1954;
export {firstName, lastName, year};
export
除了输出变量,还可以输出函数或者类(class):
export function add(x, y) {
return x + y;
}
还可以批量输出函数(当然,也要包含在大括号里),还可以用 as
对导出的变量进行重命名:
function fn1() { /* ... */ }
function fn2() { /* ... */ }
export {
fn1 as func1,
fn2 as func2,
fn2 as newFunc2
};
export
命令规定的是对外接口,必须与模块内部变量建立一一对应的关系。
// 写法一
export var n = 1;
// 写法二
var n = 1;
export { n };
// 写法三
var n = 1;
export { n as m };
// 报错
export 1;
// 报错
var n = 1;
export n;
上面代码报错的写法,其原因是:没有提供对外的接口,第一种直接输出 1,第二种虽然有变量 n,但还是直接输出 1,导致无法解构。
同样的,function 和 class 的输出,也必须遵守这样的写法。如下所示:
// 报错
function f() { /* ... */ }
export f;
// 正确
export function f() { /* ... */ };
// 正确
function f() { /* ... */ }
export { f };
export
语句输出的接口,都是和其对应的值是动态绑定的关系,即通过该接口取到的都是模块内部实时的值。
export
模块可以位于模块中的任何位置,但是必须是在模块顶层,如果在其他作用域内,则会报错。
function foo() {
// 会报语法错误 SyntaxError
export default 'bar';
}
foo();
2、Import 语句
export
定义了模块的对外接口后,其他 JS 文件就可以通过 import
语句来加载这个模块。
// main.js
import {firstName, lastName, year} from './profile';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
import
命令接受一对大括号,里面指定要从其他模块导入的变量名,必须与被导入模块 profile.js
对外接口的名称相同。
如果想重新给导入的变量一个名字,可以用 as 关键字:
import { lastName as surname } from './profile';
import
后的 from
可以指定需要导入模块的路径名,可以是绝对路径,也可以是相对路径, .js
路径可以省略,如果只有模块名,不带有路径,需要有配置文件指定。
注意
import
命令具有提升效果,会提升到整个模块的头部,首先执行(是在编译阶段执行的)。因为import
是静态执行的,不能使用表达式和变量,即在运行时才能拿到结果的语法结构。
3、module 的整体加载
除了指定加载某个输出值,还可以用 *
指定一个对象,所有的变量都会加载在这个对象上。如下所示:
// circle.js 输出两个函数
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
// main.js 加载整个模块
import { area, circumference } from './circle';
console.log('圆面积:' + area(5));
console.log('圆周长:' + circumference(12));
上面的写法是逐一指定要加载的方法,整体加载的写法如下:
import * as circle from './circle';
console.log('圆面积:' + circle.area(5));
console.log('圆周长:' + circle.circumference(12));
注意,模块整体加载所在的那个对象(上例中的 circle
),必须是可以静态分析的,所以,不允许在运行时改变。
import * as circle from './circle';
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () { /* ... */ };
4、export default
在上面的例子中,使用 import
语句导入时,都需要知道模块中所要加载的变量名或函数名;但用户可能不想阅读源码,只想直接使用接口,这种情况下,我们可以用 export default
命令,导出模块默认变量(方法/类):
// export-default.js
export default function () {
console.log('foo');
}
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字:
// import-default.js
import customName from './export-default';
// 将输出 'foo'
customName();
export default
命令也可以用于非匿名函数。下面比较一下默认输出和正常输出:
// 第一组输出
export default function crc32() {
// ...
}
// 输入
import crc32 from 'crc32';
// 第二组输出
export function crc32() {
// ...
};
// 输入
import {crc32} from 'crc32';
可以看出,使用 export default
时,import
语句不用使用大括号。
import
和 export
命令只能在模块的顶层,不能在代码块之中。否则会语法报错。这样的设计,可以提高编译器效率,但是没有办法实现运行时加载。
因为 require
是运行时加载,所以 import
命令没有办法代替 require
的动态加载功能。所以,ES6 模块方案引入了 import()
函数。完成动态加载。
// 使用 import() 方法实现动态加载
let isLogin = true;
if (isLogin) {
import('user');
} else {
// ...
}
在上面的代码中,user
用来指定所要加载的模块的位置。import
语句能接受什么参数,import()
方法能接受同样的参数。
另外,import()
方法将返回一个 Promise 对象,这样,我们便可以在代码中异步执行链式方法:
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
}).catch(error => {
main.textContent = error.message;
});
5、import() 函数适用场合
5.1 按需加载
下面的代码演示了 import()
函数按需加载的一个场景,即:点击按钮时,才加载指定的库:
button.addEventListener('click', event => {
import('./dialogBox.js').then(dialogBox => {
dialogBox.open();
}).catch(error => {
/* Error handling */
})
});
5.2 条件加载
import()
函数可以放在 if...else
语句中,实现条件加载。
// 按条件加载
let condition = true;
if (condition) {
import('moduleA').then(/* ... */ );
} else {
import('moduleB').then(/* ... */ );
}
小结
ES6 提供的模块化方案,统一了 JS 模块导入/导出方式,大大提升了开发效率,也让团队协作变得更加容易。作为前端开发者,需要深入了解 ES6 模块化的用法,这是前端工程化的基本要求。