以React项目为例,展示数据分层的优势~
#react #vue #interview
jerrywu001
创建时间:2023-10-25 09:13:06
本文以React todo list 为例,逐步讲解数据分层的优势
分层原则的主旨就是
- view层不应该关注数据如何获取和转换
- view层的render部分应该专注于render,不应该在render函数里面做数据的处理操作
- 对于数据的增删改操作,如果需要复用,可以封装相应的hooks
Todo list
忽略分页逻辑
- 数据结构 (这边的数据结构以及层级都很简单,现实业务通常比这个复杂的多的多)
{
"todos": [
{
"id": 1,
"todo": "Do something nice for someone I care about",
"completed": true,
"userId": 26
},
],
"total": 150,
"skip": 0,
"limit": 30
}
- 简单粗暴的实现如下
Todos.tsx
import { useEffect, useState } from 'react';
import './index.scss';
interface ITodo {
id: number;
todo: string;
completed: boolean;
}
export default function Todos() {
const [todos, setTodos] = useState<ITodo[]>([]);
useEffect(() => {
fetch('https://dummyjson.com/todos')
.then(res => res.json())
.then((data) => {
setTodos(data.todos);
});
}, []);
return (
<ul>
{
todos.map(v => (
<div className="todo-item" key={v.id}>
<span className="text">
{v.todo}
</span>
<span className={`tag ${v.completed ? 'completed' : ''}`}>
{v.completed ? '已完成' : '未完成'}
</span>
</div>
))
}
</ul>
);
}
为什么要做分层
可能你看了下上面的代码,心里会想:“真的没什么问题啊,要改什么呢?”
这是由于todo几乎是最简单的业务逻辑:
- 它的数据结构相当简洁
- 层级不深
- 字段的命名也相对合理
- jsx render函数很简洁
所以看起来并没有什么不妥的地方,但是你你想象下,如果你是在做一个很复杂的业务,那么:
- 它的数据结构就会相对复杂很多,会出现一些view压根用不到的属性
- 层级可能会很深,你想要的数据并不在第一或第二层
- 字段的命名也许不尽人意,或者你想要的是个文本,但是给了你数字,不同数字展示不同文本;或者说需要两个以及以上的字段去确定view中某个属性的展示效果
- jsx render函数会会变得很臃肿
这就是数据分层的意义,下面来逐步优化上面的代码
优化
数据获取和转换抽离
a. view不应该关注数据如何获取
- 创建api.ts
export async function queryTodos() {
const response = await fetch('https://dummyjson.com/todos');
return response.json();
}
- Todos.tsx
import { queryTodos } from './api';
// ...
export default function Todos() {
// ...
useEffect(() => {
queryTodos().then((data) => setTodos(data.todos));
}, []);
// return ...;
}
仔细观察代码,发现:
- 第8行,setTodos使用的
data.todos
可以进一步优化 - jsx render函数对数据进行了处理操作
这些是不符合分层原则的,代码改造如下:
- api.ts
interface IOriginalTodo {
id: number;
todo: string;
completed: boolean;
}
export interface ITodo {
id: number;
text: string;
status: string;
statusStyleName: string;
}
export async function queryTodos() {
const response = await fetch('https://dummyjson.com/todos');
const originalJson: { todos: IOriginalTodo[] } = await response.json();
return convertToViewStructure(originalJson.todos || []);
}
function convertToViewStructure(items: IOriginalTodo[] = []) {
return items.map(v => ({
id: v.id,
text: v.todo,
status: v.completed ? '已完成' : '未完成',
statusStyleName: v.completed ? 'completed' : '',
} as ITodo));
}
- Todos.tsx
import { ITodo, queryTodos } from './api';
import { useEffect, useState } from 'react';
import './index.scss';
export default function Todos() {
const [todos, setTodos] = useState<ITodo[]>([]);
useEffect(() => {
queryTodos().then((data) => setTodos(data));
}, []);
return (
<ul>
{
todos.map(v => (
<div className="todo-item" key={v.id}>
<span className="text">
{v.text}
</span>
<span className={`tag ${v.statusStyleName}`}>
{v.status}
</span>
</div>
))
}
</ul>
);
}
这样改造的好处是,无论后端数据结构怎么调整,我们只需要修改api.ts即可,view不需要动~
redner不用关注每个item如何渲染
改造很简单,就是抽离组件即可(思路不要局限于todo!想象下业务逻辑很复杂,那么render函数就是灾难)
- TodoItem.tsx
import { ITodo } from './api';
import './todo.scss';
export default function TodoItem({ data }: { data: ITodo }) {
return (
<div className="todo-item">
<span className="text">
{data.text}
</span>
<span className={`tag ${data.statusStyleName}`}>
{data.status}
</span>
</div>
);
}
- Todos.tsx
import TodoItem from './TodoItem';
import { ITodo, queryTodos } from './api';
import { useEffect, useState } from 'react';
import './index.scss';
export default function Todos() {
const [todos, setTodos] = useState<ITodo[]>([]);
useEffect(() => {
queryTodos().then((data) => setTodos(data));
}, []);
return (
<ul>
{
todos.map(v => <TodoItem data={v} key={v.id} />)
}
</ul>
);
}
数据复用场景
想象一下,如果对todo增加:增删改操作,也就是对todos
变量进行增删改,那么Todos.tsx
文件中就会多出3个函数:
- addTodo
- removeTodo
- updateTodo
试想一下,如果存在3个不同的view都需要todos,并且它们对于todo item渲染的样式不同,这时候每个文件copy paste,对于后期维护将是灾难。
所以分封装hooks势在必行!
- useTodos.ts
import { useEffect, useState } from 'react';
import { ITodo, queryTodos } from './api';
export default function useTodos() {
const [todos, setTodos] = useState<ITodo[]>([]);
const addTodo = (todo: ITodo) => {
// 与服务器交互
// then setTodos, unshift the todo
setTodos((currentTodos) => [todo, ...currentTodos]);
};
useEffect(() => {
queryTodos().then((data) => setTodos(data));
}, []);
return {
todos,
addTodo,
};
}
- Todos.tsx
import TodoItem from './TodoItem';
import useTodos from './useTodos';
import './index.scss';
export default function Todos() {
const { todos, addTodo } = useTodos();
return (
<ul>
{ todos.map(v => <TodoItem data={v} key={v.id} />) }
</ul>
);
}
- SomeOtherView.tsx
import useTodos from './useTodos';
export function SomeOtherView() {
const { todos } = useTodos();
return (
<ul>
{
todos.map(v => (
<>
{/* // 使用todos构建新的UI... */}
</>
))
}
</ul>
);
}
给api加异常处理
// ...
export async function queryTodos() {
let todos: ITodo[] = [];
try {
const response = await fetch('https://dummyjson.com/todos');
const originalJson: { todos: IOriginalTodo[] } = await response.json();
todos = convertToViewStructure(originalJson.todos || []);
} catch (error) {
// ...
}
return todos;
}
// ...
加餐
时常会面试遇到“组件卸载后如何终断请求”,直接上代码mark一下
- api.ts
interface IOriginalTodo {
id: number;
todo: string;
completed: boolean;
}
export interface ITodo {
id: number;
text: string;
status: string;
statusStyleName: string;
}
export async function queryTodos({ signal }: { signal: AbortSignal }) {
let todos: ITodo[] = [];
try {
const response = await fetch('https://dummyjson.com/todos', { signal });
const originalJson: { todos: IOriginalTodo[] } = await response.json();
todos = convertToViewStructure(originalJson.todos || []);
} catch (error) {
// ...
}
return todos;
}
function convertToViewStructure(items: IOriginalTodo[] = []) {
return items.map(v => ({
id: v.id,
text: v.todo,
status: v.completed ? '已完成' : '未完成',
statusStyleName: v.completed ? 'completed' : '',
} as ITodo));
}
- useTodos.ts
import { useEffect, useState } from 'react';
import { ITodo, queryTodos } from './api';
export default function useTodos() {
const [todos, setTodos] = useState<ITodo[]>([]);
const addTodo = (todo: ITodo) => {
// 与服务器交互
// then setTodos, unshift the todo
setTodos((currentTodos) => [todo, ...currentTodos]);
};
useEffect(() => {
const abortController = new AbortController();
const fetchTodos = async () => {
const items = await queryTodos({ signal: abortController.signal });
setTodos(items);
};
fetchTodos();
return () => {
abortController.abort();
};
}, []);
return {
todos,
addTodo,
};
}
总结
本文就是以最简单的todo业务场景去讲解数据分层的意义。因为往往日常开发下,业务逻辑会很复杂,经常看到view有几百行甚至破千行的代码,维护性以及可阅读性很差,分层原则对于解决这样的情况很有帮助!