Design Pattern 设计模式
Module Pattern 模块模式
As your application and codebase grow, it becomes increasingly important to keep your code maintainable and separated. The module pattern allows you to split up your code into smaller, reusable pieces.
随着应用程序和代码库的增长,保持代码的可维护性和独立性变得越来越重要。模块模式允许您将代码拆分为更小的、可重用的部分。
Besides being able to split your code into smaller reusable pieces, modules allow you to keep certain values within your file private. Declarations within a module are scoped (encapsulated) to that module , by default. If we don’t explicitly export a certain value, that value is not available outside that module. This reduces the risk of name collisions for values declared in other parts of your codebase, since the values are not available on the global scope.
除了能够将代码分割成更小的可重用片段之外,模块还允许您将文件中的某些值保持为私有。默认情况下,模块中的声明的作用域(封装)为该模块。如果我们没有显式导出某个值,则该值在该模块之外不可用。这可以降低代码库其他部分中声明的值发生名称冲突的风险,因为这些值在全局范围内不可用。
ES2015 Modules ES2015模块
ES2015 introduced built-in JavaScript modules. A module is a file containing JavaScript code, with some difference in behavior compared to a normal script.
ES2015引入了内置的JavaScript模块。模块是一个包含JavaScript代码的文件,与普通脚本相比在行为上有一些不同。
Let’s look at an example of a module called math.js
, containing mathematical functions.
让我们看一个名为math.js
的模块的例子,它包含数学函数。
1function add(x, y) {2 return x + y;3}4function multiply(x) {5 return x * 2;6}7function subtract(x, y) {8 return x - y;9}10function square(x) {11 return x * x;12}
We have a math.js
file containing some simple mathematical logic. We have functions that allow users to add, multiply, subtract, and get the square of values that they pass.
我们有一个math.js
文件,其中包含一些简单的数学逻辑。我们有一些函数,允许用户加、乘、减,并得到他们传递的值的平方。
However, we don’t just want to use these functions in the math.js
file, we want to be able to reference them in the index.js
file! Currently, an error gets thrown inside the index.js
file: there are no functions within the index.js
file called add
, subtract
, multiply
or square
. We are trying to reference functions that are not available in the index.js
file.
然而,我们不只是想在math.js
文件中使用这些函数,我们希望能够在index.js
文件中引用它们!目前,index.js
文件中会抛出一个错误:index.js
文件中没有名为加
、减
、乘
或平方的
函数。我们正在尝试引用index.js
文件中不可用的函数。
In order to make the functions from math.js
available to other files, we first have to export them. In order to export code from a module, we can use the export
keyword. One way of exporting the functions, is by using named exports: we can simply add the export
keyword in front of the parts that we want to publicly expose. In this case, we’ll want to add the export
keyword in front of every function, since index.js
should have access to all four functions.
为了使math.js
中的函数可用于其他文件,我们首先必须导出它们。为了从模块中导出代码,我们可以使用export
关键字。导出函数的一种方法是使用命名的导出:我们可以简单地在我们想要公开的部分前面添加export
关键字。在这种情况下,我们希望在每个函数前面添加export
关键字,因为index.js
应该可以访问所有四个函数。
1export function add(x, y) {2 return x + y;3}45export function multiply(x) {6 return x * 2;7}89export function subtract(x, y) {10 return x - y;11}1213export function square(x) {14 return x * x;15}
We just made the add
, multiply
, subtract
, and square
functions exportable! However, just exporting the values from a module is not enough to make them publicly available to all files. In order to be able to use the exported values from a module, you have to explicitly import them in the file that needs to reference them.
我们刚刚使加
、乘
、减
和平方
函数可导出!然而,仅仅从模块中导出值并不足以使它们对所有文件公开可用。为了能够使用从模块中导出的值,您必须在需要引用它们的文件中显式导入它们。
We have to import the values on top of the index.js
file, by using the import
keyword. To let javascript know from which module we want to import these functions, we need to add a from
value and the relative path to the module.
我们必须使用import
关键字导入index.js
文件顶部的值。为了让JavaScript知道我们想从哪个模块导入这些函数,我们需要添加一个from
值和模块的相对路径。
1import { add, multiply, subtract, square } from "./math.js";
We just imported the four functions from the math.js
module in the index.js
file! Let’s try and see if we can use the functions now!
我们刚刚从index.js
文件中的math.js
模块导入了四个函数!让我们试试看我们现在是否可以使用这些功能!
1function add(x, y) {2 return x + y;3}4function multiply(x) {5 return x * 2;6}7function subtract(x, y) {8 return x - y;9}10function square(x) {11 return x * x;12}
The reference error is gone, we can now use the exported values from the module!
引用错误消失了,我们现在可以使用从模块中导出的值了!
A great benefit of having modules, is that we only have access to the values that we explicitly exported using the export
keyword. Values that we didn’t explicitly export using the export
keyword, are only available within that module.
拥有模块的一个很大的好处是,我们只能访问使用export
关键字显式导出的值。我们没有使用export
关键字显式导出的值仅在该模块中可用。
Let’s create a value that should only be referencable within the math.js
file, called privateValue
.
让我们创建一个只能在math.js
文件中引用的值,名为privateValue
。
1const privateValue = "This is a value private to the module!";23export function add(x, y) {4 return x + y;5}67export function multiply(x) {8 return x * 2;9}1011export function subtract(x, y) {12 return x - y;13}1415export function square(x) {16 return x * x;17}
Notice how we didn’t add the export
keyword in front of privateValue
. Since we didn’t export the privateValue
variable, we don’t have access to this value outside of the math.js
module!
1import { add, multiply, subtract, square } from "./math.js";23console.log(privateValue);4/* Error: privateValue is not defined */
By keeping the value private to the module, there is a reduced risk of accidentally polluting the global scope. You don’t have to fear that you will accidentally overwrite values created by developers using your module, that may have had the same name as your private value: it prevents naming collisions.
Sometimes, the names of the exports could collide with local values.
1import { add, multiply, subtract, square } from "./math.js";23function add(...args) {4 return args.reduce((acc, cur) => cur + acc);5} /* Error: add has already been declared */67function multiply(...args) {8 return args.reduce((acc, cur) => cur * acc);9}10/* Error: multiply has already been declared */
In this case, we have functions called add
and multiply
in index.js
. If we would import values with the same name, it would end up in a naming collision: add
and multiply
have already been declared! Luckily, we can rename the imported values, by using the as
keyword.
Let’s rename the imported add
and multiply
functions to addValues
and multiplyValues
.
1import {2 add as addValues,3 multiply as multiplyValues,4 subtract,5 square6} from "./math.js";78function add(...args) {9 return args.reduce((acc, cur) => cur + acc);10}1112function multiply(...args) {13 return args.reduce((acc, cur) => cur * acc);14}1516/* From math.js module */17addValues(7, 8);18multiplyValues(8, 9);19subtract(10, 3);20square(3);2122/* From index.js file */23add(8, 9, 2, 10);24multiply(8, 9, 2, 10);
Besides named exports, which are exports defined with just the export
keyword, you can also use a default export. You can only have one default export per module.
Let’s make the add
function our default export, and keep the other functions as named exports. We can export a default value, by adding export default
in front of the value.
1export default function add(x, y) {2 return x + y;3}45export function multiply(x) {6 return x * 2;7}89export function subtract(x, y) {10 return x - y;11}1213export function square(x) {14 return x * x;15}
The difference between named exports and default exports, is the way the value is exported from the module, effectively changing the way we have to import the value.
Previously, we had to use the brackets for our named exports: import { module } from 'module'
.
With a default export, we can import the value without the brackets: import module from 'module'
.
1import add, { multiply, subtract, square } from "./math.js";23add(7, 8);4multiply(8, 9);5subtract(10, 3);6square(3);
The value that’s been imported from a module without the brackets, is always the value of the default export, if there is a default export available.
Since JavaScript knows that this value is always the value that was exported by default, we can give the imported default value another name than the name we exported it with. Instead of importing the add
function using the name add
, we can call it addValues
, for example.
1import addValues, { multiply, subtract, square } from "./math.js";23addValues(7, 8);4multiply(8, 9);5subtract(10, 3);6square(3);
Even though we exported the function called add
, we can import it calling it anything we like, since JavaScript knows you are importing the default export.
We can also import all exports from a module, meaning all named exports and the default export, by using an asterisk *
and giving the name we want to import the module as. The value of the import is equal to an object containing all the imported values. Say that I want to import the entire module as math
.
1import * as math from "./math.js";
The imported values are properties on the math
object.
1import * as math from "./math.js";23math.default(7, 8);4math.multiply(8, 9);5math.subtract(10, 3);6math.square(3);
In this case, we’re importing all exports from a module. Be careful when doing this, since you may end up unnecessarily importing values.
Using the *
only imports all exported values. Values private to the module are still not available in the file that imports the module, unless you explicitly exported them.
React
When building applications with React, you often have to deal with a large amount of components. Instead of writing all of these components in one file, we can separate the components in their own files, essentially creating a module for each component.
We have a basic todo-list, containing a list, list items, an input field, and a button.
1import React from "react";2import { render } from "react-dom";34import { TodoList } from "./components/TodoList";5import "./styles.css";67render(8 <div className="App">9 <TodoList />10 </div>,11 document.getElementById("root")12);
We just split our components in their separate files:
TodoList.js
for theList
componentButton.js
for the customizedButton
componentInput.js
for the customizedInput
component.
Throughout the app, we don’t want to use the default Button
and Input
component, imported from the material-ui
library. Instead, we want to use our custom version of the components, by adding custom styles to it defined in the styles
object in their files. Rather than importing the default Button
and Input
component each time in our application and adding custom styles to it over and over, we can now simply import the default Button
and Input
component once, add styles, and export our custom component.
1import React from "react";2import { render } from "react-dom";34import { TodoList } from "./components/TodoList";5import "./styles.css";67render(8 <div className="App">9 <TodoList />10 </div>,11 document.getElementById("root")12);
Notice how we have an object called style
in both Button.js
and Input.js
. Since this value is module-scoped, we can reuse the variable name without risking a name collision.
Dynamic import
When importing all modules on the top of a file, all modules get loaded before the rest of the file. In some cases, we only need to import a module based on a certain condition. With a dynamic import, we can import modules on demand.
import("module").then((module) => {
module.default();
module.namedExport();
});
// Or with async/await
(async () => {
const module = await import("module");
module.default();
module.namedExport();
})();
Let’s dynamically import the math.js
example used in the previous paragraphs.
The module only gets loaded, if the user clicks on the button.
1const button = document.getElementById("btn");23button.addEventListener("click", () => {4 import("./math.js").then((module) => {5 console.log("Add: ", module.add(1, 2));6 console.log("Multiply: ", module.multiply(3, 2));78 const button = document.getElementById("btn");9 button.innerHTML = "Check the console";10 });11});1213/*************************** */14/**** Or with async/await ****/15/*************************** */16// button.addEventListener("click", async () => {17// const module = await import("./math.js");18// console.log("Add: ", module.add(1, 2));19// console.log("Multiply: ", module.multiply(3, 2));20// });
By dynamically importing modules, we can reduce the page load time. We only have to load, parse, and compile the code that the user really needs, when the user needs it.
Besides being able to import modules on-demand, the import()
function can receive an expression. It allows us to pass template literals, in order to dynamically load modules based on a given value.
1import React from "react";23export function DogImage({ num }) {4 const [src, setSrc] = React.useState("");56 async function loadDogImage() {7 const res = await import(`../assets/dog${num}.png`);8 setSrc(res.default);9 }1011 return src ? (12 <img src={src} alt="Dog" />13 ) : (14 <div className="loader">15 <button onClick={loadDogImage}>Click to load image</button>16 </div>17 );18}
In the above example, the date.js
module only gets imported if the user clicks on the Click to load dates button. The date.js
module imports the third-party moment
module, which only gets imported when the date.js
module gets loaded. If the user didn’t need to show the dates, we can avoid loading this third-party library altogether.
Each image gets loaded after the user clicks on the Click to load image button. The images are local .png
files, which get loaded based on the value of num
that we pass to the string.
const res = await import(`../assets/dog${num}.png`);
This way, we’re not dependent on hard-coded module paths. It adds flexibility to the way you can import modules based on user input, data received from an external source, the result of a function, and so on.
With the module pattern, we can encapsulate parts of our code that should not be publicly exposed. This prevents accidental name collision and global scope pollution, which makes working with multiple dependencies and namespaces less risky. In order to be able to use ES2015 modules in all JavaScript runtimes, a transpiler such as Babel is needed.