All Tags
On this page

以React项目为例,展示数据分层的优势~

#react #vue #interview
avatar
jerrywu001
创建时间:2023-10-25 09:13:06

本文以React todo list 为例,逐步讲解数据分层的优势

分层原则的主旨就是

  1. view层不应该关注数据如何获取和转换
  2. view层的render部分应该专注于render,不应该在render函数里面做数据的处理操作
  3. 对于数据的增删改操作,如果需要复用,可以封装相应的hooks

Todo list

忽略分页逻辑

image.png

  • 数据结构 (这边的数据结构以及层级都很简单,现实业务通常比这个复杂的多的多)
{
  "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几乎是最简单的业务逻辑:

  1. 它的数据结构相当简洁
  2. 层级不深
  3. 字段的命名也相对合理
  4. jsx render函数很简洁

所以看起来并没有什么不妥的地方,但是你你想象下,如果你是在做一个很复杂的业务,那么:

  1. 它的数据结构就会相对复杂很多,会出现一些view压根用不到的属性
  2. 层级可能会很深,你想要的数据并不在第一或第二层
  3. 字段的命名也许不尽人意,或者你想要的是个文本,但是给了你数字,不同数字展示不同文本;或者说需要两个以及以上的字段去确定view中某个属性的展示效果
  4. jsx render函数会会变得很臃肿

这就是数据分层的意义,下面来逐步优化上面的代码

优化

数据获取和转换抽离

a. view不应该关注数据如何获取

image.png

  • 创建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 ...;
}

仔细观察代码,发现:

  1. 第8行,setTodos使用的data.todos可以进一步优化
  2. 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如何渲染

image.png

改造很简单,就是抽离组件即可(思路不要局限于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有几百行甚至破千行的代码,维护性以及可阅读性很差,分层原则对于解决这样的情况很有帮助!