ES6 Module

在ES6之前,Javascript并没有官方标准的模块化方案,在社区中就出现了Common.jsAMD这两种方案,前者主要用于服务端,后者用在浏览器端,为了统一写法,又出现了UMD的标准,在ES6中,Javascript终于有了官方标准的模块化方案,这篇博客我们就来看看ES6的模块化方案。

Overview

定义和使用模块

// ------lib.js-----
export function foo() {
    console.log('foo in lib.js');
}
export default function bar() { 
    console.log('bar in lib.js')
}
// ------main.js-----
import { foo } from './lib.js';
import bar from './lib.js';

如上,是ES6Module的用法,这里有两个保留字importexport,从上面的代码中,我们可以发现,对于不同的export方式,对应的import的方式也不一样。

Name Export

// ------lib.js-----
let a = 1;
export foo() {}
export bar() {}
export a;

如上,就是name export的写法,每一个函数或者变量,直接被export,这种每写一个函数或者变量就export的方式也叫做inline export,如果在一个Module中有很多需要export的东西,这种写法就有些累赘了,这时候可以写成这样:

// ------lib.js------
let a = 1;
function foo() {}
function bar() {}
export {
    foo,
    bar,
    a
}

ES6 Module中,有local nameexport name的概念,顾名思义local name就是在Module中的名字,export name就是模块暴露给外界的名字。

// ------lib.js------
let a = 1;
function foo() {} // {A}
function bar() {} 
export {
    foo as bar,  // {B}
    bar as foo,
    a as b,
}

在上面的代码中,{A}行的foolocal name,在{B}行的barexport name,同样的我们在import的时候也可以使用别名。

// ------lib.js------
export function foo() {}
// ------main.js------
import { foo as bar } from './lib.js';
bar();

Default Export

// ------lib.js------
export default foo() {}
// ------main.js------
import foo from 'lib.js';
foo();

如上,我们import了一个defaultexportdefault exportname exportimport的时候,name export需要加大括号,而default export则不需要。

转发export

// ------lib.js------
let a = 1;
function foo() {
  console.log('foo in lib.js');
}

function bar() {
  console.log('bar in lib.js')
}

export {
  foo,
  bar,
  a,
}
// ------middleLib.js------
export { foo as bar, bar as foo } from './lib';
// ------main.js------
import { foo, bar } from './middleLib';
foo();
bar();

如上我们将模块lib.js中暴露的export通过另一个模块暴露出去了,这种常见的用法是,在封装好一个大的模块后,这个大的模块只需要向外界暴露部分小模块的API

- module
  -- a.js
  -- b.js
  -- index.js

如上,在module这个folder下我们有a.js, b.js, index.jsa.js和b.js是一些功能的实现,我们将a.js, b.js中需要向外界暴露的API通过index.js转发。

ES6 Module Under Hood

ES6给我们提供了很简洁的语法去使用模块,但是ES Module简洁的外表下,背后的细节仍然值得我们注意。

ES Module是静态的

如何理解ES Module是静态的这句话呢?意思就是,ES Module不同于Common.js这种模块化方案,只有在运行时才可以确定依赖的模块,在源码中看到的依赖关系就是运行时的依赖。

//------commonJSModule.js------
function foo() {}
function bar() {}

module.exports = {
 foo,
 bar,
}
//------main.js------
if (something) {
    let foo = require('./commonJSModule.js').foo;
    foo();
} else {
    let foo = require('./commonJSModule.js').bar;
    bar();
}

如上,在CommonJS中这中动态的在运行时决定依赖的方式在ES Module中是行不通的(但是有提案在做运行时的loader —> https://github.com/whatwg/loader/)。

ES Module的变量提升

// ------lib.js------
let a = 1;
function foo() {
  console.log('foo in lib.js');
}

function bar() {
  console.log('bar in lib.js')
}

export {
  foo,
  bar,
  a,
}
// ------main.js------
foo();
bar();
import { foo, bar } from './lib';

如上,我们可以先调用export的函数,在调用之后再import,这个在编译的时候会将import提升到顶层,但是在实际开发的过程中,虽然可以这样,但是这并不是好的代码风格。

ES Module是只读的

// ------commonJSModule.js------
function foo(){}
let a = 0;
module.exports = {
    foo,
    a,
}
// ------main.js------
let a = require('./lib.js').a;
++a; // 1
console.log(require('./lib.js').a) // 0

上面的代码在CommonJS下,我们可以修改require的东西,因为在commonJS中是拷贝一份,但是在ES Module中,我们无法修改import的东西,import的模块在行为上类似于const变量和frozen object

// ------lib.js------
let a = 1;
function foo() {
  console.log('foo in lib.js');
}

function bar() {
  console.log('bar in lib.js')
}

export {
  foo,
  bar,
  a,
}
// ------main.js------
import { foo, bar, a } from './lib';
foo();
bar();
foo.a = 1; // works {A}
++a; // error {B}

ES Module静态结构的设计带来的益处

ES Module的静态结构的设计特点,让代码在编译的时候就能确定依赖关系,不同于运行时,只有代码跑起来的时候很多东西才能确定,就好比,你计划完成一项复杂的项目,静态的结构能保证你在做之前,你确定的事是不会变的,而不是只要等到在项目进行中去确定,面对可预测的问题,我们是好解决的。

  • 有助于在代码打包的时候做 dead code elimination,减小bundle文件的大小(RollUp基于ES Module实现了tree shaking
  • 静态的结构有助于lint工具的检测
  • javascript支持宏做准备(宏操作需要静态的结构 —> https://www.sweetjs.org/)

Reference


兴趣遍地都是,坚持和持之以恒才是稀缺的


Written by@wang yao
I explain with words and code.

GitHub