使用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
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
我们关注于 db,service,routes 这三个文件夹。
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 编写处理响应的通用函数。
返回一组消息,指明调用是否成功。这类消息通常具有共同的消息体样式。
通用返回格式是由 msg、error_code 及 data 三个参数组成的 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…