使用React 实现一个简单的待办事项列表 | 青训营

113 阅读6分钟

使用React 实现一个简单的待办事项列表

这是我参与「第六届青训营 」伴学笔记创作活动的第 6 天

先贴出自己的代码仓库链接:J1uT1an/TODOList: 基于Web的TODOList项目,React+Koa全栈练手Demo (github.com)

为什么会有这么一个小 Demo ?

学习了 TS 和Node.js、MondoDB 后,准备用一个前后端项目来实践验证自己所学,并且复习React基础语法和相关生态

应用特点

  • 前后端均用 TypeScript 编写
  • 接口统一遵循 RESTful 风格
  • 实现服务端的优雅错误处理
  • 实现了前后端分离
  • HTTP 请求封装,错误处理
  • 组件化,代码分层

技术栈

  • 语言
    • TypeScript(赋予 JS 强类型语言的特性)
  • 前端
    • React(当下最流行的前端框架)
    • Axios(处理 HTTP 请求)
    • Ant-Design(阿里开源的 UI 语言框架)
    • React-Router(处理页面路由)
    • Redux(数据状态管理)
    • Redux-Saga(处理异步 Action)
    • React Hooks(状态管理,简化函数)
  • 后端
    • Koa(基于 Node.js 平台的下一代 web 开发框架)
    • Mongoose(内置数据验证, 查询构建,业务逻辑钩子等,开箱即用)

功能点

  • 用户登录注册
  • Todo 的关键词查询
  • Todo 内容修改
  • Todo 状态更改
  • Todo 记录删除

实践分析

TypeScript

image.png

TS 由微软团队开发,是 JS 的一个超集,在 JS 的基础上去做一些类型检测和相关的语法补充,本质上为 JS 增加了静态类型和一些面向对象编程的相关能力(或者说是后端语言的相关特性)。

let a: number = 1; // int a = 1;
let b: string = "Hello"; // string b = 'Hello'
let arr: number[] = [1, 2, 3]; //  int arr[] = {1,2,3};

官方文档链接:TS

/interface/UserState.ts 为例,导出了一个接口

export interface UserState {
  user_id?: string; // ?代表可选
  username?: string;
  err_msg?: string;
}

user 继承 UserState 接口,会有属性推导,而在 JS 中,我们需要自己输入 user.err_msg,繁琐易出错。

在 React,我们主要通过无状态组件 function,和有状态组件 class 来构建应用,包括 props 的传递,函数的传参,类的继承等都非常需要类型约定,可以说 TS 和 React“天生一对”,使用他们,我们的代码健壮性提高了一个档次。

redux 状态管理

状态管理是目前构建单页应用中不可或缺的一环,简单应用使用组件内 State 方便快捷,但随着应用复杂度上升,会发现数据散落在不同的组件,组件通信会变得异常复杂,这时候就需要 redux 来管理全局状态。它遵循三个原则:

  • 组件数据来源于 Store,单向流动
  • 只能通过触发 action 来改变 State,通过定义 actionTypes,做到全局惟一
  • Reducer 是纯函数

由于 Reducer 只能是纯函数(简单来说,一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。),而当处于请求(Fetch)场景时,Action 需要发起异步请求,包含了副作用,所以使用借助 Redux-Saga 来处理异步 Action,处理后返回成功的同步 Action 并触发,此时是一个纯函数,最终改变 store 数据。

以 FETCH_TODO(获取 Todo 资源)为例,数据流向如下图所示:

接口设计

由于采用的是前后端分离开发,我们通过约定接口来进行数据交换,而当下最流行的便是 RESTful 风格接口,它有以下几个要点:

  • 根据请求目的,设置对应 HTTP Method,如 GET 对应读取资源(Read),PUT 对应更新资源(Update),POST 对应创建资源(Created),DELETE 代表删除资源(Delete),对应数据库 CRUD 操作
  • 动词表示请求方式,名词表示数据源,一般采用复数形式 如 GET/users/2 获取 id 为 2 的用户
  • 返回相应的 HTTP 状态码,常见的有:
    • 200 OK 请求成功,
    • 201 CREATED 创建成功,
    • 202 ACCEPTED 更新成功,
    • 204 NO CONTENT 删除成功,
    • 401 UNAUTHORIZED 未授权,
    • 403 FORBIDDEN 禁止访问,
    • 404 NOT FOUND 资源不存在,
    • 500 INTERNAL SERVER ERROR 服务器端内部错误

以 Todo 的路由为例,我们可以设计出以下接口

const todoRouter = new Router({
  prefix: "/api/todos",
});

todoRouter
  .get("/:userId/all", async (ctx: Context) => {}) // 获取所有 Todo
  .post("/search", async (ctx: Context) => {}) // 关键词搜索
  .put("/status", async (ctx: Context) => {}) // 修改状态
  .put("/content", async (ctx: Context) => {}) // 修改内容
  .post("/", async (ctx: Context) => {}) // 新增 Todo
  .delete("/:todoId", async (ctx: Context) => {}); // 删除 Todo

代码分层

首先看服务端目录:

|--server
  |--db
  |--interface
  |--routes
  |--service
  |--utils
  |--app.ts
  |--config.ts

我们关注于 dbserviceroutes 这三个文件夹。

  • db 建立数据模型(Model),相当于 MySQL 的建表环节
  • service 调用数据模型处理数据库的业务逻辑,对数据库进行 CURD,返回加工后的数据
  • routes 调用 service 中的方法处理路由请求,设置请求响应

学过 Java 的小伙伴都知道,一个接口要通过 Domain 层,DAO 层,Service 层,才会进入 Controller 层调用,我们的项目类似于这种思想,更好的逻辑分层不仅能 提高项目的维护性,还能降低耦合度,这在大型项目中尤为重要。

错误处理

service/user 为例,我们定义了 userService 类,用于处理 user 的业务逻辑,其中的 addUser 为注册用户时调用的方法。

export default class UserService {
  public async addUser(usr: string, psd: string) {
    try {
      const user = new User({
        usr,
        psd,
      });
      // 如果 usr 重复,mongodb 会抛出 duplicate key 异常,进行捕获
      return await user.save();
    } catch (error) {
      throw new Error("用户名已存在 ( ̄o ̄).zZ");
    }
  }
}

由于我们设置了 usr 字段唯一,所以当用户注册时,输入已经注册过的用户名,就会抛出异常。这时候,我们要catch,并向调用此方法的路由抛出异常,随后路由层会捕获错误,返回用户名已存在的 HTTP 响应。这就是一个较为典型的的错误处理过程。

userRouter.post("/", async (ctx: Context) => {
  const payload = ctx.request.body as IPayload;
  const { username, password } = payload;
  try {
    const data = await userService.addUser(username, password);
    if (data) {
      createRes({
        ctx,
        statusCode: StatusCode.Created,
      });
    }
  } catch (error) {
    createRes({
      ctx,
      errorCode: 1,
      msg: error.message,
    });
  }
});

统一响应

关于 API 调用的返回结果,为了格式化响应体,我们在 /utils/response.ts 编写处理响应的通用函数。

返回一组消息,指明调用是否成功。这类消息通常具有共同的消息体样式。

通用返回格式是由 msgerror_codedata 三个参数组成的 JSON 响应体:

import { Context } from "koa";
import { StatusCode } from "./enum";

interface IRes {
  ctx: Context;
  statusCode?: number;
  data?: any;
  errorCode?: number;
  msg?: string;
}

const createRes = (params: IRes) => {
  params.ctx.status = params.statusCode! || StatusCode.OK;
  params.ctx.body = {
    error_code: params.errorCode || 0,
    data: params.data || null,
    msg: params.msg || "",
  };
};

export default createRes;

当我们请求 GET /api/todos/:userId/all 时,获取指定用户的所有 Todo,返回以下响应体:

{
  "error_code": 0,
  "data": [
    {
      "_id": "5e9b0f1b576bd642796dd7d0",
      "userId": "5e9b0f08576bd642796dd7cf",
      "content": "成为全栈工程师~~~",
      "status": false,
      "__v": 0
    }
  ],
  "msg": ""
}

参考

参考了掘进作者:B2D1 的Todolist开源项目,链接:juejin.cn/post/684490…