编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

    • 基础入门

      • README
      • 入门介绍
      • 数据类型
      • 运算符
      • 函数
      • 面向对象
      • 标准库
      • 异步操作
      • 事件设计
      • 错误机制
      • 模块开发
        • 10.1 模块化概述
          • 10.1.1 模块化概念
          • 10.1.2 模块化的演进历史
        • 10.2 导出模块
          • 10.2.1 基本导出
          • 10.2.2 批量导出
          • 10.2.3 重新导出
          • 10.2.4 命名导出与默认导出的设计权衡
        • 10.3 导入模块
          • 10.3.1 导入使用
          • 10.3.2 类型导入
          • 10.3.3 动态导入
          • 10.3.4 注意事项
          • 10.3.5 导入的底层机制:活绑定
        • 10.4 CommonJS模块
          • 10.4.1 require和module.exports
          • 10.4.2 CommonJS与ESM的核心区别
          • 10.4.3 循环依赖的处理
        • 10.5 模块打包与构建
          • 10.5.1 Tree Shaking原理
          • 10.5.2 代码分割与懒加载
        • 10.6 模块设计模式
          • 10.6.1 模块的封装模式
          • 10.6.2 依赖注入
      • 字符串处理
      • 迭代器与生成器
      • Symbol
      • DOM操作
      • 网络请求
    • 综合案例

    • 专栏博客

  • CodeX
  • JavaScript入门
  • 基础入门
杨充
2025-11-27
目录

模块开发

# 10.模块开发

# 目录介绍

  • 10.1 模块化概述
    • 10.1.1 模块化概念
    • 10.1.2 模块化的演进历史
  • 10.2 导出模块
    • 10.2.1 基本导出
    • 10.2.2 批量导出
    • 10.2.3 重新导出
    • 10.2.4 命名导出与默认导出的设计权衡
  • 10.3 导入模块
    • 10.3.1 导入使用
    • 10.3.2 类型导入
    • 10.3.3 动态导入
    • 10.3.4 注意事项
    • 10.3.5 导入的底层机制:活绑定
  • 10.4 CommonJS模块
    • 10.4.1 require和module.exports
    • 10.4.2 CommonJS与ESM的核心区别
    • 10.4.3 循环依赖的处理
  • 10.5 模块打包与构建
    • 10.5.1 Tree Shaking原理
    • 10.5.2 代码分割与懒加载
  • 10.6 模块设计模式
    • 10.6.1 模块的封装模式
    • 10.6.2 依赖注入
    • 10.7.1 项目结构
    • 10.7.2 多个模块
    • 10.7.3 主类模块
    • 10.7.4 运行代码

# 10.1 模块化概述

export 是 JavaScript 模块化的核心语法,支持导出变量、函数、类等内容,并通过 import 在其他模块中使用。掌握 export 和 import 的使用可以更好地组织和管理代码。

# 10.1.1 模块化概念

模块化是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来。

JavaScript 中的 export 是 ES6(ECMAScript 2015)引入的模块化语法,用于从模块中导出变量、函数、类等,以便其他模块可以使用 import 导入这些内容。

模块化的核心价值:

  • 可维护性:每个模块职责单一,修改时影响范围小
  • 可复用性:模块可以在不同项目中复用
  • 命名空间隔离:避免全局变量污染
  • 依赖管理:模块之间的依赖关系明确

# 10.1.2 模块化的演进历史

疑惑:为什么 JavaScript 在诞生很久之后才有模块系统?

答疑:JavaScript 最初是为浏览器中简单的页面交互设计的,没有考虑大规模应用的需求。随着前端工程化的发展,社区先后提出了多种模块化方案。

论证——技术演变过程:

// ===== 阶段1:全局函数(原始时代)=====
// 所有函数和变量都在全局作用域中,极易冲突
var count = 0;
function increment() { count++; }

// ===== 阶段2:命名空间模式 =====
var MyApp = {};
MyApp.count = 0;
MyApp.increment = function() { MyApp.count++; };

// ===== 阶段3:IIFE模块模式 =====
var CounterModule = (function() {
  var count = 0; // 私有
  return {
    increment: function() { return ++count; },
    getCount: function() { return count; }
  };
})();

// ===== 阶段4:CommonJS(2009,Node.js)=====
// 同步加载,适合服务端
const fs = require('fs');
module.exports = { readConfig: () => {} };

// ===== 阶段5:AMD(2010,RequireJS)=====
// 异步加载,适合浏览器
define(['jquery'], function($) {
  return { init: function() {} };
});

// ===== 阶段6:UMD(兼容 CommonJS + AMD + 全局)=====
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define([], factory);
  } else if (typeof module === 'object') {
    module.exports = factory();
  } else {
    root.MyLib = factory();
  }
}(this, function() {
  return { name: 'MyLib' };
}));

// ===== 阶段7:ES Module(2015,语言标准)=====
// 静态结构,支持 Tree Shaking
import { readFile } from 'fs/promises';
export const config = { debug: false };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

结果展示:

阶段 方案 加载方式 适用环境 年代
1 全局变量 - 浏览器 1995
2 命名空间 - 浏览器 2002
3 IIFE - 浏览器 2008
4 CommonJS 同步 Node.js 2009
5 AMD 异步 浏览器 2010
6 UMD 兼容 通用 2011
7 ES Module 异步/静态 通用 2015

# 10.2 导出模块

# 10.2.1 基本导出

1.导出单个变量、函数或类

使用 export 关键字直接导出。

// module.js
export const name = 'Alice';
export function greet() {
  console.log('Hello!');
}
export class Person {
  constructor(name) {
    this.name = name;
  }
}
1
2
3
4
5
6
7
8
9
10

2.导出默认值

使用 export default 导出模块的默认值,一个模块只能有一个默认导出。

// module.js
const message = 'Hello, World!';
export default message;
1
2
3

# 10.2.2 批量导出

1.导出多个内容

在模块底部统一导出。

// module.js
const name = 'Alice';
function greet() {
  console.log('Hello!');
}
class Person {
  constructor(name) {
    this.name = name;
  }
}

export { name, greet, Person };
1
2
3
4
5
6
7
8
9
10
11
12

2.重命名导出

使用 as 关键字重命名导出的内容。

// module.js
const name = 'Alice';
function greet() {
  console.log('Hello!');
}

export { name as userName, greet as sayHello };
1
2
3
4
5
6
7

# 10.2.3 重新导出

1.重新导出

从一个模块中导入内容并立即导出,常用于创建"聚合模块"(barrel file)。

// index.js —— 聚合模块
export { name, greet } from './anotherModule.js';
export { default as Config } from './config.js';
export * from './utils.js'; // 重新导出所有命名导出

// 使用者只需导入聚合模块
import { name, greet, Config, formatDate } from './index.js';
1
2
3
4
5
6
7

2.重新导出默认值

重新导出另一个模块的默认值。

// module.js
export { default } from './anotherModule.js';
1
2

# 10.2.4 命名导出与默认导出的设计权衡

疑惑:什么时候用命名导出(export),什么时候用默认导出(export default)?

答疑:两种方式各有适用场景,但现代最佳实践倾向于优先使用命名导出。

论证:

// ===== 命名导出的优势 =====
// 1. 导入时有明确的名称,IDE 自动补全友好
import { formatDate, parseDate } from './date-utils.js';

// 2. 强制使用原始名称(除非显式 as 重命名),团队一致性好
// import { formatDate as fd } from './date-utils.js'; // 显式重命名

// 3. 支持 Tree Shaking —— 未使用的导出会被打包工具移除
export function used() { /* ... */ }
export function unused() { /* ... */ } // 不被导入则不打包

// ===== 默认导出的优势 =====
// 1. 导入时可以使用任意名称
import MyComponent from './component.js'; // 叫什么都行
import Comp from './component.js';

// 2. 适合"一个模块一个主要内容"的场景
export default class Button { /* ... */ }

// ===== 默认导出的问题 =====
// 1. 导入名称不一致,不同文件可能用不同名字导入同一个东西
// fileA.js: import Button from './Button';
// fileB.js: import Btn from './Button';    // 同一个东西,不同名字

// 2. 不利于自动重构 —— IDE 无法自动关联重命名

// 3. re-export 时语法不直观
// export { default as Button } from './Button'; // 较啰嗦
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

结果展示:推荐的导出策略——

场景 推荐方式 示例
工具函数集合 命名导出 export function formatDate() {}
类/组件(一个文件一个) 命名导出或默认导出均可 export class Button {}
常量/配置 命名导出 export const API_URL = '...'
聚合模块 命名导出 + re-export export { A } from './a'

# 10.3 导入模块

# 10.3.1 导入使用

普通导入,作用:导入实际的代码或值,会在编译后的 JavaScript 代码中包含这些内容。

使用场景:当你需要实际使用类、函数或变量时。

1.导入命名导出

使用 import { ... } from 'module' 导入命名导出。

// main.js
import { name, greet, Person } from './module.js';
console.log(name); // 输出: Alice
greet(); // 输出: Hello!
const person = new Person('Bob');
console.log(person.name); // 输出: Bob
1
2
3
4
5
6

2.导入默认导出

使用 import defaultExport from 'module' 导入默认导出。

// main.js
import message from './module.js';
console.log(message); // 输出: Hello, World!
1
2
3

3.导入所有内容

使用 import * as alias from 'module' 导入所有内容并绑定到一个对象。

// main.js
import * as module from './module.js';
console.log(module.name); // 输出: Alice
module.greet(); // 输出: Hello!
1
2
3
4

4.同时导入默认导出和命名导出

import React, { useState, useEffect } from 'react';
// React 是默认导出,useState/useEffect 是命名导出
1
2

5.仅执行模块(副作用导入)

import './polyfill.js'; // 只执行模块代码,不导入任何值
import './styles.css';  // Webpack 等打包工具支持导入 CSS
1
2

# 10.3.2 类型导入

作用:仅导入类型信息,不会在编译后的 JavaScript 代码中包含这些类型。

使用场景:当你只需要类型信息(例如定义 this 的类型)时。

类型导入 vs 普通导入。示例:

import type MainSession from "./index-session"; // 类型导入。只在编译时存在,运行时会被删除
import { handleQRRegister } from "./index-session"; // 普通导入(函数)。会导入实际的类和运行时代码
1
2

为什么使用类型导入

export async function handleQRRegister(
  this: MainSession,  // 这里使用 MainSession 作为 this 的类型
  qrCodeContent: string
): Promise<void> {
  // 函数体中可以访问 this.logger, this.sessionKey 等属性
  const logger = this.logger;
  // ...
}
1
2
3
4
5
6
7
8

函数绑定机制,在 index-session.ts 中,这个函数被绑定到类实例:

在 handleQRRegister 函数中,this 的类型被定义为 MainSession。

使用 call 方法将 this 绑定到 MainSession 的实例。

async handleQRRegister(qrCodeContent: string): Promise<void> {
  return handleQRRegister.call(this, qrCodeContent);  // 使用 call 绑定 this
}
1
2
3

# 10.3.3 动态导入

使用 import() 动态加载模块,返回一个 Promise。

// main.js
import('./module.js').then(module => {
  console.log(module.name); // 输出: Alice
  module.greet(); // 输出: Hello!
});

// 配合 async/await
async function loadModule() {
  const { name, greet } = await import('./module.js');
  console.log(name);
  greet();
}

// 条件加载
async function loadLocale(lang) {
  const module = await import(`./locales/${lang}.js`);
  return module.default;
}

// 路由懒加载(React)
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 10.3.4 注意事项

  1. 模块作用域:模块中的变量、函数、类默认是模块作用域,不会污染全局作用域。
  2. 严格模式:模块默认启用严格模式。
  3. 文件扩展名:在浏览器中使用模块时,文件扩展名必须是 .js 或 .mjs。
  4. 跨模块引用:模块路径可以是相对路径、绝对路径或模块名称(如 'lodash')。
  5. import 语句提升:import 语句会被提升到模块顶部,在任何代码执行前完成。
// import 语句会被提升
console.log(name); // "Alice"(不会报错,因为 import 被提升了)
import { name } from './module.js';

// 但 import() 动态导入不会被提升
const module = await import('./module.js'); // 按正常顺序执行
1
2
3
4
5
6

# 10.3.5 导入的底层机制:活绑定

疑惑:ES Module 导入的值是"拷贝"还是"引用"?

答疑:ES Module 的导入是活绑定(Live Binding)——导入的变量始终指向导出模块中的原始值。如果导出模块修改了变量,导入方也能看到最新值。

论证:

// counter.js
export let count = 0;
export function increment() {
  count++;
}

// main.js
import { count, increment } from './counter.js';
console.log(count); // 0

increment();
console.log(count); // 1 —— 能看到变化!这就是活绑定

// 注意:导入的变量是只读的
// count = 10; // TypeError: Assignment to constant variable
// 只能通过导出模块提供的函数来修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

与 CommonJS 的对比:

// CommonJS —— 值拷贝
// counter.js
let count = 0;
module.exports = {
  count,
  increment() { count++; }
};

// main.js
const { count, increment } = require('./counter.js');
console.log(count); // 0
increment();
console.log(count); // 0!! —— CommonJS 是值拷贝,看不到变化
1
2
3
4
5
6
7
8
9
10
11
12
13

结果展示:

特性 ES Module CommonJS
绑定类型 活绑定(引用) 值拷贝
导入变量 只读 可修改
加载时机 编译时(静态) 运行时(动态)
顶层 this undefined module.exports

# 10.4 CommonJS模块

# 10.4.1 require和module.exports

CommonJS 是 Node.js 使用的模块系统,使用 require() 导入和 module.exports 导出:

// math.js
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }

module.exports = { add, subtract };
// 或者逐个导出
// exports.add = add;
// exports.subtract = subtract;

// main.js
const { add, subtract } = require('./math');
console.log(add(1, 2)); // 3
1
2
3
4
5
6
7
8
9
10
11
12

module.exports vs exports 的陷阱:

// exports 是 module.exports 的引用
console.log(exports === module.exports); // true

// 可以给 exports 添加属性
exports.foo = 'bar'; // OK,等同于 module.exports.foo = 'bar'

// 但不能直接赋值 exports
exports = { foo: 'bar' }; // 错误!这会断开引用
// 此时 module.exports 仍然是 {}

// 正确做法
module.exports = { foo: 'bar' };
1
2
3
4
5
6
7
8
9
10
11
12

# 10.4.2 CommonJS与ESM的核心区别

// ===== CommonJS =====
// 1. 动态加载(运行时)
const mod = require(condition ? './a' : './b'); // 可以用变量

// 2. 同步加载
const fs = require('fs'); // 阻塞执行直到加载完成

// 3. 值拷贝
const { count } = require('./counter');
// count 是快照值,不会随源模块变化

// 4. 可以在任意位置 require
function foo() {
  if (someCondition) {
    const lib = require('heavy-lib'); // 条件加载
  }
}

// ===== ES Module =====
// 1. 静态加载(编译时)
import mod from './a'; // 不能用变量,必须是字面量路径

// 2. 异步加载
import { readFile } from 'fs/promises'; // 非阻塞

// 3. 活绑定
import { count } from './counter';
// count 始终反映源模块的最新值

// 4. import 必须在模块顶层
// if (condition) { import x from './x'; } // SyntaxError!
// 但可以用动态导入
if (condition) {
  const x = await import('./x');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 10.4.3 循环依赖的处理

疑惑:如果两个模块相互导入(A 导入 B,B 也导入 A),会发生什么?

答疑:CommonJS 和 ES Module 对循环依赖的处理方式不同。

论证:

// ===== CommonJS 的循环依赖 =====
// a.js
console.log('a 开始执行');
exports.done = false;
const b = require('./b');         // 此时跳转到 b.js 执行
console.log('在 a 中, b.done =', b.done);
exports.done = true;
console.log('a 执行完毕');

// b.js
console.log('b 开始执行');
exports.done = false;
const a = require('./a');         // a.js 还没执行完,拿到的是不完整的 exports
console.log('在 b 中, a.done =', a.done); // false(不完整值)
exports.done = true;
console.log('b 执行完毕');

// 执行 node a.js 输出:
// a 开始执行
// b 开始执行
// 在 b 中, a.done = false    ← 拿到了不完整的值!
// b 执行完毕
// 在 a 中, b.done = true
// a 执行完毕

// ===== ES Module 的循环依赖 =====
// a.mjs
import { bValue } from './b.mjs';
export const aValue = 'a';
console.log('在 a 中, bValue =', bValue); // 'b'(活绑定,最终能拿到正确值)

// b.mjs
import { aValue } from './a.mjs';
export const bValue = 'b';
console.log('在 b 中, aValue =', aValue); // undefined 或报错(取决于执行顺序)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

结果展示:ES Module 的活绑定在循环依赖中表现更好(最终能拿到正确值),但如果在变量初始化前就访问(TDZ),仍然会报错。最佳实践是尽量避免循环依赖。

# 10.5 模块打包与构建

# 10.5.1 Tree Shaking原理

Tree Shaking 是一种在打包时移除未使用代码的技术,只有 ES Module 支持。

疑惑:为什么 Tree Shaking 只能用于 ES Module?

答疑:Tree Shaking 依赖于 ES Module 的静态结构——import/export 在编译时就能确定依赖关系,打包工具可以分析出哪些导出被使用、哪些没有。CommonJS 的 require() 是动态的(可以用变量路径),无法在编译时分析。

论证:

// utils.js
export function formatDate(d) { /* ... */ }
export function formatNumber(n) { /* ... */ }
export function formatCurrency(n) { /* ... */ }

// main.js —— 只使用了 formatDate
import { formatDate } from './utils.js';
console.log(formatDate(new Date()));

// 打包后:formatNumber 和 formatCurrency 不会出现在最终代码中
1
2
3
4
5
6
7
8
9
10

影响 Tree Shaking 的因素:

// 1. 副作用代码无法被 Tree Shake
export function pure() { return 1; } // 纯函数,可以安全移除
export const result = calculate();    // 有副作用,不敢移除

// 2. package.json 的 sideEffects 字段
{
  "sideEffects": false,  // 告诉打包工具:本包的所有模块都没有副作用
  // 或者指定有副作用的文件
  "sideEffects": ["./src/polyfill.js", "*.css"]
}

// 3. import * 会阻碍 Tree Shaking(部分打包工具)
import * as utils from './utils';  // 不如具名导入
import { formatDate } from './utils'; // 更好
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 10.5.2 代码分割与懒加载

代码分割(Code Splitting):将应用代码拆分为多个 chunk,按需加载。

// 路由级别的代码分割(React)
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// 条件加载重型依赖
async function exportToExcel(data) {
  // xlsx 库只在用户点击"导出"时才加载
  const XLSX = await import('xlsx');
  const workbook = XLSX.utils.json_to_sheet(data);
  // ...
}

// Webpack 魔法注释
const Chart = lazy(() => import(
  /* webpackChunkName: "chart" */
  /* webpackPreload: true */
  './components/Chart'
));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 10.6 模块设计模式

# 10.6.1 模块的封装模式

// 模式1:工具函数集合(命名导出)
// date-utils.js
export function formatDate(date, format) { /* ... */ }
export function parseDate(str) { /* ... */ }
export function addDays(date, days) { /* ... */ }

// 模式2:类模块(默认导出)
// logger.js
class Logger {
  #level;
  constructor(level = 'info') { this.#level = level; }
  log(msg) { /* ... */ }
  error(msg) { /* ... */ }
}
export default Logger;

// 模式3:工厂模块
// database.js
let instance = null;
export function getDatabase(config) {
  if (!instance) {
    instance = new Database(config);
  }
  return instance;
}

// 模式4:常量模块
// constants.js
export const API_BASE = 'https://api.example.com';
export const MAX_RETRIES = 3;
export const TIMEOUT = 5000;

// 模式5:桶文件(Barrel file)—— 聚合导出
// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';
export { Modal } from './Modal.js';
// 使用者只需:import { Button, Input, Modal } from './components';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 10.6.2 依赖注入

// 不好的做法:模块内部硬编码依赖
import { PostgresDB } from './postgres.js';

export class UserService {
  constructor() {
    this.db = new PostgresDB(); // 硬依赖,无法测试
  }
}

// 好的做法:依赖注入
export class UserService {
  constructor(db) {
    this.db = db; // 依赖从外部传入
  }
  
  async getUser(id) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

// 生产环境
import { PostgresDB } from './postgres.js';
const service = new UserService(new PostgresDB());

// 测试环境
const mockDB = { query: () => ({ id: 1, name: 'Test' }) };
const service = new UserService(mockDB);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
上次更新: 2026/06/10, 11:13:41
错误机制
字符串处理

← 错误机制 字符串处理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式