泛型接口的系统化知识整理(基于ArkTS)-登录请求的封装改造案例
本次项目基于interview_handbook项目,目前分章阐述其核心功能和知识要点。
如需源码链接,欢迎联系笔者获取。
篇章分为以下部分:
一、问题概述
在项目编写过程中,经常要使用http请求API接口的方式向服务器获取数据,这个操作的过程中,要经常收发http信息,涉及http请求和服务器返回数据的过程,代码较为繁琐,是否能采用某种手法简化这个部分的操作? 思路:此时,我们可以采用封装的方法。 于是有以下几个问题:
(1)封装有哪些前提?封装什么?
(2)封装有哪些易错点、难点?
(3)怎么引用封装?
(4)本次案例的经验总结
下面通过登录请求的封装阐述以上三个问题
二、错误代码分析与修正
第一阶段:对Html的Get和Post功能进行封装,采用不使用泛型的方式(出BUG)
typescript
import { http } from '@kit.NetworkKit'
import { iLoginUserModel, iResponseModel } from '../../models/AccountModel'
import { promptAction, router } from '@kit.ArkUI'
// 一、提取所有请求的url中的域名当做一个基本地址
const baseUrl = 'https://api-harmony-teach.itheima.net/hm/'
// 二、对Http的Get和Post方法进行封装,写入HdHttp类中
export class HdHttp {
// 1.统一封装post请求方法
static async post(url: string, extraData?: Object) {
try {
// 1.0 创建http请求对象,并将请求对象命名为options
const httpReq = http.createHttp()
let options: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
header: {
"Content-Type": "application/json"
},
expectDataType: http.HttpDataType.OBJECT
}
// 1.1 判断extraData可选参数如果不为空,则要将其追加到options对象中
if (extraData) {
options.extraData = extraData
}
// 1.2从AppStorage('user')中获取数据token,如果有值则追加到options.header中的Authorization
let user = AppStorage.get('user') as iLoginUserModel
// 使用user?写法,就能规避bug:在undefined上点出token这个属性报错
let token = user?.token
if (token && options.header) {
options.header['Authorization'] = `Bearer ${token}`
}
// 1.3 将动态构建好的options参数和url传给request->发请求获取服务器的响应数据
url = baseUrl + url
let res = await httpReq.request(url, options)
// 1.5 优化功能:增加对登录是否成功的校验
// 增加服务器响应的code如果为401,是token失效,则重新登录获取新的有效token
let resdata = res.result as iResponseModel
// 如果服务器响应回来的数据不为10000,则表示有错误,则提示出错误信息
if (resdata.code !== 10000) {
// 如果服务器响应回来的code为401表示token失效,应该重新登录
if (resdata.code === 401) {
// token失效,重新登录
promptAction.showToast({ message: 'token失效,请重新登录' })
router.replaceUrl({ url: 'pages/LoginPage' })
} else {
promptAction.showToast({ message: resdata.message })
}
}
// 1.4 将服务器的数据返回
return resdata
} catch (err) {
promptAction.showToast({ message: '网络请求错误' })
return Promise.reject(err)
}
}
// 2. 如果要向get请求的url传参,有调用者决定来是否在url后面进行拼接
// get('clockinInfo')
// get('clockinInfo?year=2024&moth=04')
static async get(url: string) {
try {
// 2.1 创建向服务器传信息的对象
// 2.1.1 获取http对象以及要传的options对象
const httpReqeust = http.createHttp()
let options: http.HttpRequestOptions = {
header: {
"Content-Type": "application/json"
},
expectDataType: http.HttpDataType.OBJECT
}
// 2.2 获取token传给服务器
let user = AppStorage.get('user') as iLoginUserModel
let token = user?.token
if (token && options.header) {
options.header['Authorization'] = `Bearer ${token}`
}
// 2.3 发请求
url = baseUrl + url
let res = await httpReqeust.request(url, options)
// 2.4 处理响应判断code不为10000的情况和401的情况
// 增加服务器响应的code如果为401表示token失效,则重新登录获取新的有效token
let resdata = res.result as iResponseModel
// 如果服务器响应回来的数据不为10000,则表示有错误,则提示出错误信息
if (resdata.code !== 10000) {
// 如果服务器响应回来的code为401表示token失效,应该重新登录
if (resdata.code === 401) {
// token失效,重新登录
promptAction.showToast({ message: 'token失效,请重新登录' })
router.replaceUrl({ url: 'pages/LoginPage' })
} else {
promptAction.showToast({ message: resdata.message })
}
}
// 2.5 将服务器的数据返回
return resdata
} catch (err) {
promptAction.showToast({ message: 'get请求网络异常' })
return Promise.reject(err)
}
}
}
修改完善代码,使用泛型封装后得出正确的代码:
typescript
export class HdHttp {
// 1.统一封装post请求方法
static async post<T>(url: string, extraData?: Object) {
try {
// 1.0 创建http请求对象,并将请求对象命名为options
const httpReq = http.createHttp()
let options: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
header: {
"Content-Type": "application/json"
},
expectDataType: http.HttpDataType.OBJECT
}
// 1.1 判断extraData可选参数如果不为空,则要将其追加到options对象中
if (extraData) {
options.extraData = extraData
}
// 1.2从AppStorage('user')中获取数据token,如果有值则追加到options.header中的Authorization
let user = AppStorage.get('user') as iLoginUserModel
// 使用user?写法,就能规避bug:在undefined上点出token这个属性报错
let token = user?.token
if (token && options.header) {
options.header['Authorization'] = `Bearer ${token}`
}
// 1.3 将动态构建好的options参数和url传给request->发请求获取服务器的响应数据
url = baseUrl + url
let res = await httpReq.request(url, options)
AlertDialog.show({message:JSON.stringify(res.result,null,2)})
// 1.5 优化功能:增加对登录是否成功的校验
// 增加服务器响应的code如果为401,是token失效,则重新登录获取新的有效token
let resdata = res.result as iResponseModel
// 如果服务器响应回来的数据不为10000,则表示有错误,则提示出错误信息
if (resdata.code !== 10000) {
// 如果服务器响应回来的code为401表示token失效,应该重新登录
if (resdata.code === 401) {
// token失效,重新登录
promptAction.showToast({ message: 'token失效,请重新登录' })
router.replaceUrl({ url: 'pages/LoginPage' })
} else {
promptAction.showToast({ message: resdata.message })
}
}
// 1.4 将服务器的数据返回
return resdata
} catch (err) {
promptAction.showToast({ message: '网络请求错误' })
return Promise.reject(err)
}
}
}
三、细节知识点梳理
上述代码完整版本编写步骤与逻辑整理如下:
一、提取所有请求的url中的域名当做一个基本地址
二、对Http的Get和Post方法进行封装,写入HdHttp类中
统一封装post请求方法
1.0创建http请求对象,并将请求对象命名为options
1.1 判断extraData可选参数如果不为空,则要将其追加到options对象中
1.2从AppStorage('user')中获取数据token,如果有值则追加到options.header中的Authorization
1.3 将动态构建好的options参数和url传给request->发请求获取服务器的响应数据
1.4 将服务器的数据返回
1.5 优化功能:增加对登录是否成功的校验
封装get请求方法
------如果要向get请求的url传参,由调用者决定来是否在url后面进行拼接(拼接方式示例:get('clockinInfo')、get('clockinInfo?year=2024&moth=04'))
2.1 创建向服务器传信息的对象
2.2 获取token传给服务器(存于AppStorage中)
2.3 发送http请求
2.4 处理http响应,判断响应数据的集中情况
2.5 将服务器的响应数据返回
将本项目中的相关get、post的html代码需要替换为封装后的简化使用方式:
目前优先操作三个替换位置:
位置1:登录请求改造
位置2:获取当年当月的连续打卡数据
位置3:完成打卡动作
以替换位置1为例:
在登录请求改造中,由于没有定义泛型,无法确定post相关的数据结构,代码的编写一团雾水。
出现了很多类型相关的报错
那么这里post相关的数据结构具体指哪方法的数据结构呢?
根据以下的对比分析做一下思考和总结:
(1)如以上两个代码片段的分析可知,打卡动作中,主要有http请求信息、获取服务器响应结果(并进行数据转换)等步骤,期中需要封装的主要是http请求发送并获取相应结果的步骤。因此,泛型T在之列代表的是返回的结果。
(2)在上面的两个需要封装的内容中,由两个部分的信息是变化的,图中用方框记行了表示,Get方法的封装中有两个方框,分别代表了要传入封装方法中的参数和之后要返回的结果的数据类型
以POST方法在原LoginPage未封装(待封装)的部分进行说明
这里要对http请求返回的完整结果和经过处理后的结果做一个区分:如图,http返回的完整结果是包含responseCode、cookies、date、result等字段的完整内容,而result的格式则是我们定义定义的接口所返回的必要格式内容,分别由success、code、message、data四个字段组成,而data作为键的字段所带出的值对象又包含有丰富的信息,这些信息也是由开发者设定的,通常由前端后端讨论后,通过后端程序员编写接口文档和相关后端服务器功能来实现。
http请求返回的完整结果:
四、总结
根据以上梳理,对文章开头提出的问题我们可以得出这些总结:
1、封装有哪些前提?封装什么?
通常是重复使用的代码、操作过程复杂的代码才需要进行封装。
2、封装有哪些难点、易错点
(1)难点:封装复杂的代码时,尤其是内容功能多的代码时,如何梳理要封装的内容,如何确定要封装部分的参数、返回值
(2)易错:要封装替代的内容有多重返回值结果类型,使用不确认返回值结果类型的方式(即不使用泛型的方式)的方式封装,容易导致错误
3、怎么引用封装?
建立工具文件夹,专门存放各门类的封装工具(如本项目的封装文件均存放在ets/common/util文件夹下)
通过导入可引用的ArkTS文件中的类进行引用。(如本案例引用HdHttp类:import { HdHttp } from '../common/utils/request')
4、本次案例的经验总结
(1)http请求的原始结果是很长的一串数据,需要的返回结果存放在原始返回结构的result字段下。result字段则具有特定的结构,可由前后端协作制定
(2)在封装文件中,第一步先提取所有请求的url中的域名当做一个基本地址,这种方法有利于服务端更换(门户)地址时的代码修改,也有利于减少代码的冗余
(3)对于有多种返回值类型的方法体系需要封装时,一定要使用泛型界定返回值类型,提供更精确的识别、查改错误的功能