微信小程序 自动刷新 token 记录
由于对 promise 不熟悉,看了不少资料,踩了不少坑。
为了让有缘人少踩点坑。这里记录一下。
1.封装wx.request
import apiConsts from './apiConsts.js';
export async function wxrequest(url, params, method, header) {
return new Promise((resolve, reject) => {
wx.request({
url: url,
method: method,
data: params,
header: header,
success(response) {
if (response.statusCode === 200) {
if (response.data.code == apiConsts.SUCCESS) {
console.log('业务正常,返回数据:response.data', response.data);
console.log('业务正常,返回数据:response.data.data', response.data.data);
resolve(response.data.data);
} else {
console.error('业务错误:', response.data.message);
reject({
errCode: response.data.code, //业务错误码
errMsg: response.data.message
})
}
} else if (response.statusCode == 401) {
console.error('发生401错误,令牌超时无效');
reject({
errCode: 401,
errMsg: 发生401错误,令牌超时无效
})
} else if (response.statusCode == 403) {
console.error('发生403错误,必须登录');
reject({
errCode: 403,
errMsg: 发生403错误,必须登录
})
} else {
console.error('服务器端发生错误,状态码:', response.statusCode);
reject({
errCode: response.statusCode, //https错误码
errMsg: 服务器端发生错误,状态码: ${response.statusCode}
});
}
},
fail(e) {
console.error('调用api失败:', e.errMsg);
reject({
errCode: -1,
errMsg: 调用api失败: ${e.errMsg}(${e.errno})
});
}
})
})
}
- 获取数据方法
这里有个重要的操作,是 getAccessToken。获取 token。用 await 标记。
async function fetch(url, header, params, msg) {
params.signature = sign(params); //加签名
//这个地方如果是个 Promise,不返回的话就挂起,后面的就不执行了
console.log("2.等待获取 accessToken......");
//只有在更新令牌的时候才挂起
let token = await getAccessToken();
console.log("header", header);
return request(url, params, header, token, msg)
.catch(e => {
if (e.errCode == -444) {
toLogin();
} else {
return Promise.reject(e);
}
});
}
3.调用 wxrequest 的写法:这里有一个给 header 赋值 token 的操作。你可以改成其他的,看后台怎么写了。
这里也有一个 await getAccessToken() 的调用。
function request(url, params, header, token, msg) {
console.log("3.请求数据......");
console.log('request msg:', msg);
console.log('request前的 header:', header);
header['X-TOKEN'] = token;
console.log('赋值token 后的 header:', header);
return wxrequest(url, params, 'POST', header)
.catch(async (e) => {
if (e.errCode == 401) {
updateToken();
let token = await getAccessToken();
return request(url, params, header, token, '重新请求:' + msg);
}
if (e.errCode == 403) {
throw {
errCode: -444,
errMsg: '禁止访问,必须登录'
};
}
return Promise.reject(e);
})
.then(data => {
console.log("得到的数据:", data);
return data
});
}
4.let token = await getAccessToken() 的意义
await 等待一个 promise,如果这个 promise 一直是 pending 状态的话,就会挂起,一直等待。
在 request 之前调用,如果正在刷新令牌,则先挂起不请求。
在 request 之后调用,如果401了,如果已经有其他的在刷新令牌了,在重新请求数据之前,也挂起来不请求。
当刷新令牌完毕,则给挂起来的 promise,resolved 一下,挂起来的 promise 释放。
如果你的请求不需要 token,其实只要后台不检查就行了。为不为空无所谓。唯一的缺点就是不需要token 的时候,不巧 token 过期了,也要等上一等。
async function getAccessToken() {
console.log("2.1 获取令牌...");
//如果正在刷新令牌,挂起来
if (isRefreshing) {
console.log("2.2 令牌正在刷新,先挂起来...");
//将被拦截的请求挂起 存到缓存池中
const externalControl = {
resolved: null,
};
这里参考了网上的一段资料,很聪明的做法
// 这里返回了一个新的Promise变相的实现请求的挂起(只要没有resolved或rejected,请求就会一直处于pedding状态)
// 并将Promise状态的改变放到了外部一个对象来控制 externalControl ,待定池缓存这个对象即可,待需要执行后续被拦截请求,只需要利用这个对象引用的 resolved 来改变Promise状态即可实现请求挂起的放行
const pendingPromise = new Promise((resolved) => {
externalControl.resolved = resolved;
});
pendingRequests.push(externalControl);
return pendingPromise; //返回一个被挂起的 Promise,await 就一直等待
}
//没有正在刷新令牌,返回
const accessToken = getApp().globalData.tokenInfo?.accessToken;
return Promise.resolve(accessToken);
};
- 刷新令牌
如果刷新成功,则将挂起来的 promise 释放掉,其实就是调用 resolve 方法,把令牌扔出来,promise 的状态改变了。则会执行后续的代码。
async function updateToken() {
if (isRefreshing) return;
const refreshToken = getApp().globalData.tokenInfo?.refreshToken;
if (!refreshToken) {
// 注意在异步函数里 throw 异常,外部无法用 catch 捕获
throw {
errCode: -444,
errMsg: '没有刷新令牌,必须登录获取'
};
}
console.log("有 refreshToken,开始刷新令牌", refreshToken);
try {
await doUpdateToken(refreshToken);
} catch (e) {
if (parseInt(e.errCode) > 100000) {
// const REFRESH_TOKEN_IS_TIME_OUT = 900211;
// const REFRESH_TOKEN_IS_INVALID = 900212;
//判断错误的类别,要不要重新登录
// 注意在异步函数里 throw 异常,外部无法用 catch 捕获
throw {
errCode: -444,
errMsg: '刷新令牌失败,必须登录获取'
};
} else {
throw e;
}
}
}
async function doUpdateToken(refreshToken) {
isRefreshing = true; //第一个进入的修改
return refreshTokenRequest(refreshToken)
.then(tokenInfo => {
getApp().setTokenInfo(tokenInfo);
isRefreshing = false;
const newAccesssToken = tokenInfo.accessToken;
// 用新的token重新发起待定池中的请求
pendingRequests.forEach((item) => {
item.resolved(newAccesssToken);
});
// 清空缓存池
pendingRequests = [];
return newAccesssToken;
})
.catch(e => {
isRefreshing = false;
pendingRequests = []; // 清空缓存池???
return Promise.reject(e);
})
};
async function refreshTokenRequest(refreshToken) {
console.log("refreshToken:", refreshToken);
let url = refreshTokenUrl;
let method = 'POST';
let header = headers;
let params = {
refreshToken: refreshToken,
appId: apiBase.appId,
time: new Date().getTime(), //milliseconds
nonce: '',
signature: ''
}
params.signature = sign(params);
try {
let data = await wxrequest(url, params, method, header);
return Promise.resolve(data.token);
} catch (e) {
return Promise.reject(e);
}
}
不知道看明白了没?
下面是完整点的代码,试了试,基本还行。
当然还能改造,欢迎高手改造代码
import apiBase from './apiBase.js';
import apiConsts from './apiConsts.js';
import sign from '../utils/sign.js';
import throwIf from '../utils/assert.js';
import {
headers
} from './apiHeaders.js';
import {
refreshTokenUrl
} from './apiUrls.js';
import {
wxrequest
} from './request.js';
let isRefreshing = false; // 用于拦截鉴权失败的请求
let pendingRequests = []; // 被拦截请求的缓存池
async function fetch(url, header, params, msg) {
params.signature = sign(params); //加签名
//这个地方如果是个 Promise,不返回的话就挂起,后面的就不执行了
console.log("2.等待获取 accessToken......");
//只有在更新令牌的时候才挂起
let token = await getAccessToken();
console.log("header", header);
return request(url, params, header, token, msg)
.catch(e => {
if (e.errCode == -444) {
toLogin();
} else {
return Promise.reject(e);
}
});
}
async function getAccessToken() {
console.log("2.1 获取令牌...");
//如果正在刷新令牌,挂起来
if (isRefreshing) {
console.log("2.2 令牌正在刷新,先挂起来...");
//将被拦截的请求挂起 存到缓存池中
const externalControl = {
resolved: null,
};
// 这里返回了一个新的Promise变相的实现请求的挂起(只要没有resolved或rejected,请求就会一直处于pedding状态)
// 并将Promise状态的改变放到了外部一个对象来控制 externalControl ,待定池缓存这个对象即可,待需要执行后续被拦截请求,只需要利用这个对象引用的 resolved 来改变Promise状态即可实现请求挂起的放行
const pendingPromise = new Promise((resolved) => {
externalControl.resolved = resolved;
});
pendingRequests.push(externalControl);
return pendingPromise; //返回一个被挂起的 Promise,await 就一直等待
}
//没有正在刷新令牌,返回
const accessToken = getApp().globalData.tokenInfo?.accessToken;
return Promise.resolve(accessToken);
};
function request(url, params, header, token, msg) {
console.log("3.请求数据......");
console.log('request msg:', msg);
console.log('request前的 header:', header);
header['X-TOKEN'] = token;
console.log('赋值token 后的 header:', header);
return wxrequest(url, params, 'POST', header)
.catch(async (e) => {
if (e.errCode == 401) {
updateToken();
let token = await getAccessToken();
return request(url, params, header, token, '重新请求:' + msg);
}
if (e.errCode == 403) {
throw {
errCode: -444,
errMsg: '禁止访问,必须登录'
};
}
return Promise.reject(e);
})
.then(data => {
console.log("得到的数据:", data);
return data
});
}
async function updateToken() {
if (isRefreshing) return;
const refreshToken = getApp().globalData.tokenInfo?.refreshToken;
if (!refreshToken) {
// 注意在异步函数里 throw 异常,外部无法用 catch 捕获
throw {
errCode: -444,
errMsg: '没有刷新令牌,必须登录获取'
};
}
console.log("有 refreshToken,开始刷新令牌", refreshToken);
try {
await doUpdateToken(refreshToken);
} catch (e) {
if (parseInt(e.errCode) > 100000) {
// const REFRESH_TOKEN_IS_TIME_OUT = 900211;
// const REFRESH_TOKEN_IS_INVALID = 900212;
//判断错误的类别,要不要重新登录
// 注意在异步函数里 throw 异常,外部无法用 catch 捕获
throw {
errCode: -444,
errMsg: '刷新令牌失败,必须登录获取'
};
} else {
throw e;
}
}
}
async function doUpdateToken(refreshToken) {
isRefreshing = true; //第一个进入的修改
return refreshTokenRequest(refreshToken)
.then(tokenInfo => {
getApp().setTokenInfo(tokenInfo);
isRefreshing = false;
const newAccesssToken = tokenInfo.accessToken;
// 用新的token重新发起待定池中的请求
pendingRequests.forEach((item) => {
item.resolved(newAccesssToken);
});
// 清空缓存池
pendingRequests = [];
return newAccesssToken;
})
.catch(e => {
isRefreshing = false;
pendingRequests = []; // 清空缓存池???
return Promise.reject(e);
})
};
async function refreshTokenRequest(refreshToken) {
console.log("refreshToken:", refreshToken);
let url = refreshTokenUrl;
let method = 'POST';
let header = headers;
let params = {
refreshToken: refreshToken,
appId: apiBase.appId,
time: new Date().getTime(), //milliseconds
nonce: '',
signature: ''
}
params.signature = sign(params);
try {
let data = await wxrequest(url, params, method, header);
return Promise.resolve(data.token);
} catch (e) {
return Promise.reject(e);
}
}
//前往登录页面
function toLogin() {
wx.showToast({
title: '登录失败,请重新登录',
icon: 'none',
success: () => {
setTimeout(() => {
wx.reLaunch({
url: '/pages/userLogin/userLogin',
})
}, 1200);
}
})
}
export {
fetch,
};