深入了解 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 haveunique names
(or are considered syntax errors) -
with
is forbidden - Errors are thrown on assignment to
read-only properties
-
Octal numbers
like 00840 aresyntax 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 主要有三种:
- Named Exports (Zero or more exports per module)
- Default Exports (One per module)
- 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/