小程序使用 Promise 优雅的实现重新登录
前言
本文只讨论支持静默登录的场景。如微信公众号,微信小程序,即已经有微信登录态了,那么理论上小程序和公众号除首次登录和特殊需求外,不需要再次手动登录,符合小程序「轻量」理念。
背景
想象一个场景:用户点击个人中心,前端请求接口,这时登录失效,提示用户,用户点击重新登录,前端重新请求接口,渲染页面。
逻辑上没问题,但这里涉及到两点,一是用户需要点击重新登录,体验没问题,但不够优雅。另一个是前端的工作,可能需要考虑用户重新登录的交互逻辑,有开发成本。那么是否有更好的实现呢?
答案是肯定的,当登录失效时,自动静默登录,登录成功后,继续请求接口,数据正常返回给接口调用者。整个过程用户是不需要操作的,只是等待的时间多了一个接口的时间。前端接口请求也无需增加任何逻辑,只需等待 Promise 返回数据即可。
得益于 Promise,整个重新登录的开发,体验也很丝滑,对 Promise 的理解,也更深一些。
思路
- 接口 A 拦截到登录失效,调用重新登录方法
- 重新登录:
- 清除 token、userInfo
- 调用静默登录
- 登录成功后再次调用接口 A 并返回
代码
代码示例使用 uni-app。API 基本等于微信小程序,如 uni.request 和 wx.request 是一样的。
- 登录拦截
const defaultOptions = {
// 请求方法,默认 POST,必须大写
method: "POST",
// 请求路径
url: "",
// 请求参数
data: {},
// 请求头
header: {},
// 请求接口时是否开启loading, 默认开启
loading: true,
// loading文案,只有 loading 为 true 时生效
loadingTxt: "",
// 是否开启弹窗报错,默认为 true 开启
showErrorToast: true,
// 最大重试次数
retryMax: 3,
retryCount: 0,
};
export default function request(options) {
const opt = Object.assign({}, defaultOptions, options);
const token = uni.getStorageSync("token");
if (token) opt.data.token = token;
opt.loading && uni.showLoading({ title: opt.loadingTxt });
const url = `${process.env.VUE_APP_API}/api/${opt.url}`;
opt.$url = url;
return new Promise((resolve, reject) => {
uni.request({
url,
data: opt.data,
method: opt.method,
header: opt.header,
success(res) {
console.log(`request ${opt.url} success`, res);
opt.loading && uni.hideLoading();
// 接口正常响应
if (res && res.statusCode === 200) {
// 接口按预期返回
if (res.data.code === 0) return resolve(res.data);
// token失效, 重新登录
else if (res.data.code === 1) {
// 这里做了最多重试 3 次的限制,避免出现无限重复登录的情况。
if (opt.retryCount < opt.retryMax) {
return reLogin(opt).then(resolve);
} else {
uni.showToast({ title: "API Error: 登录失败", icon: "none" });
return reject("API Error: 登录失败");
}
}
}
handleError(res, opt);
reject(res.statusCode);
},
fail(err) {
opt.loading && uni.hideLoading();
reject(err);
handleError(err, opt);
},
});
});
}
- 重新登录
async function reLogin(opt) {
opt.retryCount++;
uni.showToast({ title: "登陆已过期", icon: "none" });
const count = opt.retryCount;
const time = count > 1 ? `第${count}次` : "";
uni.showLoading({ title: `${time}重新登录...` });
store.commit("SET_USERINFO", null);
store.commit("SET_TOKEN", null);
return store
.dispatch("login/slienceLogin", false)
.then(() => request(opt))
.finally(uni.hideLoading);
}
- 静默登录逻辑补充
export default {
namespaced: true,
actions: {
// 静默登录
async slienceLogin(store, loading = true) {
// 这里是调用 wx.login 获取 code,我封装成了 Promise,Promise 真香~
const { code } = await functions.login();
const body = {
url: "login/silenceLogin",
data: { code },
loading,
};
return request(body).then(({ data }) => {
store.commit("SET_TOKEN", data.token, { root: true });
store.commit("SET_USERINFO", data.userInfo, { root: true });
return data;
});
},
},
};
应用
实际使用时,出现了一个很严重的问题,就是接口并发请求时,会多次调用静默登录,比如 Promise.all([p1, p2])
,如果 token 失效,就会重新登录两次。
因此我们需要维护一个登录状态,如果在正在登录,就不要重复登录,返回正在登录的 Promise 即可。这里使用 vuex 示例,也可以使用其它方式,如 Vue 全局变量。
静默登录完善:
export default {
namespaced: true,
+ state() {
+ return {
+ // 是否正在登录
+ siging: false,
+ // 登录Promise
+ slienceLoginPromise: null,
+ };
+ },
actions: {
// 静默登录
async slienceLogin(store, loading = true) {
const { code } = await functions.login();
const body = {
url: "login/silenceLogin",
data: { code },
loading,
};
+ store.commit("setSiging", true);
+ const p = request(body)
.then(({ data }) => {
store.commit("SET_TOKEN", data.token, { root: true });
store.commit("SET_USERINFO", data.userInfo, { root: true });
return data;
})
+ .finally(() => {
+ store.commit("setSiging", false);
+ store.commit("setPromise", null);
+ });
+ store.commit("setPromise", p);
+ return p;
},
},
+ mutations: {
+ setSiging(state, val) {
+ state.siging = val;
+ },
+ setPromise(state, val) {
+ state.slienceLoginPromise = val;
+ },
+ },
};
重新登录逻辑完善
async function reLogin(opt) {
opt.retryCount++;
uni.showToast({ title: "登陆已过期", icon: "none" });
const count = opt.retryCount;
const time = count > 1 ? `第${count}次` : "";
uni.showLoading({ title: `${time}重新登录...` });
store.commit("SET_USERINFO", null);
store.commit("SET_TOKEN", null);
+ let p = null;
+ // 检测是否正在重新登录,如果是,直接返回正在登录的Promise,避免重复登录
+ if (store.state.login.siging) {
+ p = store.state.login.slienceLoginPromise;
+ } else {
+ p = store.dispatch("login/slienceLogin", false);
+ }
+ return p
.then(() => request(opt))
.finally(uni.hideLoading);
}
完美解决。如有不足,师请雅正。
本文由 咻咻 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jan 3,2021