依赖注入 如何在前端和 React 中工作?

核心是通过 React 中的 createContextuseContext 使用控制反转来处理,这样无需逐层传递属性,从而使得代码库更加简洁和易于维护。可以将变量和对象注入到需要它们的地方。

具体的使用步骤:

  1. 创建适配器接口 - 定义组件所需依赖的类型信息
  2. 创建一个Context 上下文 - 作为依赖管理容器容纳上面的适配器
  3. 在应用中使用该上下文的 Provider 来设置适配器的具体实现
  4. 然后在需要的组件中,通过 useContext() 来访问依赖并使用。

1.创建适配器接口

export type SendAnalyticsEvent = (eventType: string) => void;

2. 创建接受适配器的 Context

// 上下文形状的接口
interface DIContainerInjectors {
    sendAnalyticsEvent: SendAnalyticsEvent // references the shape defined above ^
}
 
// 创建上下文(无初始值)
const InjectionContainerContext = createContext<DIContainerInjectors>()
 
// 创建一个组件,可以提供值。
export const InjectionContainerProvider = (props: {sendAnalyticsEvent: SendAnalyticsEvent}) => {
    const injectors: DIContainerInjectors = {
        sendAnalyticsEvent: props.sendAnalyticsEvent
        // ... and any other services you want to inject in
    }
 
    return <InjectionContainerContext.Provider value={injectors}>
        {props.children}
    </InjectionContainerContext.Provider>
}
 
// helper hook to get the injectors
export const useInjectedValue = (): DIContainerInjectors => {
    const ctx = useContext(InjectionContainerContext)
    if(!ctx) throw new Error('Must use InjectionContainerProvider first')
    return ctx
}
 

3. 在应用中使用上下文

import {sendAnalyticsEvent} from './sendAnalyticsEvent'
 
export function App({Component}) {
    const someBlogPost = { /* ... */ };
 
    return (<InjectionContainerProvider sendAnalyticsEvent={sendAnalyticsEvent}>
            <YourBlogPost blogPost={someBlogPost} />
        </InjectionContainerProvider>);
}

4. 在组件中使用注入的对象

import {sendAnalyticsEvent} from './infrastructure/analytics';
 
function YourBlogPost({blogPost}: {blogPost: BlogPost}) {
    const {sendAnalyticsEvent} = useInjectedValue()
 
    const share = () => {
        sendAnalyticsEvent('share-button-pressed') // << this is now injected in!
        // we didn't import it directly
 
        // then show user some url to share
        alert('Share this url: http://example.com/' + blogPost.slug)
    }
 
    <div>
        <h1>{blogPost.title}</h1>
        <button onClick={share}>Share</button>
    </div>
}

如何测试

使用相同的 InjectionContainerProvider 组件来注入一个模拟函数

it('should send analytics event', async () => {
    const mockSendAnalyticsEventFn = jest.fn();
    render(
        <InjectionContainerProvider sendAnalyticsEvent={mockSendAnalyticsEventFn}>
            <YourBlogPost blogPost={{ title: 'Test Post', slug: 'test-post' }} />
        </InjectionContainerProvider>
    );
 
    const button = screen.getByText('Share');
    await userEvent.click(button);
 
    expect(mockSendAnalyticsEventFn).toHaveBeenCalledTimes(1);
});
 

哪些东西可以注入

在 React 中具有副作用的事情,例如 API 调用或与浏览器 Web API 交互或者业务逻辑等不应该成为 React 组件的一部分的,应该被抽象出来

  • analytics 分析
  • 内部后端 API 调用
  • 外部第三方 API 调用
  • 复杂/计算缓慢的业务逻辑
  • window.localStorage 、 window.localStorage 等
  • 配置或者环境变量
  • 日志服务

有哪些好处

  • 更易维护的代码、更多 可重用的代码,以及更容易测试的代码。
  • 简化测试(对 TDD 非常有用),您可以使用 Storybook 等工具预览您的单个组件,并轻松注入模拟服务/对象(例如,可能不需要进行真实的 API 调用)。
  • 您的代码将会减少耦合,这通常意味着更容易维护。
  • 更容易看到明确的关注点分离

原文地址:Dependency injection in React - How to Manage Dependencies in Your React App for Cleaner and Easier to Maintain Code | Code Driven Development