React 项目也能用依赖注入?我尝试了一下,真香

依赖注入(DI)这玩意儿,在后端开发里太常见了。Java 的 Spring、.NET 的 Core,全都是 DI 的典范。

但是在前端......好像很少有人提?

直到我用了 easy-model 的 IoC 容器,才发现:原来 React 也能用依赖注入,而且挺好用的。

先说说什么是依赖注入

用一个简单的例子解释一下:

没有 DI 的代码

typescript 复制代码
class ArticleService {
  private http = new AxiosHttp(); // 自己 new
  private logger = new ConsoleLogger(); // 自己 new
  private cache = new LocalCache(); // 自己 new

  async getArticles() {
    this.logger.info("Fetching articles");
    const cached = this.cache.get("articles");
    if (cached) return cached;

    const articles = await this.http.get("/api/articles");
    this.cache.set("articles", articles);
    return articles;
  }
}

问题:

  • ArticleService 和具体实现强耦合
  • 测试的时候没法换 mock
  • 换实现要改代码

有 DI 的代码

typescript 复制代码
class ArticleService {
  constructor(
    private http: HttpClient,
    private logger: Logger,
    private cache: Cache
  ) {}

  async getArticles() {
    this.logger.info("Fetching articles");
    const cached = this.cache.get("articles");
    if (cached) return cached;

    const articles = await this.http.get("/api/articles");
    this.cache.set("articles", articles);
    return articles;
  }
}

依赖由外部注入,代码只关心接口,不关心实现。

在 React 项目里有什么用?

场景一:统一的 HTTP 层

typescript 复制代码
// types/http.ts
import { z } from "zod";

// 用 Zod 定义 HTTP 客户端的接口
export const HttpSchema = z.object({
  get: z.function().args(z.string()),
  post: z.function().args(z.string(), z.unknown()),
});

export type HttpClient = z.infer<typeof HttpSchema>;

然后在各种 Model 里使用:

typescript 复制代码
// models/article.ts
export class ArticleModel {
  articles: Article[] = [];

  @inject(HttpSchema)
  private http?: HttpClient;

  @loader.load(true)
  @loader.once
  async fetchArticles() {
    this.articles = (await this.http?.get("/api/articles")) as Article[];
  }
}

// models/comment.ts
export class CommentModel {
  @inject(HttpSchema)
  private http?: HttpClient;

  async fetchComments(articleId: string) {
    return this.http?.get(`/api/articles/${articleId}/comments`);
  }
}

场景二:一行配置切换环境

开发环境和生产环境的 API 地址不同?Mock 和真实接口不同?

tsx 复制代码
// main.tsx
import { CInjection, config, Container } from "@e7w/easy-model";
import { MockHttp } from "./http/mock";
import { AxiosHttp } from "./http/axios";

config(
  <Container>
    {/* 开发环境用 Mock */}
    <CInjection schema={HttpSchema} ctor={MockHttp} />

    {/* 生产环境换 Axios */}
    {/* <CInjection schema={HttpSchema} ctor={AxiosHttp} /> */}
  </Container>
);

打包的时候换一行配置,全项目生效。

场景三:单元测试

这是 DI 最好用的地方------mock 替换 so easy。

typescript 复制代码
// article.test.ts
describe('ArticleModel', () => {
  beforeEach(() => {
    // 注入 Mock
    const mockHttp: HttpClient = {
      get: async (url: string) => {
        if (url.includes('articles')) {
          return [{ id: '1', title: 'Test Article' }];
        }
        return null;
      },
      post: async () => ({ success: true }),
    };

    config(
      <Container>
        <CInjection schema={HttpSchema} ctor={mockHttp} />
      </Container>,
    );
  });

  test('fetchArticles success', async () => {
    const article = provide(ArticleModel)();
    await article.fetchArticles();

    expect(/* articles 应有数据 */);
  });
});

实际项目结构

bash 复制代码
src/
├── types/
│   └── http.ts           # HTTP Schema 定义
├── http/
│   ├── mock-http.ts     # Mock 实现
│   └── axios-http.ts    # Axios 实现
├── models/
│   ├── article.ts
│   ├── comment.ts
│   └── user.ts
├── ioc/
│   └── container.ts     # 容器配置
└── main.tsx
typescript 复制代码
// ioc/container.ts
import { CInjection, config, Container } from "@e7w/easy-model";
import { HttpSchema } from "../types/http";
import { MockHttp } from "../http/mock-http";

export function setupContainer() {
  config(
    <Container>
      <CInjection schema={HttpSchema} ctor={MockHttp} />
    </Container>,
  );
}

命名空间隔离

如果你的项目需要多套配置,可以用命名空间隔离:

tsx 复制代码
import { CInjection, config, Container } from "@e7w/easy-model";
import { AdminAuth } from "./auth/admin";
import { UserAuth } from "./auth/user";

config(
  <>
    <Container namespace="admin">
      <CInjection schema={AuthSchema} ctor={AdminAuth} />
    </Container>
    <Container namespace="user">
      <CInjection schema={AuthSchema} ctor={UserAuth} />
    </Container>
  </>
);

然后在 Model 中指定使用哪个命名空间:

typescript 复制代码
class DashboardModel {
  @inject(AuthSchema, "admin")
  adminAuth!: AuthService;

  @inject(AuthSchema, "user")
  userAuth!: AuthService;
}

和其他 DI 框架对比

特性 easy-model InversifyJS
React 集成 ✅ 原生 ❌ 需要适配
学习成本
Zod 集成
React Hooks
TypeScript

什么时候用?

  • 项目有一定复杂度,不是简单的 CRUD
  • 需要在不同环境切换配置(dev/staging/prod)
  • 单元测试需要 mock 依赖
  • 团队习惯后端开发的 DI 模式

什么时候不用?

  • 简单的单页应用
  • 状态很扁平,没有深层依赖关系
  • 团队不熟悉 DI 概念

总结

easy-model 的 IoC 容器让我在 React 项目里也能享受依赖注入的好处:

  • 配置和代码分离:换实现不用改代码
  • 测试友好:mock 替换无压力
  • 环境切换:一行配置搞定多环境
  • 代码更清晰:只关心接口,不关心实现

如果你也在做中大型的 React 项目,强烈建议试试。

GitHub : github.com/ZYF93/easy-...

相关推荐
像素之间1 天前
为什么运行时要加set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve
前端·javascript·vue.js
M ? A1 天前
Vue转React实战:defineProps精准迁移实战
前端·javascript·vue.js·经验分享·react.js·开源·vureact
西陵1 天前
别再写 Prompt 了Spec Mode 才是下一代 AI 编程范式
前端·人工智能·ai编程
如意猴1 天前
【前端】002--怎样制作一个简历界面?
开发语言·前端·javascript
NickJiangDev1 天前
Elpis Schema 动态组件与表单:配置驱动的完整 CRUD 闭环
前端
kerli1 天前
Compose 组件:Box 核心参数及其 Bias 算法
android·前端
luckyCover1 天前
TypeScript学习系列(二):高级类型篇
前端·typescript
NickJiangDev1 天前
Elpis NPM 发布:把框架从业务中剥离出来
前端
im_AMBER1 天前
手撕发布订阅与观察者模式:从原理到实践
前端·javascript·面试
九英里路1 天前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串