乐趣区

深入理解ES6-Modules

深入了解 ES6 Modules

当下,我们几乎所有的项目都是基于 webpack、rollup 等构建工具进行开发的,模块化已经是常态。

我们对它并不陌生,今天,我们就再系统的回顾一下 ES6 的模块机制,并总结下常用的操作和最佳实践,希望对你有所帮助。

一些简单的背景

随用随取,是一种我们都希望实现的机制。

在 Javascript 中也一样,把一个大的 Javascript 程序分割成不同的部分,哪个部分要被用到,就取那一部分。

在很长一段时间内,NodeJS 拥有这样的能力,后来,越来越多的库和框架也拥有了模块化的能力,比如 CommonJS,或者基于 AMD 模型的实现(比如 RequireJs),还有后续的 Webpack, Babel 等。

到 2015 年,一个标准的模块化系统诞生了,这就是我们今天要说的主角 – ES6 模型系统。

一眼看上去,我们不发现,ES6 的模型系统和 CommonJS 语法非常的相似,毕竟 ES6 的模型系统是从 CommonJS 时代走过来的, 深受 CommonJS 影响。

看个简单的例子,比如在 CommonJs 中: (https://flaviocopes.com/commo…

//file.js
module.exports = value;

// 引入 value
const value = require('file.js')

而在 ES6 中:

// const.js
export const value = 'xxx';


import {value} from 'const.js'

语法是非常相似的。

下面我们就主要看 import 和 export,和几个相关的特性,了解 ES6 Modules 的更多方面。

模块化的好处

模块化的好处主要是两点:

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

随着时代的演进,浏览器原生也开始支持 es6 import 和 export 语法了。

先看个简单的例子:

<script type="module">
  import {addTextToBody} from '/util.js';

  addTextToBody('Modules are pretty cool.');
</script>

// util.js 
export function addTextToBody(text) {const div = document.createElement('div');
  div.textContent = text;
  document.body.appendChild(div);
}

如果要处理事件,也是一样,看个简单的例子:


<button id="test">Show Message</button>
<script type="module" crossorigin src="/showImport.js"></script>

// showImport.js
import {showMessage} from '/show.js'

document.getElementById('test').onclick = function() {showMessage();
}

// show.js
export function showMessage() {alert("Hello World!")
}

如果你想跑这个 demo, 注意要起个简单的服务:

$ http-server

否则,你会看到一个 CORS 抛错。

至于抛错的具体原因和其他细节,不是本文讨论的重点,感兴趣的可以阅读如下链接了解详情。

https://jakearchibald.com/201…

严格模式

https://developer.mozilla.org…

‘use strict’ 声明我们都不陌生,在 es5 时代我们也经常使用, 一般是在文件顶部加这个声明,目的就是禁用 Javascript 中不太友好的一部分,有助于我们写更严谨的代码。

这个特性,在 es6 语法中是默认开启的,如果代码里面有不太严格的代码,则会报错,例如:

下面是我从 MDN 中摘取的一些在 严格模式 中被 禁用 的部分:

  • Variables can’t be left undeclared
  • Function parameters must have unique names (or are considered syntax errors)
  • with is forbidden
  • Errors are thrown on assignment to read-only properties
  • Octal numbers like 00840 are syntax errors
  • Attempts to delete undeletable properties throw an error
  • delete prop is a syntax error, instead of assuming delete global[prop]
  • eval doesn’t introduce new variables into its surrounding scope
  • eval and arguments can’t be bound or assigned to
  • arguments doesn’t magically track changes to method parameters
  • arguments.callee throws a TypeError, no longer supported
  • arguments.caller throws a TypeError, no longer supported
  • Context passed as this in method invocations is not“boxed”(forced) into becoming an Object
  • No longer able to use fn.caller and fn.arguments to access the JavaScript stack
  • Reserved words (e.g protected, static, interface, etc) cannot be bound

exports 的几种用法

ES6 模块只支持静态导出,你只可以在模块的最外层作用域使用 export,不可在条件语句中使用,也不能在函数作用域中使用。

从分类上级讲,exports 主要有三种:

  1. Named Exports (Zero or more exports per module)
  2. Default Exports (One per module)
  3. Hybrid Exports

exports 总览:

// Exporting individual features
export let name1, name2, …, nameN; // also var, const
export let name1 = …, name2 = …, …, nameN; // also var, const
export function functionName(){...}
export class ClassName {...}

// Export list
export {name1, name2, …, nameN};

// Renaming exports
export {variable1 as name1, variable2 as name2, …, nameN};

// Exporting destructured assignments with renaming
export const {name1, name2: bar} = o;

// Default exports
export default expression;
export default function (…) {…} // also class, function*
export default function name1(…) {…} // also class, function*
export {name1 as default, …};

// Aggregating modules
export * from …; // does not set the default export
export * as name1 from …;
export {name1, name2, …, nameN} from …;
export {import1 as name1, import2 as name2, …, nameN} from …;
export {default} from …;

下面我就介绍一下常见的 exports 用法。

1. Named exports (导出每个函数 / 变量)

具名导出,这种方式导出多个函数,一般使用场景比如 utils、tools、common 之类的工具类函数集,或者全站统一变量等。

只需要在变量或函数前面加 export 关键字即可。

//------ lib.js ------
export const sqrt = Math.sqrt;

export function square(x) {return x * x;}
export function diag(x, y) {return sqrt(square(x) + square(y));
}

//------ main.js 使用方式 1 ------
import {square, diag} from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

//------ main.js 使用方式 2 ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

我们也可以直接导出一个列表,例如上面的 lib.js 可以改写成:

//------ lib.js ------
const sqrt = Math.sqrt;
function square(x) {return x * x;}
function add (x, y) {return x + y;}
export {sqrt, square, add}

2. Default exports (导出一个默认 函数 / 类)

这种方式比较简单,一般用于一个类文件,或者功能比较单一的函数文件使用。

一个模块中只能有一个 export default 默认输出。

export default 与 export 的主要区别有两个:

不需要知道导出的具体变量名, 导入 (import) 时不需要{}.

//------ myFunc.js ------
export default function () {};

//------ main.js ------
import myFunc from 'myFunc';
myFunc();

导出一个类

//------ MyClass.js ------
class MyClass{}

export default MyClass;

//------ Main.js ------
import MyClass from 'MyClass';

注意这里默认导出不需要用{}。

3. Mixed exports (混合导出)

混合导出,也就是 上面第一点和第二点结合在一起的情况。比较常见的比如 Lodash,都是这种组合方式。

//------ lib.js ------
export var myVar = ...;
export let myVar = ...;
export const MY_CONST = ...;

export function myFunc() {// ...}
export function* myGeneratorFunc() {// ...}
export default class MyClass {// ...}

// ------ main.js ------
import MyClass, {myFunc} from 'lib';

再比如 lodash 例子:

//------ lodash.js ------
export default function (obj) {// ...};
export function each(obj, iterator, context) {// ...}
export {each as forEach};

//------ main.js ------
import _, {forEach} from 'lodash';

4. Re-exporting (别名导出)

一般情况下,export 输出的变量就是在原文件中定义的名字,但也可以用 as 关键字来指定别名,这样做一般是为了简化或者语义化 export 的函数名。

//------ lib.js ------
export function getUserName(){// ...};
export function setName(){// ...};

// 输出别名,在 import 的时候可以同时使用原始函数名和别名
export {
  getName as get, // 允许使用不同名字输出两次
  getName as getNameV2,
  setName as set
}

5. Module Redirects (中转模块导出)

有时候为了避免上层模块导入太多的模块,我们可能使用底层模块作为中转,直接导出另一个模块的内容如下:

//------ myFunc.js ------
export default function() {...};
 
//------ lib.js ------
export * from 'myFunc';
export function each() {...};
 
//------ main.js ------
import myFunc, {each} from 'lib';

export 只支持在最外层静态导出、只支持导出变量、函数、类,如下的几种用法都是错误的。` 错误 ` 的 export 用法:

// 直接输出变量的值
export 'Mark';

// 未使用中括号 或 未加 default
// 当只有一个导出数,需加 default,或者使用中括号
var name = 'Mark';
export name;

//export 不要输出块作用域内的变量
function () {
  var name = 'Mark';
  export  {name};
}

import 的几种用法

import 的用法和 export 是一一对应的,但是 import 支持静态导入和动态导入两种方式,动态 import 支持晚一些,兼容性要差一些。

下面我就总结下 import 的基本用法:

1. Import All things

当 export 有多个函数或变量时,如文中 export 的第一点,可以使用 * as 关键字来导出所有函数及变量,同时 as 后面跟着的名称做为 该模块的命名空间。

// 导出 lib 的所有函数及变量
import * as lib from 'lib';

// 以 lib 做为命名空间进行调用,类似于 object 的方式
console.log(lib.square(11)); // 121

2. Import a single/multiple export from a module

从模块文件中导入单个或多个函数,与 * as namepage 方式不同,这个是按需导入。如下例子:

// 导入 square 和 diag 两个函数
import {square, diag} from 'lib';

// 只导入 square 一个函数
import {square} from 'lib';

// 导入默认模块
import _ from 'lodash';

// 导入默认模块和单个函数,这样做主要是简化单个函数的调用
import _, {each} from 'lodash';

3. Rename multiple exports during import

和 export 一样,也可以用 as 关键字来设置别名,当 import 的两个类的名字一样时,可以使用 as 来重设导入模块的名字,也可以用 as 来简化名称。
比如:

// 用 as 来 简化函数名称
import {
  reallyReallyLongModuleExportName as shortName,
  anotherLongModuleName as short
} from '/modules/my-module.js';

// 避免重名
import {lib as UserLib} from "alib";
import {lib as GlobalLib} from "blib";

4. Import a module for its side effects only

有时候我们只想 import 一个模块进来,比如样式,或者一个类库。

// 导入样式
import './index.less';

// 导入类库
import 'lodash';

5. Dynamic Imports

静态 import 在首次加载时候会把全部模块资源都下载下来.

我们实际开发时候,有时候需要动态 import(dynamic import)。

例如点击某个选项卡,才去加载某些新的模块:


// 当动态 import 时,返回的是一个 promise
import('lodash')
  .then((lodash) => {// Do something with lodash.});

// 上面这句实际等同于
const lodash = await import('lodash');

es7 的新用法:

async function run() {const myModule = await import('./myModule.js');

    const {export1, export2} = await import('./myModule.js');

    const [module1, module2, module3] =
        await Promise.all([import('./module1.js'),
            import('./module2.js'),
            import('./module3.js'),
        ]);
}

run();

总结

以上,我总结了 ES6 Module 的简单背景和 常见的 import , export 用法,但这远远不是它的全部,篇幅有限,如果想了解更多,可以看下面的延伸阅读部分(质量都还不错, 可以看看)。

最后

如果觉得内容有帮助,可以关注我的公众号「前端 e 进阶」,掌握最新资讯,一起学习,一起成长!

延伸阅读:

ECMAScript 6 modules: the final syntax

JavaScript modules

dynamic-import

Node 中没搞明白 require 和 import,你会被坑的很惨

https://www.zhangxinxu.com/wordpress/2018/08/browser-native-es6-export-import-module/

退出移动版