前言
虽然最终成果是封装一个uniapp的网络网络模块,但思路是不同开发语言都相通的,自觉还是有点参考价值的
项目地址:uniapp-frame: uniapp开发框架 (gitee.com)
一段网络请求可以分为三个步骤
- 请求前
- 主要工作如下:
- 设置接口请求地址
- 设置参数
- 请求头
- 请求方法
- 请求中
- 客户端与服务端建立连接,发送数据。这一步骤uniapp已经替我们做了,开发者只需要调用uni.request
- 请求后
- 主要工作如下:
- 设置请求成功,错误回调
- 数据转换,网络错误处理
- 回调到UI
可以优化和封装的是请求前,请求后。 请求中 的工作uniapp平台替我们做好了。一个完整的网络请求流程如下。
插入语:封装不是拍脑瓜子,想一出是一出,一定是有原因,有根据的。文本网络请求封装的根据是官方提供的uni.request() 。根据日常使用经验,总结哪里不方便,重复代码多,运用累计的编程知识进行封装 (经验不足的程序员是干不好封装代码这个活的)
uni.request({
url:"https://www.wanandroid.com/article/list/0/json",
method:"GET",
header:{},
data:{
"page_size ":10,
},
success: (response) => {
//todo 解析数据
let statusCode = response.statusCode;
//判断网络状态码
if (statusCode === 200) {
let httpData = response.data;
//接口业务码
if (httpData.errorCode === 0) {
let data = httpData.data;
console.debug("=======================success============================");
console.debug(data);
} else {
//接口业务码错误
let code = httpData.errorCode;
let message = httpData.errorMsg;
}
} else {
//网络状态码错误
let code = statusCode;
let message = response.errMsg;
}
},fail: (error) => {
//todo 错误处理
console.debug("=========================fail==========================");
// {"errMsg":"request:fail"}
let code = -1;
let message = error.errMsg;
}
})
根据划分请求前,中,后的思路 分析上述代码 可以按照下图方式划分。
优化思路分析
请求中 客户端与服务端交互 开发人员是无法控制的。咱不用管
请求前存在如下可优化的点
- url,原始调用方式 传递完整的接口路径
- www.wanandroid.com/article/lis… 。
- 在实际开发中,接口服务器最少会分为两个环境,开发环境和生产环境。还有可能连接同事的电脑进行联调测试。
- 相同点是接口资源地址article/list/0/json 不同点是服务器地址www.wanandroid.com/
- 所以在各种组件,封装中会出现baseUrl
- method
- 确定这次请求的请求方式,常见的就两种get,post,
- post请求 根据设置的请求头content-type的内容不同,有三种提交数据的方式
- application/json。 表明 requestBody中的数据是 json字符串
- application/x-www-form-urlencoded。表明 requestBody中的数据是 key,value键值对
- multipart/form-data。表明 有文件需要上传
- 仔细想想前端的网络基本上也就这四种情况。所以可抽象出四种方法供外界调用。
- get
- postJson
- postKV
- upload
- header 和 data 。
- 一般来说用来传递业务参数,有些接口有参数,有些接口没有参数,没什么共性,看起来不需要封装
- 但是真实项目中,肯定会有服务端参数校验,会传递公共参数。比如:用户token,平台,版本号,加密参数,时间戳巴拉巴拉的。 这种情况下,就需要统一处理,不可能前端写一个业务接口 就复制公共参数的逻辑。
请求后优化逻辑梳理
- 请求后做了一件事,就是解析数据。从成功和失败两方面解析数据
- 从代码注释可以看出,需要判断网络状态,接口业务码全部成功之后 才能够取出数据
- 如果有一个位置出错了,则进入错误逻辑
- 在错误逻辑中,声明了两个字段
- code,错误码
- message,错误信息
- 大多数情况UI 只会使用message 进行错误提示,弹个toast。 在特殊情况,当前接口逻辑复杂,当发生了某种错误时,前端需要进行逻辑处理时,code就起作用了。
- 最典型的用例是 用户token失效,接口返回错误码9527,前端添加判断,如果code === 9527 则清空用户信息,跳转到登录页面。
- 定义code 和 message 还有一个好处是 结构清晰。如果网络请求出错误,把错误信息传递到UI层时。UI层接收的数据结构始终是一致的:code,message
- 在错误逻辑中,声明了两个字段
- 在未封装的情况下,一次网络请求有4处于UI的数据交互。一处成功三处错误。UI进行错误处理很麻烦,有很多重复代码
- 小总结,在数据解析的过程中,大部分代码都是模板代码不会变的。我们只需要提供成功和失败的回调方法,把解析后的数据传递到UI层,让UI层能够拿来就用。
封装目的
- 代码分层
- 把网络请求 和 处理的数据的逻辑 移入另一个js文件 保证vue文件的尽量干净
- 添加公共参数
- 在拦截器中处理
- 优化回调
- 使用Promise 或者Callback回调函数处理网络请求回调,
- 设置baseUrl,支持多域名
- 请求时不写baseUrl,填写相对路径 但是要添加一个前缀(key) 比如:api/article/list/0/json
- api就是前缀,当作key。通过key找到对应的value,即baseUrl
- 在拦截器中 取出url,解析成两部分 前缀(api) 和 真正的接口地址(/article/list/0/json)
- 代码 保存一个map。key:前缀,value:baseUrl 或者 用 if 判断也可以。 能找到对应的baseUrl就行
- 请求后处理数据转换,网络错误
- 在拦截器中拿到返回结果,统一处理
- 日志输出
- 在拦截器拿到request 和 response 控制台输出即可
- 添加公共参数
- 拦截器 拿到request中header,根据业务赋值即可
阶段问题总结
短暂的总结一下目前的核心思路有两点
- 使用Promise 简化网络请求的使用
- 利用官方提供的拦截器完成多域名,数据转化,网络错误处理,日志输出,添加公共参数
多域名,日志输出,公共参数根据上述思路可以完成。
但是做接口数据转换和错误处理的时候遇到了麻烦如下:
- 根据文档 uni.request 请求成功返回 response 只有:data,statusCode,header,cookies
- 不同的服务器返回的数据结构可能不一样,所以就必须进行业务区分
- uni.request 返回的response 没有数据返回可以帮助我们做业务判断 那么就得想别的办法
- 还有一点官方拦截器 无法修改返回的数据结构
- 所以官方拦截器的思路就被pass了
现在的问题是如何处理多域名情况下的数据转化和错误处理。这块的逻辑肯定是不能节省的,要为不同的域名编写不同的逻辑。
新的封装思路
在上述拦截器的封装思路中,其实陷入一点点误区,如下:
- 拦截器一统天下,企图使用拦截器处理所有的业务情况。把拦截器的优先级抬的过高。
- 拦截器是帮助开发人员处理网络请求公共业务的。但正确的思路应该是先有 服务端 后有拦截器
- 用面向对象的思路来说,前端先创建一个 服务端对象(HttpClient 网络请求客户端),而拦截器是服务端对象的一个属性
- 因为每个服务端的逻辑都不一样,前端不应该预先设定有拦截器,而是否要定义拦截器,是根据服务端逻辑确定。如下图所示
理清楚拦截器与服务端的关系之后,突然有种茅塞顿开的感觉。虽然使用的是js,但用面向对象的思路去封装是目前能够想到的最好的办法。如下图
封装的思路上图应该表达的很清楚,一个域名(服务端)对应一个前端的 XXXService。每个XXXService可以维护自己的业务逻辑,对外只开get,post方法。
这样的封装方式,可拓展性很强,随便添加逻辑,XXXService对象之间互不影响。对于调用端来说,只需要调用XXXService的get,post方法。
emm 感觉挺完美 哈哈哈哈
拦截器讨论
这个封装中没有拦截器的概念,想加随时可以加,但感觉没有必要。在web端axios,Android端okHttp这些知名的网络请求库都提供了拦截器功能。在日常工作中,对于拦截器的使用大多用来添加公共参数 和 处理返回的数据。
现有的封装完全能够满足日常需求,而且可扩展性很强。好的代码不是使用多牛逼的技术和封装思路,尽量简单的情况下能够完美解决问题就是好的代码
所以没有引入拦截器的概念