微信小程序初始化登录方案

2,534 阅读3分钟

在小程序开发过程中 , 为了避免重复发送登录请求 , 通常会将用户登录操作放置在小程序初始化中 , 即 app.js 中的 onLaunch 生命周期 。 但该生命周期与页面初始化生命周期是同步进行的 。 此时如果在页面初始化中 , 发送了需要携带用户登录态的请求 , 可能出现如下情况 :

img

因为小程序初始化及页面初始化是同步进行的 。 若页面初始化完成发送请求前 , 小程序初始化中登录请求仍未响应 。 此时就会导致未携带 token 或其他鉴权信息 ,鉴权失败 。 最初为了解决该问题 , 引入了特殊生命周期方案 。

特殊生命周期方案

通过在页面中植入一个特殊的生命周期 onInit , 等小程序登录请求完成后获取当前页面实例进行调用 。 但这种方案有两个缺点 。

  • 侵入性强 , 每个需要鉴权发送请求的页面都需要植入该生命周期
  • 产品需求上 , 通常会有先填入首页 , 再从首页跳入二级页的需求 , 这样能让用户回退一次后,仍然能回到首页 。 该方案需要在登录完成后获取所有页面实例依次进行调用

该方案非常不优雅 。

后来 , 设计了第二种方案 : 请求队列方案

请求队列方案

多路复用方案

在需要用户鉴权信息的地方 , 我们统一调用 login 方法 。 在方法内部我们通过维护了一个请求队列 。 将后续请求都放置到队列中 , 在首次请求完毕后 , 取出所有待完成的请求一一响应 。 此时就无需侵入业务中 , 做到了登录请求复用的效果 。

let loginDoing = false;
const loginEvent = [];

const userProfile = observable({
  user: {
    avatar: '',
    isCompleted: false,
    nickname: '',
    uid: 0,
    token: '',
  },
  async loginProcess() {
    if(this.user.token) {
      return this.user;
    }
    loginDoing = true;
    let code;
    try {
      const codeResult = await Taro.login();
      if(codeResult.errMsg !== 'login:ok') {
        throw new Error('Taro.login 失败');
      }
      code = codeResult.code;
    } catch (e) {
      loginDoing = false;
      throw e;
    }
    const result = await post(URL().user.login, {
      code,
    });
    let user = {
      ...result.user,
      token: result.token,
    };
    this.user = user;
    loginDoing = false;
    setTimeout(() => {
      let length = loginEvent.length;
      for(let i = 0; i < length; i++) {
        loginEvent.pop()(user);
      }
    });
    return user;
  },
  login() {
    if(loginDoing) {
      return new Promise((resolve) => {
        loginEvent.push(resolve);
      });
    } else {
      return this.loginProcess()
    }
  },
});

上面的代码中 , 我们通过存储一个队列来完成请求的复用 。 在此基础上 , 我们进行一下优化 。

更优雅的复用方案

在请求队列的使用中 , 我们再封装一层来适配各不同登录逻辑 。

function reuse(process) {
    let store;
    let doing = false;
    let requests = [];

    function dispatch (mode, result) {
        requests.forEach((request) => {
            request[mode](result)
        });
        requests = [];
    }

    return function login () {
        return new Promise(async (resolve, reject) => {
            if(store) {
                resolve(store);
                return;
            }
            requests.push({
                resolve,
                reject,
            });
            if(doing) {
                return;
            }
            doing = true;
            try {
                const processResult = await process();
                store = processResult;
                dispatch('resolve', processResult);
            } catch (e) {
                dispatch('reject', e);
            }
        })
    }
}

使用各场景验证一下效果

const requestLogin = reuse(() => {
    return new Promise(resolve => {
        console.log('===处理登录请求===');
        setTimeout(() => {
            console.log('===登录请求响应===');
            resolve({
                token: 'mock'
            });
        }, 1000);
    });
});

requestLogin().then(e => console.log('连续发起请求: A', e));
requestLogin().then(e => console.log('连续发起请求: B', e));

setTimeout(() => {
    requestLogin().then(e => console.log('未完成初始化请求时发起请求', e));
}, 500);

setTimeout(() => {
    requestLogin().then(e => console.log('后续发起请求A', e));
}, 2000);

// Console Result :
// ===处理登录请求===
// ===登录请求响应===
// 连续发起请求: A { token: 'mock' }
// 连续发起请求: B { token: 'mock' }
// 未完成初始化请求时发起请求 { token: 'mock' }
// 后续发起请求A { token: 'mock' }
// 后续发起请求B { token: 'mock' }

可以看到 , 我们在连续发送请求 、 未完成初始化请求时发送请求 、 后续发送请求场景中 , 都可以得到之前请求返回的结果 。 且只进行了一次登录请求 。 符合我们的预期

结语

在实际应用中 , 该封装还可以用在更多场景中 。

如果各位有更好的思路欢迎评论交流

我已经将这部分代码已经发布到了 GithubNPM 中 , 后续会在上面更新更多在小程序开发中的奇技淫巧类库 。 欢迎大家关注 。