在实现前端页面和功能之前, 首先需要确保我们(小程序前端)向服务器发起的网络请求能够得到确认和响应,不然一切都是徒劳。 因此,通过 uniapp 官方文档可以看到 —— 官方向我们提供了网络请求的API 。见下图:
经测试(为了体现测试的真实性,并没有在本机上进行测试,而是购买了一台服务器,将后端测试代码和数据库中的数据部署在购买的服务器中,访问的BASE_URL是服务器的外网IP:端口号)
使用官方文档提供的API能够得到我们服务器的确认和响应。
但是,官方文档提供的API并不能满足我们的需求 —— 基于前期的需求分析,为了保证用户数据的安全性和用户体验的良好性,团队决定采取并使用 “双TOKEN” 的网络请求机制。这一机制决定了我们需要对请求进行拦截 —— 在请求头上添加TOKEN。 故需要对官方文档提供的API进行再次封装以满足我们的需求。经结合相关资料,对uniapp网络请求进行了封装。详细介绍在代码中的注释上。
/**
* 通用uni-app网络请求
* 基于 Promise 对象实现更简单的 request 使用方式,支持请求和响应拦截
*/
export default {
// 请求配置中心
config: {
// 服务器url: "http://外网IP地址:端口号/"
// 本机url: "http://localhost:8080/"
baseUrl: "http://xxx.xxx.xxx.xxx:xxx/",
// 请求头
header: {
'Content-Type': 'application/json;charset=UTF-8',
'Content-Type': 'application/x-www-form-urlencoded'
},
// 请求携带的数据
data: {},
// 请求默认方法为GET请求
method: "GET",
// 数据类型为JSON
dataType: "json",
/* 如设为json,会对返回的数据做一次 JSON.parse */
responseType: "text",
// 请求成功后调用的方法
success() {},
// 请求失败后调用的方法
fail() {},
// 请求完成时调用的方法
complete() {}
},
// 请求和响应拦截器
interceptor: {
request: null,
response: null
},
// 封装官方文档提供的API:uni.request
request(options) {
if (!options) {
options = {}
}
// 请求配置参数
options.baseUrl = options.baseUrl || this.config.baseUrl
options.dataType = options.dataType || this.config.dataType
options.url = options.baseUrl + options.url
options.data = options.data || {}
options.method = options.method || this.config.method
// 返回Promise
return new Promise((resolve, reject) => {
let _config = null
// 设置响应拦截器
options.complete = (response) => {
let statusCode = response.statusCode
response.config = _config
if (this.interceptor.response) {
let newResponse = this.interceptor.response(response)
if (newResponse) {
response = newResponse
}
}
// 请求成功
if (statusCode === 200) {
resolve(response);
} else { // 请求失败
uni.showToast({
title: "未知错误",
icon: 'error',
duration: 2000,
mask: true
})
reject(response)
}
}
// 请求配置
_config = Object.assign({}, this.config, options)
_config.requestId = new Date().getTime()
// 设置请求拦截器
if (this.interceptor.request) {
this.interceptor.request(_config)
}
// 调用官方文档提供的API,发起请求
uni.request(_config);
});
},
// 以下请求可以单独调用
// GET 请求
get(url, data, options) {
if (!options) {
options = {}
}
options.url = url
options.data = data
options.method = 'GET'
return this.request(options)
},
// POST 请求
post(url, data, options) {
if (!options) {
options = {}
}
options.url = url
options.data = data
options.method = 'POST'
return this.request(options)
},
// PUT 请求
put(url, data, options) {
if (!options) {
options = {}
}
options.url = url
options.data = data
options.method = 'PUT'
return this.request(options)
},
// DELETE 请求
delete(url, data, options) {
if (!options) {
options = {}
}
options.url = url
options.data = data
options.method = 'DELETE'
return this.request(options)
}
}
/**
* 将业务所有接口统一起来便于维护
*/
import http from './interface'
export const test = (data) => {
// 设置请求拦截器
http.interceptor.request = (config) => {
console.log('请求拦截', config)
config.header = {
// 在发起请求之前,让请求的header携带TOKEN
"token": uni.getStorageSync('access_token')
}
}
// 设置响应拦截器
http.interceptor.response = (response) => {
if (response.code === 200) {
// 请求成功的执行逻辑
console.log('响应拦截', response)
} else { // 请求失败的执行逻辑
uni.showToast({
title: response.msg,
icon: 'none',
duration: 2000,
mask: true
})
}
return response;
}
// 发起test请求
return http.request({
baseUrl: 'http://localhost:8080/',
url: 'test',
method: 'POST'
data: data
})
}
/**
* 登录业务
*/
export const login = (username, password) => {
// 设置响应拦截器
http.interceptor.response = (response) => {
// 登录成功,则将两个 token 存在用户手机磁盘上,并跳转到首页
if (response.code == 200) {
uni.setStorageSync('access_token', response.data.access_token)
uni.setStorageSync('refresh_token', response.data.refresh_token)
uni.switchTab({
url: '/pages/index/index'
})
} else { // 登录失败则弹窗提示相应消息给用户
uni.showToast({
title: response.msg,
icon: 'none',
duration: 2000,
mask: true
})
}
return response;
}
// 发起登录请求
return http.request({
url: 'xxx/' + username +'/' + password,
method: 'POST',
})
}
// 默认全部导出 import api from '@/common/http/api'
export default {
test,
login
}
首先,在登录之前是没有双TOKEN字段的
发起登录请求成功后,根据业务逻辑,设置双TOKEN到磁盘上,同时跳转到首页。 可以看到响应拦截打印出来的请求配置参数,由于后端的代码是测试代码,因此没有严格按照POST请求进行登录验证,而采用了GET
可以看到,我们的“武器还是很锋利的” —— 微信小程序的请求封装还是很成功的。 说明我们不仅能够向服务器发起请求,还能对所有的请求和响应进行拦截,从而实现请求和响应方面相应的业务逻辑。因此,现在只需要专注于页面业务逻辑的实现即可。
为什么现在只需要专注页面业务逻辑即可呢?
比如:“双TOKEN” 网络请求机制的业务逻辑 —— 用户发起带ACCESS_TOKEN的请求,成功了没啥要说的。
主要是ACCESS_TOKEN过期后的逻辑: ACCESS_TOKEN过期后,会从服务器返回的响应中接收到相应的状态码(比如405),前端收到405后,会发起携带REFRESH_TOKEN的请求,让服务器重新发俩新的TOKEN回来。正常情况下服务器会返回俩新的TOKEN,但是假如REFRESH_TOKEN也过期了,那没办法,用户只能重新登录了。这请求和响应方面的逻辑在后期都可以进行修改,并不影响页面业务逻辑。
既然说到了 “双TOKEN” 网络请求机制, 那就再讲一下我们团队为什么选择这样一个机制:
众所周知,携带TOKEN的优势:能够减少敏感数据在网络上进行传递,从而保证用户数据的安全性 —— “你”登录后,“你”每次发起的请求都是携带TOKEN的,不仅很少有其他敏感数据在网上进行传递,因为携带TOKEN还能让服务器知道“你”是谁。同时在一定程度上可用保证用户体验 —— 比如自动登录,只需要存在TOKEN并且TOKEN不过期,那么用户可以直接登录,不需要每次进来都重新输入用户名和密码登录,也保证了一定的安全性。
但是 “单TOKEN机制并不完美” —— 假如发起的请求携带TOKEN过期了,那么会强制用户重新登录,假如用户用几个小时编写完文档,美滋滋的发起提交请求,然后因为TOKEN过期了,你居然让用户重新登录,并且文档数据还没有保存,那不是裂开了吗? 而双TOKEN机制就可以尽可能的避免这种情况的发生,从而提供良好的用户体验。