module-pattern

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.
随着应用和代码库规模的增长,保持代码的可维护性和分离性变得愈发重要。模块模式可以帮助你将代码拆分为更小的、可复用的片段。
除了能将代码拆分为更小的可复用片段外,模块还允许你将文件中的某些值设为_私有_。默认情况下,模块内的声明作用域是_封装_在该模块内的。如果我们没有显式导出某个值,该值在模块外部就无法访问。这降低了代码库其他部分声明的值发生命名冲突的风险,因为这些值不会出现在全局作用域中。

When to Use

使用场景

  • Use this when you need to organize code into maintainable, encapsulated units
  • This is helpful when you want to keep certain values private to a module and avoid global scope pollution
  • Use this to enable tree-shaking and reduce bundle sizes
  • 当你需要将代码组织为可维护、封装的单元时使用
  • 当你希望将某些值设为模块私有,避免污染全局作用域时,这种模式很有帮助
  • 使用它来启用 tree-shaking,减小打包体积

Instructions

使用说明

  • Use ES2015
    import
    /
    export
    syntax for module definitions
  • Use named exports for multiple values and default exports for the primary value of a module
  • Keep non-exported values private to reduce naming collision risks
  • Use dynamic
    import()
    for on-demand module loading to reduce initial bundle size
  • 使用ES2015的
    import
    /
    export
    语法定义模块
  • 多个值使用命名导出,模块的主要值使用默认导出
  • 将未导出的值设为私有,降低命名冲突风险
  • 使用动态
    import()
    实现按需加载模块,减小初始打包体积

Details

详细内容

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.
Let's look at an example of a module called
math.js
, containing mathematical functions.
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;
}
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.
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.
We can then import the values in another file 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.
js
import { add, multiply, subtract, square } from "./math.js";
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.
Let's create a value that should only be referenceable within the
math.js
file, called
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;
}
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!
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. In this case, we can rename the imported values, by using the
as
keyword.
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);
Besides named exports, you can also use a default export. You can only have one default export per module.
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;
}
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'
.
js
import add, { multiply, subtract, square } from "./math.js";

add(7, 8);
multiply(8, 9);
subtract(10, 3);
square(3);
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.
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.
js
import * as math from "./math.js";

math.default(7, 8);
math.multiply(8, 9);
math.subtract(10, 3);
math.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.
ES2015引入了内置的JavaScript模块。模块是包含JavaScript代码的文件,其行为与普通脚本有所不同。
让我们看一个名为
math.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;
}
我们有一个
math.js
文件,包含一些简单的数学逻辑。其中的函数支持用户对传入的值进行加法、乘法、减法运算,以及求平方。
为了让
math.js
中的函数能被其他文件访问,我们首先需要_导出_它们。要从模块中导出代码,可以使用
export
关键字。一种导出函数的方式是使用_命名导出_:只需在我们希望公开的部分前添加
export
关键字即可。
然后我们可以在另一个文件中使用
import
关键字导入这些值。为了让JavaScript知道要从哪个模块导入这些函数,我们需要添加
from
值以及模块的相对路径。
js
import { add, multiply, subtract, square } from "./math.js";
模块的一大优势是,我们_只能访问使用
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;
}
注意我们没有在
privateValue
前添加
export
关键字。由于我们没有导出
privateValue
变量,在
math.js
模块外部无法访问这个值!
通过将值设为模块私有,可以降低意外污染全局作用域的风险。你不必担心会意外覆盖使用你模块的开发者创建的同名值:这避免了命名冲突。
有时,导出的名称可能会与本地值冲突。这种情况下,我们可以使用
as
关键字_重命名_导入的值。
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);
}

/* 来自math.js模块 */
addValues(7, 8);
multiplyValues(8, 9);
subtract(10, 3);
square(3);

/* 来自index.js文件 */
add(8, 9, 2, 10);
multiply(8, 9, 2, 10);
除了命名导出,你还可以使用_默认导出_。每个模块只能有一个默认导出。
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知道这个值始终是默认导出的值,我们可以给导入的默认值起一个与导出时不同的名称。
我们还可以使用星号
*
导入模块的所有导出值,包括所有命名导出_和_默认导出,并指定我们想要用来引用该模块的名称。
js
import * as math from "./math.js";

math.default(7, 8);
math.multiply(8, 9);
math.subtract(10, 3);
math.square(3);
在这种情况下,我们导入了模块的_所有_导出值。这样做时要小心,因为你可能会不必要地导入一些值。
使用
*
只会导入所有导出的值。模块私有值在导入模块的文件中仍然不可用,除非你显式导出了它们。

React

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 can split components into separate files:
  • TodoList.js
    for the
    List
    component
  • Button.js
    for the customized
    Button
    component
  • Input.js
    for the customized
    Input
    component
Throughout the app, we don't want to use the default
Button
and
Input
component, imported from a 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.
Notice how we can 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.
使用React构建应用时,你经常需要处理大量组件。与其将所有组件写在一个文件中,我们可以将组件拆分到各自的文件中,本质上为每个组件创建一个模块。
我们可以将组件拆分到不同的文件中:
  • TodoList.js
    用于
    List
    组件
  • Button.js
    用于自定义的
    Button
    组件
  • Input.js
    用于自定义的
    Input
    组件
在整个应用中,我们不想使用从UI库导入的默认
Button
Input
组件。相反,我们希望使用自定义版本的组件,通过在它们的文件中定义的
styles
对象添加自定义样式。我们无需在应用中每次都导入默认的
Button
Input
组件并反复添加自定义样式,现在只需导入一次默认的
Button
Input
组件,添加样式后导出我们的自定义组件即可。
注意我们可以在
Button.js
Input.js
中都有一个名为
style
的对象。由于这个值是_模块作用域_的,我们可以重用变量名而不必担心命名冲突。

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.
js
import("module").then((module) => {
  module.default();
  module.namedExport();
});

// Or with async/await
(async () => {
  const module = await import("module");
  module.default();
  module.namedExport();
})();
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.
js
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.
当在文件顶部导入所有模块时,所有模块会在文件其余部分加载前加载。在某些情况下,我们只需要根据特定条件导入模块。通过动态导入,我们可以按需导入模块。
js
import("module").then((module) => {
  module.default();
  module.namedExport();
});

// 或者使用async/await
(async () => {
  const module = await import("module");
  module.default();
  module.namedExport();
})();
通过动态导入模块,我们可以减少页面加载时间。我们只需要在用户真正需要的时候加载、解析和编译他们所需的代码。
除了能够按需导入模块外,
import()
函数还可以接收表达式。它允许我们传入模板字面量,以便根据给定值动态加载模块。
js
const res = await import(`../assets/dog${num}.png`);
这样,我们就不必依赖硬编码的模块路径。这为你根据用户输入、从外部源接收的数据、函数结果等动态导入模块提供了灵活性。
使用模块模式,我们可以封装不应公开的代码部分。这可以防止意外的命名冲突和全局作用域污染,从而降低使用多个依赖和命名空间的风险。为了能在所有JavaScript运行时中使用ES2015模块,需要使用Babel这样的转译器。

Source

来源