ES6 模块管理方案:export 和 import 详解

随着前端项目越来越庞大,JS 代码交叉引用、相互依赖等问题导致前端开发混乱,因此,对代码模块化是前端工程化的刚需。代码模块化,主要有以下两点好处:

  • 避免全局变量污染
  • 有效的处理依赖关系

ES2015(即:ES6)引入了模块的概念,提供了 exportimport 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 语句不用使用大括号。

importexport 命令只能在模块的顶层,不能在代码块之中。否则会语法报错。这样的设计,可以提高编译器效率,但是没有办法实现运行时加载。

因为 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 模块化的用法,这是前端工程化的基本要求。

分享