微信小程序 【关键部分】

发布于:2024-05-07 ⋅ 阅读:(24) ⋅ 点赞:(0)

1. 动机

最近在开发小程序,小程序既需兼顾针对新用户的内容预览,又要为注册用户提供服务,简单梳理下,基本需求如下:

  1. 小程序共三个tab页,所有用户都可以浏览首页内容,了解我们可以提供的优质服务;
  2. 进入其他两个页面之后,如果用户没有登录,那就显示登录按钮,如果登录了,则显示服务内容;
  3. 用户在一个页面登陆之后,全局生效。

就这么个看起来很简单的需求,也经过了如下迭代:

  1. 将登录状态和凭据存储在 App.globalData.authorize 中,每个需要授权的页面 onload 生命周期检查 App.globalData.authorize.authorized ,为 true 时渲染服务内容,为 false 则显示登录按钮;
  2. 但如果打开了需要授权的页面 A 但是没有登录,再打开页面 B 登录,这时候回到 A 页面,登录按钮赫然在眼,这是因为 A 页面的 onload 回调函数只执行了一次;
  3. 为了能在 A 页面及时共享 B 页面登录后的状态,在 A 页面的 onshow 生命周期里再获取了一次登录状态,但这样一来,打开 A 页面的时候,会出现短暂的白屏,用户甚至有可能看到按钮变成服务内容的整个过程。

翻遍小程序 API 文档 ,也没有发现用于监听登录的生命周期,就算有也用不了,因为我们有着自己的账号体系,服务端认证完毕才算真正的登录成功。
所以我决定自己包装原有的 Page 函数,添加一个 onauth 生命周期——

2. 事件总线

首先是自定义登录事件的触发与监听,官方的 EventChannel 需要向后兼容,横竖是个订阅回调,那我还不如自己撸一个得了:

/**
 * @file utils/event.js
 */

/**
 * @const EMPTY_HANDLER
 * @desc 空事件回调,被取消事件将被指向此函数
 */
const EMPTY_HANDLER = () => {};

/**
 * @const eventSet - 事件监听函数集
 */
const eventSet = {
  authorize: []
};

/**
 * @function emit - 发送全局事件
 * @param {String} type - 事件类型
 * @param {Object} event - 事件对象
 */
export const emit = (type, event) => (eventSet[type] || []).forEach(item => item(Object.freeze(event)));

/**
 * @function on - 注册全局事件
 * @param {String} type - 事件类型
 * @param {Function} callback - 事件回调函数
 */
export const on = (type, callback) => {
  if (!eventSet[type]) {
    eventSet[type] = [];
  }

  if (!callback instanceof Function) {
    throw new Error('callback must be a Function!');
  }

  return eventSet[type].push(callback)
};

/**
 * @function off - 取消对某事件的监听
 * @param {String} type - 事件类型 
 * @param {Number} id - 需要取消的事件ID,即 registEvent 所返回的值
 */
export const off = (type, id) => {
  if (!eventSet[type]) return

  eventSet[type][id - 1] = EMPTY_HANDLER

  // 如果某类事件已经全被取消的话,将其置为空数组
  const noListener = !eventSet[type].reduce((pre, cur) => (cur && cur === EMPTY_HANDLER) || pre, false);
  if (noListener){
    eventSet[type] = []
  };
}

有关订阅-回调的知识请自行百度,简而言之就是一个杂志亭,订阅者订阅了某款杂志,当发布者发布该款杂志的时候,杂志亭就将杂志送到订阅者手里,双方需要约定的就是杂志名称一致,也就是 on方法和 emit 方法的第一个参数。

3. 定义AuthPage

然后是对 Page 函数的魔改:

/**
 * @file utils/auth-page.js
 */

import { on } from '/event.js';

export const AuthPage = function(options){
  const { onAuth, data, onLoad } = options;
  const userInfo = {
    nickName: '',  // 昵称
    account: '',  // 账号
    avatar: {  // 头像
      small: '',
      middle: '',
      large: ''
    },
    title: 'student',  // 头衔
    phoneNumber: 0,  // 电话号码
    gender: 'secret',  // 性别
    'class': ''  // 班级
  }

  if (options.data){
    options.data.authorized = false;
    options.data.userInfo = userInfo
  } else {
    options.data = {
      authorized: false,
      userInfo: userInfo
    }
  }

  /**
   * 仍旧调用原始的 Page 方法
   */
  Page(Object.assign(
    options,
    {
      onLoad: function(...arg) {
        const { authorize, userInfo } = getApp().globalData;

        // 执行开发者期望的 onload 事件
        onLoad instanceof Function && onLoad.bind(this)(...arg);

        // 页面初始化时,若已经授权,直接执行授权回调
        // 否则将授权回调注册为授权事件回调
        if (onAuth instanceof Function){
          if (authorize.authorized){
            onAuth.bind(this)({
              type: 'authorize',
              authorized: true,
              token: authorize.token,
              userInfo: userInfo
            });
          } else {
            on('authorize', onAuth.bind(this));
          }
        }
      }
    }
  ));
}

4. 触发登录事件

自定义的 onAuth 生命周期里订阅了一个 authorize 事件,登录之后需要发布相应的事件来触发其回调,触发的函数需要写在登录组件里:

import { emit } from '../../utils/event.js';

wx.login({
    success: res => {
        // ...这里省略了一些复杂的登录流程
        getApp().globalData.authorize = {
            authorized: true
        };
        emit('authorize', res);
    }
})

然后,在两个需要登录的 tab 页引入 AuthPage 替换原有的 Page 函数,并在配置项里写 onAuth 回调,就可以监听登录事件了。


补充:

5.适用范围与最佳实践

5.1 适用范围
  1. 部分页面有权限控制,未登录用户与登录用户可以查看的内容不一样,或者不同级别的用户看到的内容不一样;
  2. 除了触发登录事件的页面,其他页面的权限修改过程不能直接呈现给用户,如果没有这个限制的话,判断登录这事可以在 onShow 函数里完成;
  3. 登录机制仅作为一个组件嵌入需要登录入口的所有页面,而不是单独做一个登录页,因为跳转页面这个操作,肯定是直接呈现给用户的了;
  4. AuthPage 不会过多,且切换权限的时候不会有非常耗时的操作。
5.2 最佳实践
    // 页面 A
    AuthPage({
        onLoad: function(){
            const { authorized } = getApp().globalData.authorize;
            let showText = '你还没有登录';
            if(authorized){
                showText = '你已经登陆了';
            }
            this.setState({ showText});
        },
        onAuth: function(){
            this.setState({
                showText: '你已经登陆了'
            })
        }
    });
    
    // 页面 B 
    AuthPage({
        onLoad: function(){
            const { authorized } = getApp().globalData.authorize;
            
            if(authorized){
                this.setState({
                    showLogin: false,  // 隐藏登录组件
                    showContent: true  // 展示内容组件
                })
            } else {
                this.setState({
                    showLogin: true,  // 展示登录组件
                    showContent: false  // 隐藏内容组件
                })
            }
        },
        onAuth: function(option){
            this.setState({
                showLogin: false,  // 隐藏登录组件
                showContent: true  // 展示内容组件
            })
        }
    });

这样写的好处就是:用户未登陆的时候,也可以看到 A 页面的一些内容,使得用户可以初步了解应用内容;用户在 B 页面则会看到登录入口;登录之后 B 页面的登陆组件就被隐藏,取代的是应用内容,而此时 A 页面其实已经在后台悄悄更新了,用户对此是毫无感知的(除非后台更新的页面内容过多导致页面卡顿)。

6 总结

由于先前没有给定用例,需要理解整个流程才能上手编码,导致“懂的人不屑用,不懂的人不会用”的尴尬,所以亡羊补牢,增补了使用范围和用例。

仅供参考!!!