深入剖析Restful API前端接口模型架构
一、前言
之前在前公司接触到一种很有意思的权限校验接口组合方式,在当时的Vue项目里,所有权限校验接口都是在一个Model
对象中,借助修饰器来实现的。我觉得挺有趣,就用React模拟了一下这个实现过程,下面就给大家详细讲讲。
二、useMutation实现
先给大家看看我模拟出来的React的useMutation
钩子函数,这个钩子函数能让代码更好理解。它的代码在utils/useMutation.ts
文件里,具体内容如下:
// utils/useMutation.ts import { useState } from'react'; // 定义useMutation函数,接收一个包含url、method、variables的配置对象 export function useMutation<T, P>(options: { url: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE'; variables: (params: T) => any; }) { // 定义loading状态,用于表示请求是否正在进行,初始值为false const [loading, setLoading] = useState(false); // 定义data状态,用于存储请求返回的数据,初始值为null const [data, setData] = useState (null); // 定义error状态,用于存储请求过程中发生的错误,初始值为null const [error, setError] = useState(null); // 定义mutate函数,用于发起请求 const mutate = async (params: T) => { // 请求开始,设置loading为true,清除之前的错误 setLoading(true); setError(null); // 根据传入的参数生成查询字符串 const queryParams = new URLSearchParams(options.variables(params)).toString(); // 根据请求方法拼接url,如果是GET请求,将查询字符串拼接到url后面 const url = options.method === 'GET'? `${options.url}?${queryParams}` : options.url; try { // 发起fetch请求 const response = await fetch(url, { method: options.method, headers: { 'Content-Type': 'application/json', }, }); // 打印响应信息,方便调试 console.log('Response:', response); // 如果响应状态码表示成功 if (response.ok) { // 解析响应数据为JSON格式,并存储到data状态中 const result = await response.json(); setData(result); } else { // 如果响应失败,抛出错误 throw new Error('Request failed'); } } catch (err) { // 如果请求过程中发生错误,将错误信息存储到error状态中 setError(err); } finally { // 请求结束,设置loading为false setLoading(false); } }; // 返回包含mutate函数、loading状态、data状态和error状态的对象 return { mutate, loading, data, error }; }
这里实现的useMutation
其实是把从接口定义到函数实现的整个过程都整合到一起了。虽然这么做在一定程度上降低了结耦复用率,不过也让代码更集中处理权限校验接口相关的逻辑。
三、Model的构建与使用
接下来看看Model
类,我们在这个类里使用useMutation
来实现权限校验接口的装饰。代码如下:
import { useMutation } from "../utils/useMutation"; // 定义User数据类型,包含接口返回的各种信息 interface UserData { result: number; err_msg: string; data: { id: number; // 用户ID dep: string; // 部门 Per: string; // 权限 }; } // 定义List数据类型,包含接口返回的列表相关信息 interface ListData { result: number; err_msg: string; data: Array<{ id: number; name: string; dep: string; Per: string }>; } // 定义Model类,在类的构造函数中初始化请求方法 export class Model { // 初始化用户信息请求方法,传递参数id获取用户数据 user = useMutation<{ id: number }, UserData>({ url: 'http://localhost:3000/user', method: 'GET', variables: (p) => ({ id: p.id }), }); // 初始化列表信息请求方法,传递分页参数获取列表数据 list = useMutation<{ page: number, size: number }, ListData>({ url: 'http://localhost:3000/list', method: 'GET', variables: (p) => ({ page: p.page, size: p.size }), }); }
在这个Model
类里,我们挂载了user
和list
两个属性,分别对应获取用户信息和列表信息的接口。部分类型定义可通通忽略,这里我便在Model身上去挂载了这些属性,打印看看。不过,刚定义完的时候,这些属性里的数据都是空的,因为接口还没有真正发起请求,在浏览器的network面板里也看不到请求记录。
要想让接口发起请求,我们需要在业务层调用它们。这里使用单例模式导出Model
对象,这样在全局都能使用同一个对象的属性。下面是业务层的代码示例:
import React, { useEffect, useState } from'react'; import {useApollo} from 'apllo.js'; import { Model } from './api/model'; // 定义一个React组件GameAppModel const GameAppModel: React.FC = () => { // 使用useApollo获取Model实例 const model = useApollo(Model); // 组件挂载时打印model.user和model.list信息 useEffect(()=>{ console.log('model.user:', model.user); console.log('model.list:', model.list); }) // 定义用户数据获取状态和列表数据获取状态 const [isUserFetched, setIsUserFetched] = useState(false); const [isListFetched, setIsListFetched] = useState(false); // 定义获取用户数据的函数 const handleFetchUser = async () => { // 重置用户数据获取状态 setIsUserFetched(false); // 调用model.user的mutate方法获取用户数据,假设传递用户id为1 await model.user.mutate({ id: 1 }); // 设置用户数据获取状态为true setIsUserFetched(true); }; // 定义获取列表数据的函数 const handleFetchList = async () => { // 重置列表数据获取状态 setIsListFetched(false); // 调用model.list的mutate方法获取列表数据,假设获取第1页,10条数据 await model.list.mutate({ page: 1, size: 10 }); // 设置列表数据获取状态为true setIsListFetched(true); }; // 如果正在获取用户数据或列表数据,显示加载中的提示 if (model.user.loading || model.list.loading) { return <div>Loading...</div> ; } // 如果获取用户数据或列表数据时发生错误,显示错误提示 if (model.user.error || model.list.error) { return <div>Error occurred while fetching data.</div> ; } return ( <div> <h2>User Info:</h2> {/* 定义获取用户信息的按钮,根据loading状态禁用按钮并显示不同文字 */} <button disabled="disabled"> {model.user.loading? 'Loading User...' : 'Fetch User Info'} </button> {/* 如果已经获取到用户数据,显示数据;否则显示未获取到数据的提示 */} {isUserFetched && model.user.data? ( <pre>{JSON.stringify(model.user.data, null, 2)}</pre> ) : ( No user data fetched )} <h2>Item List:</h2> {/* 定义获取列表信息的按钮,根据loading状态禁用按钮并显示不同文字 */} <button disabled="disabled"> {model.list.loading? 'Loading List...' : 'Fetch Item List'} </button> {/* 如果已经获取到列表数据,显示数据;否则显示未获取到数据的提示 */} {isListFetched && model.list.data? ( <pre>{JSON.stringify(model.list.data, null, 2)}</pre> ) : ( No list data fetched )} </div> ); }; export default GameAppModel;
增删改我就不再细细演示了,下面讲讲resful API 的介绍以及,优势在哪里为什么要这样去做。
四、Restful API介绍
讲完了上面的实现过程,下面来聊聊Restful API。REST可不是什么协议或者标准,它其实是一种架构风格。它有几个比较重要的指导原则:
4.1 统一接口
常见的像GET
、POST
、PUT
、DELETE
这些,每个接口都有自己明确的用途,比如GET
一般用来获取数据,POST
用于创建数据等。
4.2 无状态
这就要求客户端给服务器发送的每个请求,都得包含能让服务器理解和完成这个请求的所有必要信息,服务器不会记住客户端之前的请求状态。
4.3 可缓存约束
服务器返回的响应得明确告诉客户端这个响应能不能被缓存起来,这样可以提高性能。
4.4 分层
系统是分层架构的,每个组件只能看到和它直接交互的那一层,不能跨层访问,这样可以降低系统的复杂度。
4.5 客户端-服务器分离
现在大多数项目都是前后端分离的,前端负责展示和用户交互,后端负责处理业务逻辑和数据存储,这一点大家应该都比较熟悉。
五、Restful API在前端模型层的优势
5.1 专注业务层逻辑
使用Restful API设计前端模型层,开发者不用操心接口调用的那些细节,像发起HTTP请求、处理错误这些,都交给Model
层或者统一的API管理模块去处理,自己只需要专注于具体的业务逻辑就行。
5.2 数据状态管理
Model
层会负责管理接口返回的数据状态,还能把数据缓存起来。这样如果需要再次使用这些数据,就不用重复请求服务器了,性能也就提高了。开发者也能很方便地获取这些数据,进行后续的校验或者判断。
5.3 统一的错误处理
可以把网络错误(比如404、500这些状态码对应的错误)、权限错误(像401、403这些)以及业务错误都集中起来处理。开发者只需要关注和业务相关的错误,其他错误交给统一的处理机制就行。
5.4 解耦接口层与业务层
通过抽象出接口层(也就是API模块或者Model
层),可以把HTTP请求的逻辑和具体的业务逻辑隔离开。业务层直接调用Model
提供的接口,不用关心底层网络请求是怎么实现的,这样代码的结构更清晰,维护起来也更方便。
以上就是我对Restful API前端接口模型架构的一些理解和分析,希望对大家有所帮助。如果在实际开发中遇到相关问题,欢迎一起讨论!