普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月28日首页

前端5分钟技术转全栈!orpc + cloudflare 项目实践

2026年1月28日 17:57

前言

标题党了一下哈哈,全栈还是有很多知识要学习的,这里只是简单介绍一套我觉得还不错的技术栈,希望对大家有帮助。

大前提: 请先注册一个 cloudflare 账户。5块钱买个域名绑定到 cloudflare 上,可以参考我之前的文章。

快速创建项目

我在 GitHub 上偶然发现了一个很有意思的项目,想推荐给大家: www.better-t-stack.dev/new

它提供了一个可视化页面,可以自由选择自己需要的技术栈,并自动生成初始化命令,一行命令就能创建完整项目。即使对技术栈不太熟悉,也可以直接使用社区中使用人数较多的预设配置,例如左下角提供的 T3 Stack、PERN Stack 等方案。同时还支持在线预览项目的文件结构,整体体验做得相当成熟。

这次我选择的是前后端统一部署到 Cloudflare,运行时使用 Cloudflare Workers,前后端之间通过 oRPC 通信。对于技术栈中一些不太熟悉的部分,我会在后文单独说明。通过复制生成的命令行即可完成项目初始化,最终得到的是一个前后端共存的 monorepo 项目结构,使用 Turbo 统一管理前后端的运行、调试和构建流程。

前后端代码全部采用 TypeScript 编写,对前端同学非常友好,上手成本也很低。(题外话: 我觉得写 JS / TS真幸福,应了那句话: 能用 JS 写的最后都用 JS 写😂)

pnpm create better-t-stack@latest cf-todo --frontend tanstack-router --backend hono --runtime workers --api orpc --auth none --payments none --database sqlite --orm drizzle --db-setup d1 --package-manager pnpm --git --web-deploy cloudflare --server-deploy cloudflare --install --addons biome turborepo --examples todo

运行项目

我们先把项目运行起来再看看代码。根据提示先运行一些Database的命令。pnpm run db:generate

然后启动本地看看效果 pnpm run dev (注: 如果项目运行不起来,尝试升级一下pnpm版本,这个monorepo借助了pnpm-workspace的特性,一些 catalog 可能需要 pnpm 高版本才支持。)

可以看到项目运行成功分别在本地 3001、3002端口,作者贴心的展示了一个TODO list示例。

了解代码

文件结构

很清爽的文件结构,apps下有前后端,然后packages下是一些共用的内容,根目录是格式化的biome与pnpm与turbo的配置。

Pnpm-workspace

www.pnpm.cn/catalogs 使用了catalogs 方便在各个子包中共享同一个npm包避免重复安装。

然后在 /apps/web/package.json/apps/server/package.json 我们就可以看到使用根目录的catalogs了。

www.pnpm.cn/workspaces workspaces 方便引用同一工作区的其他子模块的文件。

Hono

Hono 是一个轻量级的 Web 框架,主要面向 JavaScript 和 TypeScript 生态。它的设计目标是高性能、低开销,以及在不同运行时环境中的一致体验

官网: hono.dev/ ,常用与于 cloudflare 想结合出现。nodejs backend 框架也是多种多样的,选择自己喜欢的即可。

ORPC

orpc 简单讲就是 trpc 的进化版。

trpc可能有的同学也不是很了解,引用自官网的介绍 快速迭代,无忧无虑。轻松构建端到端类型安全的 API

说人话就是前后端共用一套TS,然后前端调用接口就可以直接以函数调用的方式访问后端接口,前端可以直接获得后端暴露的 API 类型定义。不用传统的前后端联调,后端给openapi文档,前端生成对应的TS接口响应入参与出参了,直接一套前端要调用接口直接点出来。

orpc 就是在 trpc 的基础上进行改造,官网: orpc.dev/docs/gettin… oRPC(开放 API 远程过程调用)结合了 RPC(远程过程调用)与 OpenAPI,允许您通过类型安全的 API 定义和调用远程(或本地)过程,同时遵循 OpenAPI 规范。 可以在享受trpc的同时生成openapi规范的文档。

访问我们这个示例项目的 http://localhost:3000/api-reference 就可以看到一个美观的openapi规范的在线接口示例

我们来看orpc有多方便。

packages\api\src\routers\todo.ts 接口的定义

apps\web\src\routes\todos.tsx 前端界面直接调用点出来要使用的函数。然后用 @tanstack/react-query 相关的hook进行处理,

orpc也可以很好的与tanstack-query相结合。这样子直接开发个人项目的时候就免去了接口联调的麻烦,写完后端前端直接调用。

drizzle

drizzle orm 就是方便你来增删改查数据的,在没有这些ORM的时候需要直接写SQL语句执行,有了这些ORM他们封装了一些方法让你更轻松的掌控数据。然后实在复杂的SQL也可以自定义。

node的ORM选择有蛮多的 选自己喜欢的或者大家推荐比较多的即可。

Alchemy

这个东西我也是第一次见到,我直接在bing搜还搜不到 需要加一些关键词,但感觉做的还不错,这里是官网: alchemy.run/what-is-alc… 简单讲就是帮你管理一些部署用的基础设施信息,我这里是部署到 cloudflare,然后有一个 alchemy.run.ts 文件就是管理全部部署到 cloudflare相关的事情。

如果你尝试 pnpm run deploy 部署这个项目的时候会发现运行不成功,因为你还没有给本地的 alchemy 授权你的 cloudflare 账户。

npm i -g alchemy安装完成后 alchemy configure 会打开一个授权页授权到本地即可。如果打不开或授权失败,请尝试打开vpn的 TUN 模式试下。

Turborepo

Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。它专为扩展单体仓库而设计,也能加速单包工作区中的工作流。

vercel开源的一个项目,常用于处理 monorepo 类型仓库的构建。

Cloudflare

我们这个项目前后端都部署到 cf 的 woker上,有很多的免费额度,cf真是大善人。然后数据存储的数据库使用的也是 cf 的D1数据库,个人MVP的项目初期应该够用了,升级了也不贵,不愧是赛博佛祖。

项目部署

授权完成后,我们运行 pnpm run deploy 试一下。可以看到给出了前后端两个地址。

访问前端后发现,我靠这么请求的是localhost:3000, 原来我们前后端的 .env 文件都没进行修改。

在后端的根目录创建一个.env.dev文件,CORS_ORIGIN 填写前端访问的地址。

CORS_ORIGIN=[填写前端地址]

前端创建一个 .env.dev文件

VITE_SERVER_URL=[填写后端地址]

然后添加一个 pnpm run deploy:dev 命令在根目录的 package.json 文件,表示我们要对 env.dev 环境变量的内容进行打包构建。复制上一行的 deploy 命令基础上再添加了一个 ALCHEMY_ENV=dev 的标识。

"deploy:dev": "cross-env ALCHEMY_ENV=dev turbo -F @cf-todo/infra deploy",

再修改 alchemy.run.ts 文件,根据环境变量读取对应的env文件

import alchemy from "alchemy";
import { D1Database, Vite, Worker } from "alchemy/cloudflare";
import { config } from "dotenv";

const mode = process.env.ALCHEMY_ENV ?? process.env.NODE_ENV ?? "development";
const loadEnv = (path: string, override = false) => {
  config({ path, override });
};

loadEnv("./.env");
loadEnv(`./.env.${mode}`, true);
loadEnv("../../apps/web/.env");
loadEnv(`../../apps/web/.env.${mode}`, true);
loadEnv("../../apps/server/.env");
loadEnv(`../../apps/server/.env.${mode}`, true);

const app = await alchemy("cf-todo");

const db = await D1Database("database", {
  migrationsDir: "../../packages/db/src/migrations",
});

export const web = await Vite("web", {
  cwd: "../../apps/web",
  assets: "dist",
  bindings: {
    VITE_SERVER_URL: alchemy.env.VITE_SERVER_URL!,
  },
});

export const server = await Worker("server", {
  cwd: "../../apps/server",
  entrypoint: "src/index.ts",
  compatibility: "node",
  bindings: {
    DB: db,
    CORS_ORIGIN: alchemy.env.CORS_ORIGIN!,
  },
  dev: {
    port: 3000,
  },
});

console.log(`Web    -> ${web.url}`);
console.log(`Server -> ${server.url}`);

await app.finalize();

最后还需要在turbo.json 文件添加下构建传递的这个标识变量。

"deploy": {
  "cache": false,
  "env": ["ALCHEMY_ENV"]
},

重新部署

先运行 pnpm run destory 销毁之前的资源。再执行 pnpm run deploy:dev。nice 可以使用了,美滋滋~

结语

现在,一个简单完整的前后端项目已经准备好,并且可以零成本完成部署,已经交到你手里了。接下来就可以充分发挥想象力和创造力,去打磨一个让人眼前一亮的产品。你也可以选择继续深入研究这个项目,从中学习和理解当前主流的工程化实践与技术选型思路。希望对大家有帮助!

改造部署文件参考: github.com/LLmoskk/orp…

最后,感恩Cloudflare!感恩 better-t-stack.dev/new 项目!

Node.js 深度进阶——多核突围:Worker Threads 与多进程集群

2026年1月28日 09:48

在《Node.js 深度进阶》的第三篇,我们要解决 Node.js 面对 CPU 密集型任务时的先天短板。

由于 V8 引擎的设计,Node.js 主线程是一个单线程环境。如果你在主线程里跑一个耗时 2 秒的加解密算法或大规模图像压缩,整个服务器在这 2 秒内将无法响应任何其他请求。

为了突破这个限制,我们需要开启“多核模式”。Node.js 提供了两种完全不同的方案:多进程(Cluster)与多线程(Worker Threads)


一、 多进程集群(Cluster):横向扩展的防弹衣

这是 Node.js 最早、也是最稳健的多核方案。它的核心逻辑是:复制多个完全独立的进程,每个进程跑在不同的 CPU 核心上。

1. 架构逻辑:句柄传递

  • Master 进程: 不处理业务逻辑,只负责监控 Worker 进程的状态和分发网络请求。
  • Worker 进程: 独立的 V8 实例,拥有独立的内存空间。
  • 负载均衡: Master 进程通过 Round-Robin(轮询) 算法将客户端连接分发给不同的 Worker。

2. 适用场景:高并发 Web 服务

由于进程间内存隔离,一个 Worker 崩溃不会导致整个服务宕机。这是生产环境下提高可用性的首选。

  • 生产工具: 实际开发中,我们通常直接使用 PM2。它底层封装了 Cluster 模块,提供了自动重启、负载均衡和性能监控。

二、 多线程(Worker Threads):纵向深挖的利剑

直到 Node.js v10.5.0,我们才拥有了真正的多线程。与多进程不同,多线程运行在同一个进程内。

1. 架构逻辑:共享内存

  • Isolate 隔离: 每个线程依然有自己的 V8 Isolate 和事件循环,但它们可以共享底层的物理内存。

  • 零拷贝通讯(SharedArrayBuffer): 这是榨干性能的关键。

    • 在多进程中,进程通信(IPC)需要序列化和反序列化数据,非常耗时。
    • 在多线程中,你可以使用 SharedArrayBuffer 让多个线程直接读写同一块二进制内存,实现零拷贝传输

2. 适用场景:CPU 密集型计算

  • 图像/视频处理(如生成缩略图)。
  • 大规模数据解析(如解析数 GB 的 JSON/CSV)。
  • 复杂的加密/解密逻辑。

三、 深度对比:该选哪种“突围”方式?

特性 多进程 (Cluster) 多线程 (Worker Threads)
内存占用 高(每个进程都要一套完整的 V8 运行时) 较低(共享部分内存和底层库)
通讯开销 高(IPC 序列化,适合传小消息) 极低(可实现内存共享,适合处理大数据)
隔离性 极强(进程崩溃互不影响) 较弱(内存共享可能导致竞态,需要加锁)
启动速度 慢(需要启动新操作系统进程) 快(启动新的线程上下文)

四、 实战:利用 Worker Threads 处理大数据

作为 8 年全栈,当你在处理耗时计算时,应该这样写:

JavaScript

// main.js
const { Worker } = require('worker_threads');

function runService(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData: data });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

// 这样主线程的 Event Loop 依然可以处理其他用户请求
runService({ task: 'image-compress', buffer: bigBuffer }).then(console.log);

💡 给前端开发者的硬核贴士

  • 不要滥用多线程: 创建线程本身是有开销的。如果任务执行时间小于 10ms,开启线程的开销可能比直接执行还要大。建议使用 线程池(Thread Pool) 模式。
  • 状态同步: 使用多线程共享内存时,必须注意原子性(Atomics) 。Node.js 提供了 Atomics 对象来确保在多个线程操作同一块内存时不会发生冲突。

结语

多核突围,本质上是空间换时间隔离换稳定的权衡。对于 Web 接入层,用 Cluster 提升吞吐量;对于计算密集层,用 Worker Threads 提升单次处理速度。

Node.js 深度进阶——超越事件循环:Libuv 线程池与异步瓶颈

2026年1月28日 09:47

在《Node.js 深度进阶》的第二篇,我们要打破“单线程”的思维幻觉。

很多开发者认为 Node.js 异步就是靠事件循环(Event Loop),但在高并发和复杂 I/O 场景下,Libuv 线程池才是那个在后台默默干脏活累活、决定系统吞吐量上限的“影子武士”。


一、 谁在干重活?Libuv 线程池的真相

Node.js 的主线程只负责执行 JavaScript 代码和分发任务。对于那些无法实现非阻塞 OS 异步的任务,Libuv 会将其扔进一个内部线程池中执行。

1. 默认“四壮汉”与瓶颈

默认情况下,Libuv 线程池只有 4 个线程

  • 主要受众: 文件系统操作(fs)、加密运算(crypto)、压缩(zlib)以及 DNS 查询(dns.lookup)。
  • 瓶颈场景: 如果你并发读取 10 个超大文件,或者同时计算 10 个复杂的 scrypt 哈希,前 4 个任务会占满线程池,剩下 6 个只能在队列里排队。主线程虽然闲着,但 I/O 已经卡死了。

2. 网络 I/O 的特殊待遇

值得注意的是,网络套接字(Sockets)通常不进入线程池。Libuv 利用了 OS 原生的多路复用技术(如 Linux 的 epoll、Windows 的 IOCP),这是 Node.js 能支持上万个并发网络连接的底层秘诀。


二、 深度调优:如何“榨干”多核性能

1. 扩充线程池:UV_THREADPOOL_SIZE

在处理大量文件或加密任务时,默认的 4 线程往往不够。

  • 策略: 你可以通过环境变量增加线程数(最大 1024)。

Bash

# 启动时根据 CPU 核心数调整,通常设为核数的 2-4 倍比较均衡
UV_THREADPOOL_SIZE=8 node server.js
  • 注意: 并不是越多越好。过多的线程会导致**上下文切换(Context Switching)**开销激增,反而降低效率。

2. 区分任务:Worker Threads vs Libuv

作为资深全栈,你要区分两类“耗时任务”:

  • I/O 密集型: 调优 UV_THREADPOOL_SIZE
  • CPU 密集型(如图像处理、大规模计算): 应该使用 worker_threads 模块创建独立的 JS 执行环境,避免 Libuv 的 C++ 线程池被 JS 逻辑拖慢。

三、 微观瓶颈:process.nextTick 的“霸权”

在 Event Loop 中,并不是所有异步都“玩得公平”。

1. 饿死事件循环(I/O Starvation)

process.nextTick 并不属于 Event Loop 的任何阶段,它属于 Microtask Queue

  • 执行优先级: 只要当前操作完成,主线程会立即清空所有的 nextTick 队列,只有清空后才会继续 Event Loop 的下一阶段。
  • 风险: 如果你递归调用 process.nextTick,主线程会永远留在这个队列里。Event Loop 会被彻底卡死,任何磁盘 I/O 或网络请求都无法被响应。

2. setImmediate:公平竞争的绅士

相比之下,setImmediate 运行在 Event Loop 的 Check 阶段。它允许 I/O 轮询先行,因此不会饿死事件循环,是处理非紧急异步逻辑的首选。


四、 性能侦探:监控 Event Loop 延迟

高并发场景下,我们必须监控 Event Loop Lag(事件循环延迟)。

  • 诊断: 如果 Lag 持续超过 50ms,说明你的主线程被长任务卡住了,或者微任务队列堆积。
  • 工具推荐: 使用 clinic.js doctor 或原生 perf_hooks 模块。

JavaScript

const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
// 定时打印直方图数据,分析 99 分位延迟
setInterval(() => console.log(`Lag: ${h.mean / 1e6}ms`), 5000);

💡 结语

超越事件循环,意味着你要从“代码怎么写”进阶到“系统怎么转”。调整 UV_THREADPOOL_SIZE、避开 nextTick 陷阱、监控主线程延迟,是你作为高级全栈在应对极端高并发时的“三板斧”。

昨天以前首页

🚀 深度解析:JS 数组的性能黑洞与 V8 引擎的“潜规则”

2026年1月27日 17:35

一、那个让服务器 CPU 飙升 100% 的“...”

上周五下午 4:50,正当我准备收工去吃火锅时,告警群突然炸了。某核心微服务的 CPU 占用率瞬间从 15% 飙升到 100%,内存也在疯狂抖动。

定位代码后,我发现了一行看起来人畜无害的代码:

// 为了合并三个从数据库查出来的结果集(每个约 5 万条数据)
const combinedData = [...largeArray1, ...largeArray2, ...largeArray3];

在开发环境下一切正常,但在高并发、大数据量的生产场景下,这一行代码直接成了“性能杀手”。

为什么? 很多人觉得 ES6 的扩展运算符(Spread Operator)只是 Array.prototype.concat 的语法糖,但实际上,V8 对它们的处理逻辑有着天壤之别。


二、V8 引擎的“潜规则”:数组的几种形态

在 V8 引擎内部,数组并不是简单的线性表。为了极致的性能,V8 会根据数组存储的内容动态切换存储模式。如果你不了解这些,你的代码可能正在悄悄拖慢整个系统的速度。

1. Packed vs Holey (连续 vs 有洞)

这是数组性能的分水岭。

  • Packed (连续数组):数组中所有的索引都有值。V8 可以直接计算偏移量,性能接近 C++ 数组。
  • Holey (有洞数组):数组中存在缺失的索引(例如 const arr = [1, , 3])。一旦数组变“洞”,V8 就必须在原型链上进行查找,甚至退化到“字典模式”,性能骤降。

避坑案例:千万不要用 delete arr[0] 来删除元素,这会产生一个永久的“洞”。请务必使用 splice

2. Smi -> Double -> Elements (类型演化)

  • Smi (Small Integer):存储的是小整数,这是最快的一种模式。
  • Double:一旦你往数组推入一个浮点数,数组就会演变为 Double 模式,涉及到“装箱/拆箱”开销。
  • Elements:一旦推入对象或混合类型,性能最慢。

重点:这种演化是不可逆的。即使你把对象删掉,数组依然会保留在 Elements 模式。


三、性能大 PK:ES5 方法 vs ES6 新特性

1. 扩展运算符 (...) vs Array.concat

回到开头的事故案例。为什么 [...a, ...b] 慢?

  • 扩展运算符 (...):它本质上是调用数组的迭代器(Iterator)。V8 必须逐个遍历元素并推入新数组,这涉及到大量的函数调用和迭代开销。
  • Array.prototype.concat:它是高度优化的内置方法。V8 内部可以直接进行内存块拷贝 (Memcpy),完全不经过 JavaScript 层的迭代。

实测数据:在处理 10 万级数据合并时,concatspread 快了近 3 倍,且内存峰值更低。

2. for vs forEach vs for...of

  • for 循环:永远的王者,没有任何额外开销。
  • forEach (ES5):带回调函数。早期由于闭包和函数调用开销确实慢,但现代 V8 通过 Inlining (内联优化),在大多数场景下已经能和 for 循环平起平坐。
  • for...of (ES6):基于迭代器。虽然语法优美,但在极高性能要求的循环中,迭代器的创建和 next() 调用依然存在细微开销。

3. find (ES6) vs filter (ES5)

如果你只需要找一个元素,永远不要用 filter().length

  • find()短路操作,找到即停。
  • filter() 会完整遍历数组并创建一个中间数组,浪费 CPU 和内存。

四、如何编写“高性能”的数组代码?

作为一名资深工程师,建议你在核心链路遵循以下原则:

1. 预分配数组空间

如果你预先知道数组的大小,直接 new Array(size) 比不断 push 要快。不断 push 会触发 V8 的动态扩容逻辑,导致内存重新分配和数据迁移。

2. 保持数组的“纯净度”

const arr = [1, 2, 3]; // Smi 模式,极速
arr.push(4.5);         // 退化为 Double 模式
arr.push('oops');      // 退化为 Elements 模式,性能滑坡

尽量让数组内部存储同类型的数据,尤其是避免在高性能循环中混合数字和对象。

3. 大数据合并禁用 Spread

在 React/Redux 的 reducer 中,我们习惯了 return [...state, newItem]。如果 state 只有几十个元素没问题,但如果是上万条记录的列表,请改用 concat 或先 push 再返回。


五、总结

性能优化不是为了“卷”语法,而是为了理解底层逻辑。

  1. 小规模数据:语义清晰最重要,大方使用 ES6 扩展符和 for...of
  2. 大规模数据 (万级以上):回归 for 循环与 concat,警惕迭代器开销。
  3. 核心库开发:必须关注 Packed/Holey 状态,确保数组在 V8 内部保持最快路径。

那天凌晨三点,当我把 spread 改回 concat 后,CPU 监控曲线瞬间恢复了平滑,我也终于赶上了那顿火锅。


「iDao 技术魔方」—— 聚焦 AI、前端、全栈矩阵,让技术落地更有深度。

几种依赖注入的使用场景 - InversifyJS为例

作者 irises
2026年1月27日 11:12

依赖注入不仅仅是一个让代码看起来“高级”的工具,它的核心价值在于解耦。通过将对象的“创建权”从业务逻辑中剥离并交给容器,我们能获得极高的灵活性。

关于依赖注入相关概念可参考依赖注入的艺术:编写可扩展 JavaScript 代码的秘密

以下是依赖注入最具代表性的五个使用场景。


1. 单元测试 (Unit Testing)

痛点: 当业务类直接 new 依赖时,测试该类就必须执行依赖的真实逻辑(如真实扣款、真实写库),导致测试缓慢且危险。

❌ 方式 A:不使用 InversifyJS (强耦合)

OrderService 内部强行依赖了 RealPayment。要测试 checkout 方法,你必须真的发起支付,无法轻松 Mock。

TypeScript

// 具体的支付实现
class RealPayment {
    pay(amount: number) {
        console.log(`$$$ 调用银行接口扣款: ${amount}`); // 真实副作用
    }
}

class OrderService {
    private payment: RealPayment;

    constructor() {
        // 😱 致命缺陷:硬编码依赖,测试时无法替换!
        this.payment = new RealPayment();
    }

    checkout(amount: number) {
        this.payment.pay(amount);
    }
}

✅ 方式 B:使用 InversifyJS (依赖抽象)

业务类只依赖接口 IPayment。在单元测试中,我们可以通过容器绑定一个 MockPayment,轻松隔离副作用。

TypeScript

// 1. 定义接口
interface IPayment { pay(amount: number): void; }

// 2. 业务逻辑 (只依赖接口)
@injectable()
class OrderService {
    constructor(
        @inject(TYPES.Payment) private payment: IPayment // 注入接口
    ) {}

    checkout(amount: number) {
        this.payment.pay(amount);
    }
}

// --- 单元测试文件 spec.ts ---
const testContainer = new Container();

// 🧪 测试时:绑定 Mock 实现
const mockPayment = { 
    pay: jest.fn() // 使用 Jest 等测试库的 Mock 函数
}; 
testContainer.bind(TYPES.Payment).toConstantValue(mockPayment);
testContainer.bind(OrderService).toSelf();

const service = testContainer.get(OrderService);
service.checkout(100);

// 断言:验证是否调用了 mock 方法,而不是真的扣款
expect(mockPayment.pay).toHaveBeenCalledWith(100);

2. 可替换的组件 (Swappable Components)

痛点: 同一个接口有多种实现(例如:存储策略既有本地存储,又有云存储)。传统写法通常伴随着大量的 if-else 或工厂模式代码。

❌ 方式 A:不使用 InversifyJS (工厂模式/条件判断)

调用者需要知道具体的实现类,且扩展新策略时需要修改工厂代码。

TypeScript

class LocalStorage { save() { console.log("存硬盘"); } }
class CloudStorage { save() { console.log("存 AWS S3"); } }

class FileManager {
    private storage: any;

    constructor(type: string) {
        // 😱 违反开闭原则:每次加新策略都要改这里
        if (type === 'local') {
            this.storage = new LocalStorage();
        } else {
            this.storage = new CloudStorage();
        }
    }
}

✅ 方式 B:使用 InversifyJS (命名绑定)

使用 @named 标签,可以在不修改业务逻辑代码的情况下,灵活注入不同的策略。

TypeScript

@injectable()
class FileManager {
    constructor(
        // ✨ 优雅:同时注入两种策略,按需使用
        @inject(TYPES.Storage) @named("local") private local: IStorage,
        @inject(TYPES.Storage) @named("cloud") private cloud: IStorage
    ) {}

    backup() {
        this.local.save(); // 先存本地
        this.cloud.save(); // 再存云端
    }
}

// --- 容器配置 ---
container.bind<IStorage>(TYPES.Storage).to(LocalStorage).whenTargetNamed("local");
container.bind<IStorage>(TYPES.Storage).to(CloudStorage).whenTargetNamed("cloud");

3. 跨环境运行 (Cross-Environment Execution)

痛点: 开发环境用 SQLite,生产环境用 PostgreSQL。如果不使用 DI,代码中会充斥着 process.env.NODE_ENV 的判断,导致代码混乱。

❌ 方式 A:不使用 InversifyJS (环境判断污染逻辑)

TypeScript

class DatabaseService {
    constructor() {
        // 😱 环境配置逻辑泄漏到了业务类中
        if (process.env.NODE_ENV === 'production') {
            this.connection = new PostgresConnection();
        } else {
            this.connection = new SqliteConnection();
        }
    }
    
    query() {
        return this.connection.exec("SELECT * FROM users");
    }
}

✅ 方式 B:使用 InversifyJS (容器模块化配置)

业务代码完全干净,环境切换的逻辑被移到了容器配置层(Composition Root)。

TypeScript

// 1. 业务代码 (完全不知道当前是什么环境)
@injectable()
class DatabaseService {
    constructor(@inject(TYPES.DbConnection) private conn: IDbConnection) {}
}

// 2. 环境配置模块 (config.ts)
const devModule = new ContainerModule((bind) => {
    bind<IDbConnection>(TYPES.DbConnection).to(SqliteConnection);
});

const prodModule = new ContainerModule((bind) => {
    bind<IDbConnection>(TYPES.DbConnection).to(PostgresConnection);
});

// 3. 入口文件 (index.ts)
const container = new Container();
if (process.env.NODE_ENV === 'production') {
    container.load(prodModule); // 🏭 加载生产模块
} else {
    container.load(devModule);  // 🛠️ 加载开发模块
}

4. 插件式架构 (Plugin Architecture)

痛点: 系统核心需要加载第三方插件。如果不使用 DI,核心系统必须手动 import 并实例化插件,这使得动态扩展变得极其困难。

❌ 方式 A:不使用 InversifyJS (手动列表)

核心代码必须“认识”每一个插件。

TypeScript

import { GitPlugin } from "./plugins/git";
import { DockerPlugin } from "./plugins/docker";

class App {
    private plugins: any[] = [];

    constructor() {
        // 😱 扩展性差:想加个插件,还得改核心代码的构造函数
        this.plugins.push(new GitPlugin());
        this.plugins.push(new DockerPlugin());
    }

    run() {
        this.plugins.forEach(p => p.exec());
    }
}

✅ 方式 B:使用 InversifyJS (多重注入 Multi-Injection)

核心系统定义接口,插件自行注册到容器。核心系统自动获取所有符合接口的插件。

TypeScript

// 核心系统
@injectable()
class App {
    private plugins: IPlugin[];

    constructor(
        // ✨ 魔法:自动把容器里所有绑定为 TYPES.Plugin 的实例都注入进来,形成数组
        @multiInject(TYPES.Plugin) plugins: IPlugin[]
    ) {
        this.plugins = plugins;
    }
}

// 插件 A (独立文件)
bind<IPlugin>(TYPES.Plugin).to(GitPlugin);

// 插件 B (独立文件)
bind<IPlugin>(TYPES.Plugin).to(DockerPlugin);

// 这种模式下,新增插件只需要 bind 一下,不需要修改 App 类的任何代码。

5. 复杂的生命周期管理 (Singleton vs Transient)

痛点: 某些对象(如缓存、数据库连接池)必须是全局单例,而某些对象(如 HTTP 请求上下文)必须每次新建。手动管理这些单例模式非常容易出错。

❌ 方式 A:不使用 InversifyJS (手动单例模式)

开发者必须手动实现 Singleton 模式,代码啰嗦且难以维护。

TypeScript

class CacheService {
    private static instance: CacheService;
    
    // 😱 样板代码:每个单例类都要写这一坨逻辑
    private constructor() {} 

    public static getInstance(): CacheService {
        if (!CacheService.instance) {
            CacheService.instance = new CacheService();
        }
        return CacheService.instance;
    }
}

// 使用时必须小心
const cache = CacheService.getInstance();

✅ 方式 B:使用 InversifyJS (声明式生命周期)

类本身不需要知道自己是不是单例,全靠容器配置。

TypeScript

@injectable()
class CacheService {
    constructor() { console.log("CacheService Created"); }
}

@injectable()
class RequestHandler {
    constructor() { console.log("RequestHandler Created"); }
}

// --- 容器配置 ---
// 1. 单例:整个应用只创建一次
container.bind(CacheService).toSelf().inSingletonScope();

// 2. 瞬态:每次请求都创建新的
container.bind(RequestHandler).toSelf().inTransientScope();

// --- 运行结果 ---
const cache1 = container.get(CacheService);
const cache2 = container.get(CacheService);
// 输出: "CacheService Created" (只输出一次,cache1 === cache2)

const handler1 = container.get(RequestHandler);
const handler2 = container.get(RequestHandler);
// 输出: "RequestHandler Created" (输出两次,handler1 !== handler2)

❌
❌