ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 24.模块 > 原文: [http://exploringjs.com/impatient-js/ch_modules.html](http://exploringjs.com/impatient-js/ch_modules.html) JavaScript 模块的当前环境非常多样化:ES6 带来了内置模块,但是它们之前的模块系统仍然存在。了解后者有助于理解前者,所以让我们进行调查。 ### 24.1。在模块之前:脚本 最初,浏览器只有 _ 脚本 _ - 在全局范围内执行的代码片段。例如,考虑一个 HTML 文件,它通过以下 HTML 元素加载 _ 脚本文件 _: ```html <script src="my-library.js"></script> ``` 在脚本文件中,我们模拟一个模块: ```js 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 创造)。 这种模拟模块的方法有几个问题: * 脚本文件中的库通过全局变量导出和导入功能,这会冒名称冲突的风险。 * 没有明确声明依赖关系,并且脚本没有内置的方法来加载它所依赖的脚本。因此,网页不仅要加载页面所需的脚本,还要加载这些脚本的依赖关系,依赖项的依赖关系等等。它必须按正确的顺序执行! ### 24.2。在 ES6 之前创建的模块系统 在 ECMAScript 6 之前,JavaScript 没有内置模块。因此,该语言的灵活语法用于在语言中实现自定义模块系统 _。两个流行的是 CommonJS(针对服务器端)和 AMD(异步模块定义,针对客户端)。_ #### 24.2.1。服务器端:CommonJS 模块 模块的原始 CommonJS 标准主要是为服务器和桌面平台创建的。它是 Node.js 模块系统的基础,在那里它获得了令人难以置信的流行度。对这种受欢迎程度的贡献是 Node 的软件包管理器 npm,以及支持在客户端使用 Node 模块(browserify 和 webpack)的工具。 从现在开始,我可以互换地使用术语 _CommonJS 模块 _ 和 _Node.js 模块 _,即使 Node.js 还有一些额外的功能。以下是 Node.js 模块的示例。 ```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 的特征如下: * 专为服务器设计。 * 模块意味着同步加载。 * 紧凑的语法。 #### 24.2.2。客户端:AMD(异步模块定义)模块 创建 AMD 模块格式是为了在浏览器中比 CommonJS 格式更容易使用。它最受欢迎的实现是 RequireJS。以下是 RequireJS 模块的示例。 ```js 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 的特点如下: * 专为浏览器设计。 * 模块意味着异步加载。这对于浏览器来说是一个至关重要的要求,代码不能等到模块下载完毕。必须在模块可用时通知它。 * 语法稍微复杂一些。从好的方面来说,AMD 模块可以直接执行,无需自定义创建和执行源代码(想想`eval()`)。网上并不总是允许这样做。 #### 24.2.3。 JavaScript 模块的特征 看看 CommonJS 和 AMD,JavaScript 模块系统之间的相似之处出现了: * 每个文件有一个模块(AND 每个文件也支持多个模块)。 * 这样的文件基本上是一段执行的代码: * 导出:该代码包含声明(变量,函数等)。默认情况下,这些声明保留在模块的本地,但您可以将其中一些声明标记为导出。 * 导入:模块可以从其他模块导入实体。那些其他模块通过 _ 模块说明符 _(通常是路径,偶尔 URL)来识别。 * 模块是 _ 单例 _:即使多次导入模块,也只存在单个实例。 * 没有使用全局变量。相反,模块说明符用作全局 ID。 ### 24.3。 ECMAScript 模块 ESAS 引入了 ECMAScript 模块:它们坚定地遵循 JavaScript 模块的传统,并分享现有模块系统的许多特性: * 使用 CommonJS,ES 模块共享紧凑语法,单个导出的语法比 _ 命名导出 _(到目前为止,我们只看到命名导出)和支持循环依赖关系更好。 * 对于 AMD,ES 模块共享异步加载和可配置模块加载的设计(例如,如何解析说明符)。 ES 模块也有新的好处: * 它们的语法比 CommonJS 更紧凑。 * 它们的模块具有静态结构(在运行时无法更改)。这样可以实现静态检查,优化的导入访问,更好的捆绑(交付更少的代码)等等。 * 他们对循环进口的支持是完全透明的。 这是 ES 模块语法的示例: ```js import {importedFunc1} from 'other-module1'; import {importedFunc2} from 'other-module2'; function internalFunc() { ··· } export function exportedFunc() { importedFunc1(); importedFunc2(); internalFunc(); } ``` 从现在开始,“模块”意味着“ECMAScript 模块”。 #### 24.3.1。 ECMAScript 模块:三部分 ECMAScript 模块包括三个部分: 1. 声明模块语法:什么是模块?如何申报进出口? 2. 语法的语义:如何处理由导入创建的变量绑定?如何处理导出的变量绑定? 3. 用于配置模块加载的编程加载器 API。 第 1 部分和第 2 部分与 ES6 一起介绍。第 3 部分的工作正在进行中。 ### 24.4。命名出口 每个模块可以有零个或多个命名导出。 例如,请考虑以下三个文件: ```js lib/my-math.js main1.js main2.js ``` 模块`my-math.js`有两个命名导出:`square`和`MY_CONSTANT`。 ```js let notExported = 'abc'; export function square(x) { return x * x; } export const MY_CONSTANT = 123; ``` 模块`main1.js`有一个命名导入,`square`: ```js import {square} from './lib/my-math.js'; assert.equal(square(3), 9); ``` 模块`main2.js`有一个所谓的 _ 命名空间导入 _ - `my-math.js`的所有命名导出都可以作为对象`myMath`的属性访问: ```js import * as myMath from './lib/my-math.js'; assert.equal(myMath.square(3), 9); ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:命名出口** `exercises/modules/export_named_test.js` ### 24.5。默认导出 每个模块最多只能有一个默认导出。这个想法是模块 _ 是 _ 的默认导出值。模块可以同时具有命名导出和默认导出,但通常最好坚持每个模块一种导出样式。 作为默认导出的示例,请考虑以下两个文件: ```js my-func.js main.js ``` 模块`my-func.js`具有默认导出: ```js export default function () { return 'Hello!'; } ``` 模块`main.js`默认 - 导入导出的函数: ```js import myFunc from './my-func.js'; assert.equal(myFunc(), 'Hello!'); ``` 注意语法差异:命名导入周围的花括号表示我们将 _ 传入 _ 模块,而默认导入 _ 是 _ 模块。 默认导出的最常见用例是包含单个函数或单个类的模块。 #### 24.5.1。默认导出的两种样式 执行默认导出有两种样式。 首先,您可以使用`export default`标记现有声明: ```js export default function foo() {} // no semicolon! export default class Bar {} // no semicolon! ``` 其次,您可以直接默认导出值。在那种风格中,`export default`本身就像一个宣言。 ```js 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`只需要一个值。 ```js // Not legal JavaScript! export default const foo = 1, bar = 2, baz = 3; ``` 使用此假设代码,您不知道三个值中的哪一个是默认导出。 ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:默认导出** `exercises/modules/export_default_test.js` ### 24.6。命名模块 命名模块文件及其导入的变量没有既定的最佳实践。 在本章中,我使用了以下命名方式: * 模块文件的名称是破折号的,并以小写字母开头: ```js ./my-module.js ./some-func.js ``` * 命名空间导入的名称是小写的和驼峰式的: ```js import * as myModule from './my-module.js'; ``` * 默认导入的名称是小写的和驼峰式的: ```js import someFunc from './some-func.js'; ``` 这种风格背后的理由是什么? * npm 不允许包名中的大写字母( [source](npm%20doesn’t%20allow%20uppercase%20letters) )。因此,我们避免使用驼峰,因此“本地”文件的名称与 npm 包的名称一致。 * 将基于短划线的文件名转换为以驼峰为基础的 JavaScript 变量名称有明确的规则。由于我们如何命名命名空间导入,这些规则适用于命名空间导入和默认导入。 我也喜欢下划线模块文件名,因为你可以直接使用这些名称进行名称空间导入(没有任何翻译): ```js import someFunc from './some-func.js'; ``` 但是这种样式对默认导入不起作用:我喜欢下划线外壳用于命名空间对象,但它不是函数等的好选择。 ### 24.7。导入是导出的只读视图 到目前为止,我们直观地使用了进口和出口,一切似乎都按预期运作。但现在是时候仔细研究进出口的真实关系了。 考虑以下两个模块: ```js counter.js main.js ``` `counter.js`导出一个(mutable!)变量和一个函数: ```js export let counter = 3; export function incCounter() { counter++; } ``` `main.js` name-导入两个导出。当我们使用`incCounter()`时,我们发现与`counter`的连接是实时的 - 我们总是可以访问该变量的实时状态: ```js 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 相互导入,则循环地执行以下步骤: * L 的执行开始。 * L 进口 M. L's 进口指向 M 内的未初始化槽。 * L'的尸体尚未执行。 * M 的执行开始(由导入触发)。 * M 进口 L. * M 的主体被执行。现在 L's 进口有值(由于实时连接)。 * L 的主体被执行。现在 M 的进口有值。 循环导入是您应该尽可能避免的,但它们可能出现在复杂系统或重构系统中。重要的是,当发生这种情况时,事情不会破裂。 ### 24.8。模块说明符 一个关键规则是: > 所有 ES 模块说明符必须是有效的 URL 并指向实际文件。 除此之外,一切仍然有点不稳定。 #### 24.8.1。模块说明符的类别 在我们进一步了解之前,我们需要建立以下类别的模块说明符(源自 CommonJS): * 相对路径:以点开头。例子: ```js './some/other/module.js' '../../lib/counter.js' ``` * 绝对路径:以斜杠开头。例: ```js '/home/jane/file-tools.js' ``` * 完整的 URL:包括协议(从技术上讲,路径也是 URL)。例: ```js 'https://example.com/some-module.js' ``` * 裸路径:不要以点,斜线或协议开头。在 CommonJS 模块中,裸路径很少有文件扩展名。 ```js 'lodash' 'mylib/string-tools' 'foo/dist/bar.js' ``` #### 24.8.2。 Node.js 中的 ES 模块说明符 Node.js 中对 ES 模块的支持正在进行中。目前的计划(截至 2018-12-20)是按如下方式处理模块说明符: * 相对路径,绝对路径和完整 URL 按预期工作。他们都必须指向真实的文件。 * 裸路径: * 内置模块(`path`,`fs`等)可以通过裸路径导入。 * 所有其他裸路径必须指向文件:`'foo/dist/bar.js'` * ES 模块的默认文件扩展名为`.mjs`(可能有一种方法可以切换到每个包的不同扩展名)。 #### 24.8.3。浏览器中的 ES 模块说明符 浏览器处理模块说明符如下: * 相对路径,绝对路径和完整 URL 按预期工作。他们都必须指向真实的文件。 * 最终如何处理裸路径尚不清楚。您最终可以通过查找表将它们映射到其他说明符。 * 模块的文件扩展名无关紧要,只要它们与内容类型`text/javascript`一起提供即可。 请注意,将模块说明符编译为单个文件的浏览器和 webpack 等捆绑工具对模块说明符的限制要少于浏览器,因为它们在编译时运行,而不是在运行时运行。 ### 24.9。语法缺陷:导入不是解构 导入和解构看起来都很相似: ```js import {foo} from './bar.js'; // import const {foo} = require('./bar.js'); // destructuring ``` 但它们完全不同: * 进口与出口保持联系。 * 您可以在解构模式中再次进行解构,但导入语句中的`{}`不能嵌套。 * 重命名的语法不同: ```js import {foo as f} from './bar.js'; // importing const {foo: f} = require('./bar.js'); // destructuring ``` 理由:解构让人想起对象字面值(包括嵌套),而导入则唤起重命名的想法。 ### 24.10。预览:动态加载模块 到目前为止,导入模块的唯一方法是通过`import`语句。这些语句的局限性: * 您必须在模块的顶层使用它们。也就是说,当你在一个街区内时,你不能导入一些东西。 * 模块说明符始终是固定的。也就是说,您无法根据条件更改导入的内容,也无法动态检索或组装说明符。 [即将推出的 JavaScript 功能](https://github.com/tc39/proposal-dynamic-import)改变了:`import()`运算符,它被用作异步函数(它只是一个运算符,因为它需要隐式访问当前模块的 URL)。 请考虑以下文件: ```js lib/my-math.js main1.js main2.js ``` 我们已经看过模块`my-math.js`: ```js let notExported = 'abc'; export function square(x) { return x * x; } export const MY_CONSTANT = 123; ``` 这是在`main1.js`中使用`import()`的样子: ```js 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 提供了更好的语法。 ```js 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 的标准版本,但可能会相对较快。这意味着支持是混合的,可能不一致。 ### 24.11。进一步阅读 * 更多关于`import()`:[“ES 提案:`import()` - 在 2ality 上动态导入 ES 模块”](http://2ality.com/2017/01/import-operator.html)。 * 有关 ECMAScript 模块的深入了解,请参考[“探索 ES6”](http://exploringjs.com/es6/ch_modules.html)。 ![](https://img.kancloud.cn/ff/a8/ffa8e16628cad59b09c786b836722faa.svg) **测验** 参见[测验应用程序](ch_quizzes-exercises.html#quizzes)。