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

13 阅读3分钟

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

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

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

先说说什么是依赖注入

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

没有 DI 的代码

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 的代码

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 层

// 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 里使用:

// 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 和真实接口不同?

// 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。

// 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 应有数据 */);
  });
});

实际项目结构

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
// 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>,
  );
}

命名空间隔离

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

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 中指定使用哪个命名空间:

class DashboardModel {
  @inject(AuthSchema, "admin")
  adminAuth!: AuthService;

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

和其他 DI 框架对比

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

什么时候用?

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

什么时候不用?

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

总结

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

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

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

GitHub: github.com/ZYF93/easy-…