微信小程序 自動刷新 token 記錄
由于對 promise 不熟悉,看了不少資料,踩了不少坑。
為了讓有緣人少踩點(diǎn)坑。這里記錄一下。
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('業(yè)務(wù)正常,返回?cái)?shù)據(jù):response.data', response.data);
console.log('業(yè)務(wù)正常,返回?cái)?shù)據(jù):response.data.data', response.data.data);
resolve(response.data.data);
} else {
console.error('業(yè)務(wù)錯誤:', response.data.message);
reject({
errCode: response.data.code, //業(yè)務(wù)錯誤碼
errMsg: response.data.message
})
}
} else if (response.statusCode == 401) {
console.error('發(fā)生401錯誤,令牌超時無效');
reject({
errCode: 401,
errMsg: 發(fā)生401錯誤,令牌超時無效
})
} else if (response.statusCode == 403) {
console.error('發(fā)生403錯誤,必須登錄');
reject({
errCode: 403,
errMsg: 發(fā)生403錯誤,必須登錄
})
} else {
console.error('服務(wù)器端發(fā)生錯誤,狀態(tài)碼:', response.statusCode);
reject({
errCode: response.statusCode, //https錯誤碼
errMsg: 服務(wù)器端發(fā)生錯誤,狀態(tài)碼: ${response.statusCode}
});
}
},
fail(e) {
console.error('調(diào)用api失敗:', e.errMsg);
reject({
errCode: -1,
errMsg: 調(diào)用api失敗: ${e.errMsg}(${e.errno})
});
}
})
})
}
- 獲取數(shù)據(jù)方法
這里有個重要的操作,是 getAccessToken。獲取 token。用 await 標(biāo)記。
async function fetch(url, header, params, msg) {
params.signature = sign(params); //加簽名
//這個地方如果是個 Promise,不返回的話就掛起,后面的就不執(zhí)行了
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.調(diào)用 wxrequest 的寫法:這里有一個給 header 賦值 token 的操作。你可以改成其他的,看后臺怎么寫了。
這里也有一個 await getAccessToken() 的調(diào)用。
function request(url, params, header, token, msg) {
console.log("3.請求數(shù)據(jù)......");
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("得到的數(shù)據(jù):", data);
return data
});
}
4.let token = await getAccessToken() 的意義
await 等待一個 promise,如果這個 promise 一直是 pending 狀態(tài)的話,就會掛起,一直等待。
在 request 之前調(diào)用,如果正在刷新令牌,則先掛起不請求。
在 request 之后調(diào)用,如果401了,如果已經(jīng)有其他的在刷新令牌了,在重新請求數(shù)據(jù)之前,也掛起來不請求。
當(dāng)刷新令牌完畢,則給掛起來的 promise,resolved 一下,掛起來的 promise 釋放。
如果你的請求不需要 token,其實(shí)只要后臺不檢查就行了。為不為空無所謂。唯一的缺點(diǎn)就是不需要token 的時候,不巧 token 過期了,也要等上一等。
async function getAccessToken() {
console.log("2.1 獲取令牌...");
//如果正在刷新令牌,掛起來
if (isRefreshing) {
console.log("2.2 令牌正在刷新,先掛起來...");
//將被攔截的請求掛起 存到緩存池中
const externalControl = {
resolved: null,
};
這里參考了網(wǎng)上的一段資料,很聰明的做法
// 這里返回了一個新的Promise變相的實(shí)現(xiàn)請求的掛起(只要沒有resolved或rejected,請求就會一直處于pedding狀態(tài))
// 并將Promise狀態(tài)的改變放到了外部一個對象來控制 externalControl ,待定池緩存這個對象即可,待需要執(zhí)行后續(xù)被攔截請求,只需要利用這個對象引用的 resolved 來改變Promise狀態(tài)即可實(shí)現(xiàn)請求掛起的放行
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 釋放掉,其實(shí)就是調(diào)用 resolve 方法,把令牌扔出來,promise 的狀態(tài)改變了。則會執(zhí)行后續(xù)的代碼。
async function updateToken() {
if (isRefreshing) return;
const refreshToken = getApp().globalData.tokenInfo?.refreshToken;
if (!refreshToken) {
// 注意在異步函數(shù)里 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;
//判斷錯誤的類別,要不要重新登錄
// 注意在異步函數(shù)里 throw 異常,外部無法用 catch 捕獲
throw {
errCode: -444,
errMsg: '刷新令牌失敗,必須登錄獲取'
};
} else {
throw e;
}
}
}
async function doUpdateToken(refreshToken) {
isRefreshing = true; //第一個進(jìn)入的修改
return refreshTokenRequest(refreshToken)
.then(tokenInfo => {
getApp().setTokenInfo(tokenInfo);
isRefreshing = false;
const newAccesssToken = tokenInfo.accessToken;
// 用新的token重新發(fā)起待定池中的請求
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);
}
}
不知道看明白了沒?
下面是完整點(diǎn)的代碼,試了試,基本還行。
當(dāng)然還能改造,歡迎高手改造代碼
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; // 用于攔截鑒權(quán)失敗的請求
let pendingRequests = []; // 被攔截請求的緩存池
async function fetch(url, header, params, msg) {
params.signature = sign(params); //加簽名
//這個地方如果是個 Promise,不返回的話就掛起,后面的就不執(zhí)行了
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變相的實(shí)現(xiàn)請求的掛起(只要沒有resolved或rejected,請求就會一直處于pedding狀態(tài))
// 并將Promise狀態(tài)的改變放到了外部一個對象來控制 externalControl ,待定池緩存這個對象即可,待需要執(zhí)行后續(xù)被攔截請求,只需要利用這個對象引用的 resolved 來改變Promise狀態(tài)即可實(shí)現(xiàn)請求掛起的放行
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.請求數(shù)據(jù)......");
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("得到的數(shù)據(jù):", data);
return data
});
}
async function updateToken() {
if (isRefreshing) return;
const refreshToken = getApp().globalData.tokenInfo?.refreshToken;
if (!refreshToken) {
// 注意在異步函數(shù)里 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;
//判斷錯誤的類別,要不要重新登錄
// 注意在異步函數(shù)里 throw 異常,外部無法用 catch 捕獲
throw {
errCode: -444,
errMsg: '刷新令牌失敗,必須登錄獲取'
};
} else {
throw e;
}
}
}
async function doUpdateToken(refreshToken) {
isRefreshing = true; //第一個進(jìn)入的修改
return refreshTokenRequest(refreshToken)
.then(tokenInfo => {
getApp().setTokenInfo(tokenInfo);
isRefreshing = false;
const newAccesssToken = tokenInfo.accessToken;
// 用新的token重新發(fā)起待定池中的請求
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,
};