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-...

相关推荐
somebody1 小时前
零经验学 react 的第15天 - 过渡动画(使用 react-transition-group 库进行实现)
前端
SuperEugene2 小时前
Vue3 + Element Plus 表单开发实战:防重复提交、校验、重置、loading 统一|表单与表格规范篇
前端·javascript·vue.js
SuperEugene2 小时前
Vue3 + Element Plus 中后台弹窗规范:开闭、传参、回调,告别弹窗地狱|Vue 组件与模板规范篇
开发语言·前端·javascript·vue.js·前端框架
桜吹雪2 小时前
在前端运行Qwen3.5原生多模态模型
前端·人工智能·机器学习
孟祥_成都2 小时前
前端下午茶:这 3 个网页特效建议收藏(送源码)
前端·javascript·css
SuperEugene2 小时前
VXE-Table 4.x 实战规范:列配置 + 合并单元格 + 虚拟滚动,避坑卡顿 / 错乱 / 合并失效|表单与表格规范篇
开发语言·前端·javascript·vue.js·前端框架·vxetable
火车叼位2 小时前
Volta 下 `corepack` 失踪之谜:问题不在 Node,而在命令入口
前端
Csvn2 小时前
Redux Toolkit 实战
react.js
cmd2 小时前
别再用错!5种JS类型判断方法,从原理到实战一文吃透
前端·javascript