React 组件不应该包含业务逻辑,你同意吗? 如果是,请继续阅读。 如果没有,请立即停止。 这篇文章是写给别人的。
只需 3 个步骤即可将依赖注入添加到您的 React 项目中:
创建一个“容器”来放置您的依赖项
创建一个钩子 useInject 来检索依赖项
使用组件中的钩子
长版
如果您对答案持观望态度,我将尝试为您提供一些关于为什么我们不应该这样做的见解:
React 是一个用于创建用户界面的库; 这个定义应该阻止我们将业务逻辑放入组件中。
包含业务逻辑的组件很难阅读、维护和测试。
从组件中提取业务逻辑是一个能够重用它的好主意。
好的,如果我们从组件中取出业务逻辑,我们应该将其编写在某个地方,例如外部类或函数中。 我们该怎么做呢? 通过使用依赖注入!
什么是依赖注入,为什么要使用它
依赖注入 (DI) 是一种软件设计模式,它将对象的创建与其使用分开。 实际上,对象的实例化不是在使用它们的代码中创建事物,而是委托给负责创建对象并将其提供给需要它们的组件的外部实体。
依赖注入旨在使代码更加灵活、模块化且易于测试。 DI 不是严格的、紧密耦合的代码,而是允许模块化组件轻松替换或扩展,而无需更改使用它们的代码。
此外,DI 有助于提高代码的可读性和可维护性,因为它使组件之间的依赖关系变得明确,并有利于复杂依赖关系的管理。
总之,依赖注入的作用是:
将对象的创建与其使用分开
使代码更加灵活、模块化且易于测试
提高代码的可读性和可维护性。
React 中的依赖注入
假设我们需要开发一个管理待办事项列表的 React 应用程序(多么奇特)。
我们将有一个显示待办事项列表的组件。我们希望在渲染组件时从某些 API 加载列表。 由于我们不想在组件中编写业务逻辑,因此我们将拥有一个执行 API 调用的服务,我们可以在组件中使用该服务。 如该图所示。
在此序列图中,组件通过调用方法或函数向服务发送数据请求。 然后,服务通过发送请求从外部 API 检索数据。 一旦接收到数据,服务就会对其进行处理并将其返回给组件。
事件的顺序由图中的箭头指示。 从API到Service的虚线箭头表示从API检索数据,而从Service到Component的实线箭头表示将处理后的数据返回到Component。
在实现级别,代码可能如下所示:
import React, { useState, useEffect } from 'react';
import TodoService from './TodoService';
function TodoList() {
const [todos, setTodos] = useState([]);
useEffect(() => {
async function fetchTodos() {
try {
const todos = await TodoService.getTodos(); // Call TodoService to get todos
setTodos(todos);
} catch (error) {
console.error(error);
}
}
fetchTodos();
}, []);
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
export default TodoList;
在此代码中,我们定义了一个 TodoList 组件,该组件从 TodoService 调用 getTodos 方法。 我们使用 useState 挂钩来跟踪从服务返回的待办事项,并使用 useEffect 挂钩在组件安装时获取待办事项。
当 fetchTodos 函数被调用时,它使用await关键字等待getTodos方法返回todos。 一旦待办事项返回并使用 setTodos 函数将其设置为待办事项状态变量。
最后,我们使用地图函数渲染待办事项列表并显示每个待办事项的标题。 请注意,这是一个简化的示例,TodoService 的实现可能会根据所使用的 API 的不同而有所不同。
使用 props 进行依赖注入
import React, { useState, useEffect } from 'react';
function TodoList({ todoService }) {
const [todos, setTodos] = useState([]);
useEffect(() => {
async function fetchTodos() {
try {
const todos = await todoService.getTodos(); // Call injected TodoService to get todos
setTodos(todos);
} catch (error) {
console.error(error);
}
}
fetchTodos();
}, [todoService]);
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
export default TodoList;
在此更新的代码中,我们将 TodoService 作为 prop 注入到 TodoList 组件中。 该组件不再直接导入 TodoService,而是使用注入的服务来获取 todo。
当组件安装时, fetchTodos 函数使用注入的 todoService 来检索 todos。 这样,我们可以通过将不同的服务实现传递给 TodoList 组件来轻松交换 TodoService 的实现。
要将 TodoList 组件与 TodoService 的特定实现一起使用,我们将该服务作为 prop 传递,如下所示:
import React from 'react';
import TodoService from './TodoService';
import TodoList from './TodoList';
function App() {
return <TodoList todoService={TodoService} />;
}
export default App;
通过将 TodoService 作为 prop 传递给 TodoList 组件,我们实现了更加模块化和灵活的设计,因为我们可以轻松地在 TodoService 的不同实现之间切换,而无需更改 TodoList 组件。
在为组件编写测试时,这非常有用。
此时,我们有两个问题:
我们仍然需要将 TodoService 导入到 App 组件中,该组件不使用依赖注入。
props 只是在应用程序中传输数据的有效方法之一,因为它们仅适用于嵌套层。
该图显示了 props 如何通过组件的层次结构传递。 组件缩进得越多,它在组件树中的嵌套就越深。 我们不想要这种嵌套。
使用 React Context 进行依赖注入
下面是使用 TodoContext 从组件调用 TodoService 的 React 代码示例:
import React, { useState, useEffect, useContext } from 'react';
import TodoContext from './TodoContext';function TodoList() {
const [todos, setTodos] = useState([]);
const todoService = useContext(TodoContext); // Retrieve TodoService from TodoContext
useEffect(() => {
async function fetchTodos() {
try {
const todos = await todoService.getTodos(); // Call TodoService from TodoContext to get todos
setTodos(todos);
} catch (error) {
console.error(error);
}
}
fetchTodos();
}, [todoService]); return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
export default TodoList;
在此代码中,我们使用 useContext 挂钩从 TodoContext 检索 TodoService。 然后我们使用检索到的 todoService 来获取 todos。
useEffect 钩子用于在组件安装时获取待办事项。 fetchTodos 函数使用检索到的 todoService 来检索 todos 并将它们设置为 todos 状态变量。
要使用此组件,我们首先创建一个 TodoContext 并将组件包装在其中,如下所示:
import React from 'react';
import TodoContext from './TodoContext';
import TodoService from './TodoService';
import TodoList from './TodoList';
function App() {
return (
<TodoContext.Provider value={TodoService}>
<TodoList />
</TodoContext.Provider>
);
}
export default App;
在此示例中,我们创建一个 TodoContext 并将 TodoService 作为其值传递。 然后,我们将 TodoList 组件包装在 TodoContext.Provider 组件内,以便 TodoList 组件可以使用 useContext 挂钩从上下文中检索 TodoService。
使用控制反转容器 (IOC) 进行依赖注入
使用这个上下文概念,我们可以再采取一步,使用控制反转容器。
等等,什么是控制反转容器 (IoC)?
在 React 中,IoC(控制反转)容器是一种工具,可让您管理应用程序中不同组件和服务之间的依赖关系。 它提供了一种方法来定义和注册服务或对象(依赖项)一次,然后将它们注入到依赖它们的其他组件中。 这有助于解耦组件,并使您的应用程序更加模块化且更易于维护。
React 中的 IoC 容器通常通过提供一个中央注册表来工作,该注册表引用可用作依赖项的所有对象。 组件可以从容器请求这些依赖项,而不是直接创建它们。 这种方法还可以轻松地用替代实现替换依赖项或模拟它们进行测试。
React 有几种流行的 IoC 容器,例如 InversifyJS、Awilix 和 BottleJS,它们提供了构造函数注入、属性注入和自动依赖解析等各种功能。 一些 IoC 容器比其他容器更复杂,因此选择适合您的项目需求和复杂程度的容器非常重要。
在我们的例子中,我们将从头开始编写一个示例,如下所示:
import React, { createContext, useContext } from 'react';
// Create a new context for the container
const ContainerContext = createContext();
// Define a component that provides the container to its children
const ContainerProvider = ({ container, children }) => {
return <ContainerContext.Provider value={container}>{children}</ContainerContext.Provider>;
};
// Define a hook to access the container from within a component
const useContainer = () => {
const container = useContext(ContainerContext);
if (!container) {
throw new Error('Container not found. Make sure to wrap your components with a ContainerProvider.');
}
return container;
};
// Define a hook to inject dependencies from the container
const useInject = (identifier) => {
const container = useContainer();
return container.resolve(identifier);
};
// Example usage:
const MyService = () => {
return { foo: 'bar' };
};
const MyComponent = () => {
const myService = useInject('myService');
return <div>{myService.foo}</div>; // Output: 'bar'
};
const container = {
registry: {
myService: MyService()
},
resolve(identifier) {
if (!this.registry.hasOwnProperty(identifier)) {
throw new Error(`Object with identifier ${identifier} not found in container`);
}
return this.registry[identifier];
}
};
const App = () => {
return (
<ContainerProvider container={container}>
<MyComponent />
</ContainerProvider>
);
};
在此示例中,我们创建一个 ContainerProvider 组件,该组件将容器对象作为 prop,并使用 ContainerContext 上下文将其提供给其子组件。 我们还定义了一个 useContainer 钩子,可用于从组件内检索容器。
然后,我们定义一个 MyService 对象并将其添加到容器对象的注册表属性中。 我们还在容器对象上定义了一个解析方法,该方法采用标识符并从注册表中检索相应的对象。 在本例中,resolve 方法返回 MyService 对象。
我们用 ContainerProvider 包装 MyComponent,并将容器对象作为 prop 传入,然后渲染 App 组件,该组件渲染用 ContainerProvider 包装的 MyComponent。 当呈现 MyComponent 时,它会从容器中检索 myService 依赖项,并将其 foo 属性呈现到屏幕上。
在此代码中,我们添加了一个 useInject 挂钩,该挂钩将标识符作为参数并从容器中检索相应的对象。 useInject 钩子在内部调用 useContainer 来检索容器,然后调用容器上的resolve 方法来检索对象。
接下来,我们创建一个 MyComponent,它使用 useInject 挂钩从容器中检索 myService 依赖项,并将其 foo 属性呈现到屏幕上。
最后,我们渲染 App 组件,它渲染用 ContainerProvider 包装的 MyComponent。 当呈现 MyComponent 时,它使用 useInject 挂钩从容器中检索 myService 依赖项,并将其 foo 属性呈现到屏幕上。
总之,依赖注入是一种设计模式,它允许更灵活、模块化且易于测试的代码。 它将对象的创建与其使用分开,使组件之间的依赖关系变得明确并促进复杂依赖关系的管理。
在 React 中,依赖注入可以通过将服务作为 props 注入或使用容器通过钩子为组件提供依赖关系来实现。 这种做法可以使代码更清晰、更易于维护,从而更容易重用业务逻辑和管理依赖项。
遵循此模式可以提高 React 代码的可读性、可维护性和可测试性。