小程序使用 Promise 优雅的实现重新登录
in JavaScript with 0 comment

小程序使用 Promise 优雅的实现重新登录

in JavaScript with 0 comment

小程序使用 Promise 优雅的实现重新登录

前言

本文只讨论支持静默登录的场景。如微信公众号,微信小程序,即已经有微信登录态了,那么理论上小程序和公众号除首次登录和特殊需求外,不需要再次手动登录,符合小程序「轻量」理念。

背景

想象一个场景:用户点击个人中心,前端请求接口,这时登录失效,提示用户,用户点击重新登录,前端重新请求接口,渲染页面。

逻辑上没问题,但这里涉及到两点,一是用户需要点击重新登录,体验没问题,但不够优雅。另一个是前端的工作,可能需要考虑用户重新登录的交互逻辑,有开发成本。那么是否有更好的实现呢?

答案是肯定的,当登录失效时,自动静默登录,登录成功后,继续请求接口,数据正常返回给接口调用者。整个过程用户是不需要操作的,只是等待的时间多了一个接口的时间。前端接口请求也无需增加任何逻辑,只需等待 Promise 返回数据即可。

得益于 Promise,整个重新登录的开发,体验也很丝滑,对 Promise 的理解,也更深一些。

思路

  1. 接口 A 拦截到登录失效,调用重新登录方法
  2. 重新登录:
    • 清除 token、userInfo
    • 调用静默登录
    • 登录成功后再次调用接口 A 并返回

代码

代码示例使用 uni-app。API 基本等于微信小程序,如 uni.request 和 wx.request 是一样的。

  1. 登录拦截
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);
            },
        });
    });
}
  1. 重新登录
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);
}
  1. 静默登录逻辑补充
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);
}

完美解决。如有不足,师请雅正。