JavaScript 模块的当前环境非常多样化:ES6 带来了内置模块,但是它们之前的模块系统仍然存在。了解后者有助于理解前者,所以让我们进行调查。
最初,浏览器只有 _ 脚本 _ - 在全局范围内执行的代码片段。例如,考虑一个 HTML 文件,它通过以下 HTML 元素加载 _ 脚本文件 _:
<script src="my-library.js"></script>
在脚本文件中,我们模拟一个模块:
var myModule = function () { // Open IIFE
// Imports (via global variables)
var importedFunc1 = otherLibrary1.importedFunc1;
var importedFunc2 = otherLibrary2.importedFunc2;
// Body
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports (assigned to global variable `myModule`)
return {
exportedFunc: exportedFunc,
};
}(); // Close IIFE
在我们开始使用实际模块(在 ES6 中引入)之前,所有代码都是用 ES5 编写的(没有const
和let
,只有var
)。
myModule
是一个全局变量。定义模块的代码包含在 _ 立即调用的函数表达式 _(IIFE)中。创建一个函数并立即调用它,与直接执行代码相比只有一个好处(不包装它):在 IIFE 中定义的所有变量都保持在其范围内,不会变为全局变量。最后,我们选择要导出的内容并通过对象字面值返回。这种模式被称为 _ 揭示模块模式 _(由 Christian Heilmann 创造)。
这种模拟模块的方法有几个问题:
在 ECMAScript 6 之前,JavaScript 没有内置模块。因此,该语言的灵活语法用于在语言中实现自定义模块系统 。两个流行的是 CommonJS(针对服务器端)和 AMD(异步模块定义,针对客户端)。
模块的原始 CommonJS 标准主要是为服务器和桌面平台创建的。它是 Node.js 模块系统的基础,在那里它获得了令人难以置信的流行度。对这种受欢迎程度的贡献是 Node 的软件包管理器 npm,以及支持在客户端使用 Node 模块(browserify 和 webpack)的工具。
从现在开始,我可以互换地使用术语 _CommonJS 模块 _ 和 _Node.js 模块 _,即使 Node.js 还有一些额外的功能。以下是 Node.js 模块的示例。
// Imports
var importedFunc1 = require('other-module1').importedFunc1;
var importedFunc2 = require('other-module2').importedFunc2;
// Body
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports
module.exports = {
exportedFunc: exportedFunc,
};
CommonJS 的特征如下:
创建 AMD 模块格式是为了在浏览器中比 CommonJS 格式更容易使用。它最受欢迎的实现是 RequireJS。以下是 RequireJS 模块的示例。
define(['other-module1', 'other-module2'],
function (otherModule1, otherModule2) {
var importedFunc1 = otherModule1.importedFunc1;
var importedFunc2 = otherModule2.importedFunc2;
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
return {
exportedFunc: exportedFunc,
};
});
AMD 的特点如下:
eval()
)。网上并不总是允许这样做。看看 CommonJS 和 AMD,JavaScript 模块系统之间的相似之处出现了:
ESAS 引入了 ECMAScript 模块:它们坚定地遵循 JavaScript 模块的传统,并分享现有模块系统的许多特性:
使用 CommonJS,ES 模块共享紧凑语法,单个导出的语法比 _ 命名导出 _(到目前为止,我们只看到命名导出)和支持循环依赖关系更好。
对于 AMD,ES 模块共享异步加载和可配置模块加载的设计(例如,如何解析说明符)。
ES 模块也有新的好处:
这是 ES 模块语法的示例:
import {importedFunc1} from 'other-module1';
import {importedFunc2} from 'other-module2';
function internalFunc() {
···
}
export function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
从现在开始,“模块”意味着“ECMAScript 模块”。
ECMAScript 模块包括三个部分:
第 1 部分和第 2 部分与 ES6 一起介绍。第 3 部分的工作正在进行中。
每个模块可以有零个或多个命名导出。
例如,请考虑以下三个文件:
lib/my-math.js
main1.js
main2.js
模块my-math.js
有两个命名导出:square
和MY_CONSTANT
。
let notExported = 'abc';
export function square(x) {
return x * x;
}
export const MY_CONSTANT = 123;
模块main1.js
有一个命名导入,square
:
import {square} from './lib/my-math.js';
assert.equal(square(3), 9);
模块main2.js
有一个所谓的 _ 命名空间导入 _ - my-math.js
的所有命名导出都可以作为对象myMath
的属性访问:
import * as myMath from './lib/my-math.js';
assert.equal(myMath.square(3), 9);
exercises/modules/export_named_test.js
每个模块最多只能有一个默认导出。这个想法是模块 _ 是 _ 的默认导出值。模块可以同时具有命名导出和默认导出,但通常最好坚持每个模块一种导出样式。
作为默认导出的示例,请考虑以下两个文件:
my-func.js
main.js
模块my-func.js
具有默认导出:
export default function () {
return 'Hello!';
}
模块main.js
默认 - 导入导出的函数:
import myFunc from './my-func.js';
assert.equal(myFunc(), 'Hello!');
注意语法差异:命名导入周围的花括号表示我们将 _ 传入 _ 模块,而默认导入 _ 是 _ 模块。
默认导出的最常见用例是包含单个函数或单个类的模块。
执行默认导出有两种样式。
首先,您可以使用export default
标记现有声明:
export default function foo() {} // no semicolon!
export default class Bar {} // no semicolon!
其次,您可以直接默认导出值。在那种风格中,export default
本身就像一个宣言。
export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };
为什么有两种默认导出样式?原因是export default
不能用于标记const
:const
可能定义多个值,但export default
只需要一个值。
// Not legal JavaScript!
export default const foo = 1, bar = 2, baz = 3;
使用此假设代码,您不知道三个值中的哪一个是默认导出。
exercises/modules/export_default_test.js
命名模块文件及其导入的变量没有既定的最佳实践。
在本章中,我使用了以下命名方式:
模块文件的名称是破折号的,并以小写字母开头:
./my-module.js
./some-func.js
命名空间导入的名称是小写的和驼峰式的:
import * as myModule from './my-module.js';
默认导入的名称是小写的和驼峰式的:
import someFunc from './some-func.js';
这种风格背后的理由是什么?
npm 不允许包名中的大写字母( source )。因此,我们避免使用驼峰,因此“本地”文件的名称与 npm 包的名称一致。
将基于短划线的文件名转换为以驼峰为基础的 JavaScript 变量名称有明确的规则。由于我们如何命名命名空间导入,这些规则适用于命名空间导入和默认导入。
我也喜欢下划线模块文件名,因为你可以直接使用这些名称进行名称空间导入(没有任何翻译):
import someFunc from './some-func.js';
但是这种样式对默认导入不起作用:我喜欢下划线外壳用于命名空间对象,但它不是函数等的好选择。
到目前为止,我们直观地使用了进口和出口,一切似乎都按预期运作。但现在是时候仔细研究进出口的真实关系了。
考虑以下两个模块:
counter.js
main.js
counter.js
导出一个(mutable!)变量和一个函数:
export let counter = 3;
export function incCounter() {
counter++;
}
main.js
name-导入两个导出。当我们使用incCounter()
时,我们发现与counter
的连接是实时的 - 我们总是可以访问该变量的实时状态:
import { counter, incCounter } from './counter.js';
// The imported value `counter` is live
assert.equal(counter, 3);
incCounter();
assert.equal(counter, 4);
请注意,虽然连接是实时的并且我们可以读取counter
,但我们无法更改此变量(例如,通过counter++
)。
为什么 ES 模块会以这种方式运行?
首先,分割模块更容易,因为以前的共享变量可以成为导出。
其次,这种行为对于循环导入至关重要。在执行模块之前,模块的导出是已知的。因此,如果模块 L 和模块 M 相互导入,则循环地执行以下步骤:
循环导入是您应该尽可能避免的,但它们可能出现在复杂系统或重构系统中。重要的是,当发生这种情况时,事情不会破裂。
一个关键规则是:
所有 ES 模块说明符必须是有效的 URL 并指向实际文件。
除此之外,一切仍然有点不稳定。
在我们进一步了解之前,我们需要建立以下类别的模块说明符(源自 CommonJS):
相对路径:以点开头。例子:
'./some/other/module.js'
'../../lib/counter.js'
绝对路径:以斜杠开头。例:
'/home/jane/file-tools.js'
完整的 URL:包括协议(从技术上讲,路径也是 URL)。例:
'https://example.com/some-module.js'
裸路径:不要以点,斜线或协议开头。在 CommonJS 模块中,裸路径很少有文件扩展名。
'lodash'
'mylib/string-tools'
'foo/dist/bar.js'
Node.js 中对 ES 模块的支持正在进行中。目前的计划(截至 2018-12-20)是按如下方式处理模块说明符:
path
,fs
等)可以通过裸路径导入。'foo/dist/bar.js'
.mjs
(可能有一种方法可以切换到每个包的不同扩展名)。浏览器处理模块说明符如下:
text/javascript
一起提供即可。请注意,将模块说明符编译为单个文件的浏览器和 webpack 等捆绑工具对模块说明符的限制要少于浏览器,因为它们在编译时运行,而不是在运行时运行。
导入和解构看起来都很相似:
import {foo} from './bar.js'; // import
const {foo} = require('./bar.js'); // destructuring
但它们完全不同:
进口与出口保持联系。
您可以在解构模式中再次进行解构,但导入语句中的{}
不能嵌套。
重命名的语法不同:
import {foo as f} from './bar.js'; // importing
const {foo: f} = require('./bar.js'); // destructuring
理由:解构让人想起对象字面值(包括嵌套),而导入则唤起重命名的想法。
到目前为止,导入模块的唯一方法是通过import
语句。这些语句的局限性:
即将推出的 JavaScript 功能改变了:import()
运算符,它被用作异步函数(它只是一个运算符,因为它需要隐式访问当前模块的 URL)。
请考虑以下文件:
lib/my-math.js
main1.js
main2.js
我们已经看过模块my-math.js
:
let notExported = 'abc';
export function square(x) {
return x * x;
}
export const MY_CONSTANT = 123;
这是在main1.js
中使用import()
的样子:
const dir = './lib/';
const moduleSpecifier = dir + 'my-math.js';
function loadConstant() {
return import(moduleSpecifier)
.then(myMath => {
const result = myMath.MY_CONSTANT;
assert.equal(result, 123);
return result;
});
}
方法.then()
是 Promises 的一部分,这是一种处理异步结果的机制,本书稍后将对此进行介绍。
此代码中的两件事在以前是不可能的:
接下来,我们将在main2.js
中实现完全相同的功能,但是通过所谓的 _ 异步函数 _,它为 Promises 提供了更好的语法。
const dir = './lib/';
const moduleSpecifier = dir + 'my-math.js';
async function loadConstant() {
const myMath = await import(moduleSpecifier);
const result = myMath.MY_CONSTANT;
assert.equal(result, 123);
return result;
}
唉,import()
还不是 JavaScript 的标准版本,但可能会相对较快。这意味着支持是混合的,可能不一致。
import()
:“ES 提案:import()
- 在 2ality 上动态导入 ES 模块”。参见测验应用程序。