关于 module-pattern
面向 JavaScript 开发者的模块模式指南,讲解如何拆分代码、封装私有值,并通过显式导入和导出共享功能。还介绍动态导入及模块化带来的维护和加载收益。
主要功能
- 01按模块拆分可维护代码
- 02通过非导出值实现封装
- 03使用命名和默认导出
- 04按需动态导入模块
- 05减少全局命名冲突
适用场景
- 01将大型代码库拆分为独立文件
- 02封装组件样式或内部值
- 03按需加载非首屏功能
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill module-pattern安装后在 Claude Code 中输入/module-pattern确认已加载。
详细介绍
模块模式
目录
随着应用和代码库不断增长,保持代码易于维护并划分清晰变得愈发重要。模块模式可以将代码拆分为更小、可复用的部分。
模块不仅能将代码拆分为更小的可复用部分,还能让某些值在文件中保持 私有。默认情况下,模块中的声明仅在该模块的作用域内(封装)。如果不显式导出某个值,该值在模块外部就不可用。由于这些值不会进入全局作用域,这可以降低代码库其他部分声明的值发生命名冲突的风险。
适用场景
- 需要将代码整理成易于维护、经过封装的单元时使用
- 希望将某些值限制为模块私有,并避免污染全局作用域时,此模式很有用
- 使用此模式启用摇树优化并减小打包体积
不适用场景
- 已支持 ES2015 原生模块的静态
import/export时——为获得更好的工具支持和摇树优化,优先使用静态导入 - 代码库已使用打包工具,却仅为了封装而采用基于 IIFE 的模块模式时
- 对于模块开销会带来不必要复杂度的简单脚本
说明
- 使用 ES2015
import/export语法定义模块 - 多个值使用命名导出,模块的主要值使用默认导出
- 将未导出的值保留为模块私有,以降低命名冲突风险
- 使用动态
import()按需加载模块,以减小初始打包体积
详情
ES2015 模块
ES2015 引入了内置的 JavaScript 模块。模块是包含 JavaScript 代码的文件,其行为与普通脚本略有不同。
来看一个名为 math.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 值和模块的相对路径。
import { add, multiply, subtract, square } from "./math.js";
使用模块的一大好处是,我们_只能访问通过 export 关键字显式导出的值_。没有通过 export 关键字显式导出的值,只能在该模块内部使用。
我们来创建一个只能在 math.js 文件中引用的值,名为 privateValue。
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 关键字_重命名_导入的值。
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 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'。
import add, { multiply, subtract, square } from "./math.js";
add(7, 8);
multiply(8, 9);
subtract(10, 3);
square(3);
由于 JavaScript 知道这个值始终是默认导出的值,因此我们可以为导入的默认值指定一个不同于导出时所用的名称。
我们还可以使用星号 * 导入模块中的所有导出内容,也就是所有命名导出_和_默认导出,并为导入的模块指定一个名称。
import * as math from "./math.js";
math.default(7, 8);
math.multiply(8, 9);
math.subtract(10, 3);
math.square(3);
在这种情况下,我们会导入模块中的_所有_导出内容。使用这种方式时要小心,因为可能会导入不必要的值。
使用 * 只会导入所有已导出的值。模块私有的值仍无法在导入该模块的文件中使用,除非你显式导出了这些值。
React
使用 React 构建应用时,你通常需要处理大量组件。我们不必把所有组件都写在一个文件中,而是可以将它们分别放在独立的文件中,实质上为每个组件创建一个模块。
我们可以将组件拆分到不同的文件中:
TodoList.js用于List组件Button.js用于自定义的Button组件Input.js用于自定义的Input组件
在整个应用中,我们不想使用从 UI 库导入的默认 Button 和 Input 组件。相反,我们想使用组件的自定义版本,在各自文件的 styles 对象中为其添加自定义样式。我们不必每次都在应用中导入默认的 Button 和 Input 组件并反复为它们添加自定义样式,现在只需导入一次默认的 Button 和 Input 组件,添加样式,然后导出自定义组件即可。
注意,我们可以在 Button.js 和 Input.js 中都使用名为 style 的对象。由于这个值是_模块作用域_的,我们可以重复使用变量名,而不必担心名称冲突。
动态导入
在文件顶部导入所有模块时,其他代码运行前就会加载所有模块。在某些情况下,我们只需要根据特定条件导入模块。使用动态导入,我们可以按需导入模块。
import("module").then((module) => {
module.default();
module.namedExport();
});
// Or with async/await
(async () => {
const module = await import("module");
module.default();
module.namedExport();
})();
通过动态导入模块,我们可以缩短页面加载时间。只有在用户需要时,我们才需要加载、解析和编译用户实际需要的代码。
除了能够按需导入模块,import() 函数还可以接收表达式。这样我们就可以传入模板字面量,根据给定的值动态加载模块。
const res = await import(`../assets/dog${num}.png`);
这样一来,我们就不再依赖硬编码的模块路径。它让你可以根据用户输入、从外部来源接收的数据、函数的结果等来导入模块,方式更加灵活。
使用模块模式,我们可以封装不应公开的代码部分。这可以避免意外的名称冲突和全局作用域污染,从而降低与多个依赖项和命名空间协作时的风险。为了能在所有 JavaScript 运行时中使用 ES2015 模块,需要使用 Babel 等转译器。