依赖注入(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-...