随着应用程序和代码库的增长,保持代码的可维护性和独立性变得越来越重要。模块模式允许您将代码分割成更小的、可重用的部分。
除了能够将代码分割成更小的可重用片段之外,模块还允许您将文件中的某些值保留为private 。默认情况下,模块内的声明的范围(封装)到该模块。如果我们不显式导出某个值,则该值在该模块外部不可用。这降低了代码库其他部分中声明的值发生名称冲突的风险,因为这些值在全局范围内不可用。
ES2015模块
ES2015 引入了内置 JavaScript 模块。模块是包含 JavaScript 代码的文件,与普通脚本相比,其行为存在一些差异。
让我们看一个名为 的模块的示例**math.js
**,其中包含数学函数。
js
function add(x, y) {
return x + y;
}
function multiply(x) {
return x * 2;
}
function subtract(x, y) {
return x - y;
}
function square(x) {
return x * x;
}
我们有一个**math.js
**包含一些简单数学逻辑的文件。我们有一些函数允许用户进行加法、乘法、减法以及获取他们传递的值的平方。
然而,我们不只是想在**math.js
文件中使用这些函数,我们希望能够在 index.js
文件中引用它们!目前,文件内会引发错误 index.js
:文件内没有 index.js
名为 add
、 subtract
或 multiply
的函数 square
。我们正在尝试引用文件中不可用的函数 index.js
**。
为了使这些函数可**math.js
用于其他文件,我们首先必须将它们导出。为了从模块导出代码,我们可以使用关键字 export
。导出函数的一种方法是使用命名导出 export
:我们只需在要公开公开的部分前面添加关键字即可。在这种情况下,我们需要 export
在每个函数前面添加关键字,因为 index.js
**应该可以访问所有四个函数。
js
export function add(x, y) {
return x + y;
}
export function multiply(x) {
return x * 2;
}
export function subtract(x, y) {
return x - y;
}
export function square(x) {
return x * x;
}
我们刚刚将**add
、 multiply
、 subtract
和 square
**函数设为可导出!但是,仅从模块导出值并不足以使它们对所有文件公开可用。为了能够使用从模块导出的值,您必须将它们显式导入到需要引用它们的文件中。
index.js
我们必须使用关键字导入文件顶部的值 import
。为了让 javascript 知道我们要从哪个模块导入这些函数,我们需要添加一个**from
**值和模块的相对路径。
js
import { add, multiply, subtract, square } from "./math.js";
math.js
我们刚刚从文件中的模块导入了四个函数 index.js
!我们现在就来试试看是否可以使用这些功能!
js
function add(x, y) {
return x + y;
}
function multiply(x) {
return x * 2;
}
function subtract(x, y) {
return x - y;
}
function square(x) {
return x * x;
}
引用错误消失了,我们现在可以使用模块导出的值!
拥有模块的一个很大的好处是我们只能访问使用关键字显式导出的值 export
。我们未使用**export
**关键字显式导出的值仅在该模块中可用。
让我们创建一个只能在**math.js
文件中引用的值,称为 privateValue
**.
js
const privateValue = "This is a value private to the module!";
export function add(x, y) {
return x + y;
}
export function multiply(x) {
return x * 2;
}
export function subtract(x, y) {
return x - y;
}
export function square(x) {
return x * x;
}
请注意我们没有**export
在 前面添加关键字 privateValue
。由于我们没有导出 privateValue
变量,因此我们无法在 math.js
**模块外部访问该值!
js
import { add, multiply, subtract, square } from "./math.js";
console.log(privateValue);
/* Error: privateValue is not defined */
通过将值保持为模块私有,可以降低意外污染全局范围的风险。您不必担心会意外覆盖开发人员使用您的模块创建的值,这些值可能与您的私有值同名:它可以防止命名冲突。
有时,导出的名称可能会与本地值发生冲突。
js
import { add, multiply, subtract, square } from "./math.js";
function add(...args) {
return args.reduce((acc, cur) => cur + acc);
} /* Error: add has already been declared */
function multiply(...args) {
return args.reduce((acc, cur) => cur * acc);
}
/* Error: multiply has already been declared */
在本例中,我们有名为**add
和 multiply
in 的函数 index.js
。如果我们导入具有相同名称的值,最终会导致命名冲突: add
并且 multiply
**已经被声明了!幸运的是,我们可以使用关键字重命名 **as
**导入的值。
让我们将导入的**add
and multiply
函数重命名为 addValues
**and multiplyValues
。
js
import {
add as addValues,
multiply as multiplyValues,
subtract,
square
} from "./math.js";
function add(...args) {
return args.reduce((acc, cur) => cur + acc);
}
function multiply(...args) {
return args.reduce((acc, cur) => cur * acc);
}
/* From math.js module */
addValues(7, 8);
multiplyValues(8, 9);
subtract(10, 3);
square(3);
/* From index.js file */
add(8, 9, 2, 10);
multiply(8, 9, 2, 10);
除了命名导出(仅使用**export
关键字定义的导出)之外,您还可以使用默认导出。每个模块只能有一个**默认导出。
让我们将该**add
函数设为默认导出,并将其他函数保留为命名导出。 export default
**我们可以通过在值前面添加来导出默认值。
js
export default function add(x, y) {
return x + y;
}
export function multiply(x) {
return x * 2;
}
export function subtract(x, y) {
return x - y;
}
export function square(x) {
return x * x;
}
命名导出和默认导出之间的区别在于从模块导出值的方式,有效地改变了我们导入值的方式。
以前,我们必须使用括号来命名导出:import { module } from 'module'
。使用默认导出,我们可以导入不带 括号的值: import module from 'module'
。
js
import add, { multiply, subtract, square } from "./math.js";
add(7, 8);
multiply(8, 9);
subtract(10, 3);
square(3);
如果有可用的默认导出,则从不带括号的模块导入的值始终是默认导出的值。
由于 JavaScript 知道该值始终是默认导出的值,因此我们可以为导入的默认值指定另一个名称,而不是导出时使用的名称。例如,我们可以调用它,而不是**add
使用名称导入函数。 add
** addValues
js
import addValues, { multiply, subtract, square } from "./math.js";
addValues(7, 8);
multiply(8, 9);
subtract(10, 3);
square(3);
即使我们导出了名为 的函数add
,我们也可以以任何我们喜欢的方式导入它,因为 JavaScript 知道您正在导入默认导出。
我们还可以通过使用星号并给出我们想要导入模块的名称来导入模块中的所有导出,即所有命名导出和默认导出。 *
导入的值等于包含所有导入值的对象。假设我想将整个模块导入为**math
**.
js
import * as math from "./math.js";
导入的值是**math
**对象的属性。
js
import * as math from "./math.js";
math.default(7, 8);
math.multiply(8, 9);
math.subtract(10, 3);
math.square(3);
在本例中,我们将从模块导入所有导出。 执行此操作时要小心,因为您最终可能会导入不必要的值。
使用 *
only 导入所有导出的值。模块私有的值在导入模块的文件中仍然不可用,除非您显式导出它们。
反应
使用 React 构建应用程序时,您经常需要处理大量组件。我们可以将这些组件分离到它们自己的文件中,而不是将所有这些组件写入一个文件中,本质上是为每个组件创建一个模块。
我们有一个基本的待办事项列表,包含一个列表 、列表项 、一个输入字段 和一个按钮。
js
import React from "react";
import { render } from "react-dom";
import { TodoList } from "./components/TodoList";
import "./styles.css";
render(
<div className="App">
<TodoList />
</div>,
document.getElementById("root")
);
我们只是将组件拆分到单独的文件中:
TodoList.js
对于**List
**组件Button.js
对于定制 **Button
**组件Input.js
对于定制 **Input
**组件。
在整个应用程序中,我们不想使用从库导入的默认值**Button
和组件。相反,我们希望通过向其文件中的对象中定义的自定义样式添加自定义样式来使用组件的自定义版本。现在,我们只需导入一次默认值和组件,添加样式,然后导出自定义组件,而不是每次在应用程序中导入默认值和组件并一遍又一遍地向其添加自定义样式。 Input
material-ui
styles
** Button
Input
Button
Input
js
import React from "react";
import { render } from "react-dom";
import { TodoList } from "./components/TodoList";
import "./styles.css";
render(
<div className="App">
<TodoList />
</div>,
document.getElementById("root")
);
style
请注意我们如何在 和 中 Button.js
调用一个对象 Input.js
。由于该值是模块范围的,因此我们可以重用变量名称,而不会冒名称冲突的风险。
动态导入
当导入文件顶部的所有模块时,所有模块都会先于文件的其余部分加载。在某些情况下,我们只需要根据某种条件导入一个模块。通过动态导入,我们可以按需导入模块。
js
import("module").then((module) => {
module.default();
module.namedExport();
});
// Or with async/await
(async () => {
const module = await import("module");
module.default();
module.namedExport();
})();
让我们动态导入**math.js
**前面段落中使用的示例。
仅当用户单击按钮时,该模块才会加载。
js
const button = document.getElementById("btn");
button.addEventListener("click", () => {
import("./math.js").then((module) => {
console.log("Add: ", module.add(1, 2));
console.log("Multiply: ", module.multiply(3, 2));
const button = document.getElementById("btn");
button.innerHTML = "Check the console";
});
});
/*************************** */
/**** Or with async/await ****/
/*************************** */
// button.addEventListener("click", async () => {
// const module = await import("./math.js");
// console.log("Add: ", module.add(1, 2));
// console.log("Multiply: ", module.multiply(3, 2));
// });
通过动态导入模块,我们可以减少页面加载时间。我们只需在用户需要时加载、解析和编译用户真正需要的代码。
除了能够按需导入模块之外,该**import()
** 函数还可以接收表达式。它允许我们传递模板文字,以便根据给定值动态加载模块。
js
import React from "react";
export function DogImage({ num }) {
const [src, setSrc] = React.useState("");
async function loadDogImage() {
const res = await import(`../assets/dog${num}.png`);
setSrc(res.default);
}
return src ? (
<img src={src} alt="Dog" />
) : (
<div className="loader">
<button onClick={loadDogImage}>Click to load image</button>
</div>
);
}
在上面的示例中,**date.js
仅当用户单击 "单击加载日期" 按钮时才会导入该模块。该 date.js
模块导入第三方 moment
模块,只有在 date.js
**模块加载时才会导入第三方模块。如果用户不需要显示日期,我们可以完全避免加载这个第三方库。
用户单击 "单击加载图像"按钮后,将加载 每个图像。图像是本地文件,根据我们传递给字符串 .png
的值加载。 num
javascript
const res = await import(`../assets/dog${num}.png`);
这样,我们就不再依赖于硬编码的模块路径。它为您根据用户输入、从外部源接收的数据、函数结果等导入模块的方式增加了灵活性。
通过模块模式,我们可以封装不应该公开的部分代码。这可以防止意外的名称冲突和全局范围污染,从而降低使用多个依赖项和命名空间的风险。为了能够在所有 JavaScript 运行时中使用 ES2015 模块,需要像Babel这样的转译器。