「开源库推荐」uniapp(vue3 版本) 中如何优雅的解决 app.onLaunch 与页面 onLoad 异步问题

1,417 阅读4分钟

问题背景

在 uniapp 开发中,我们经常需要在应用启动时(onLaunch)执行一些异步操作(如获取 token、用户信息等),而这些数据又需要在页面生命周期(如 onLoadonShow)中使用。

由于异步操作的特性,可能会导致页面生命周期执行时,onLaunch 中的异步操作还未完成。

如在 onLaunch 中进行登录后取得 token,项目各页面需要带上该 token 请求其他接口。

// App.vue
// 在 App.vue 的 onLaunch 生命周期请求获取token
onLaunch(() => {
  uni.login({
    success: async (res) => {
      const { token } = await request('xxxx/login', {
        code: res.code,
      });
      // 保存到缓存,请求其他接口时传入
      uni.setStorageSync(TOKEN_KEY, token);
    },
  });
});
// 其他页面
onShow(async () => {
  // 获取页面数据,该 getPageData 方法需要token
  getPageData();
});

传统解决方案的局限性

现阶段网上都是使用 promise 来解决的,大体思路如下所示:

// main.js
app.config.globalProperties.$onLaunched = new Promise((resolve) => {
  app.config.globalProperties.$isResolve = resolve;
});
// App.vue
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
const { $isResolve } = proxy;

onLaunch(() => {
  uni.login({
    success: async (res) => {
      const { token } = await request('xxxx/login', {
        code: res.code,
      });
      $isResolve();
      // 保存到缓存,请求其他接口时传入
      uni.setStorageSync(TOKEN_KEY, token);
    },
  });
});
</script>
const { proxy } = getCurrentInstance();
// 其他页面
onShow(async () => {
  // 等 vue 实例 $onLaunched resolve 后再执行获取页面数据的操作
  await proxy.$onLaunched;
  // 获取页面数据,该 getPageData 方法需要token
  getPageData();
});

上面的方法虽然可以解决异步问题,但是使用起来却很麻烦。

想象下这种场景:

页面中需要依赖 onLaunch 获取的多个数据,上面的方法使用起来就捉襟见肘,并且很难维护

// App.vue
onLaunch(() => {
  // 获取token
  // 获取配置信息
  // 获取用户信息
});
// 页面
onShow(() => {
  getData1(); // 依赖onLaunch中获取的token
  getData2(); // 依赖onLaunch中获取的配置信息和token
  getData3(); // 依赖onLaunch中获取的token、配置信息、用户信息
});

常见的 Promise 解决方案虽然能解决问题,但存在以下不足:

  1. 代码冗余:每个页面都需要等待 Promise 解析
  2. 维护困难:当依赖多个异步数据时,代码复杂度急剧上升
  3. 可读性差:业务逻辑与异步等待逻辑混杂
  4. 扩展性弱:新增依赖项需要修改多处代码

解决方案(下载和使用)

custom-hooks-plus 提供了一种声明式的解决方案,通过自定义钩子将生命周期与数据状态绑定,优雅地解决了异步时序问题。

如下面这个自定义钩子的触发时机为 Token 的值不为空 + uniapp 的 onShow 生命周期。

onCustomShow(() => {
  console.log('onShow+token');
}, 'Token');

下载

npm i custom-hooks-plus

createProxy 的作用(不推荐、推荐使用pinia的形式)

createProxy 的作用就是监听传入对象的变化。

// global.ts 文件
import { createProxy } from 'custom-hooks-plus'

interface GlobalData {
  token: string
  userInfo: number
}

export const globalData = createProxy({
  token: '',
  userInfo: {
    name: ''
  }
})

export function set<K extends keyof GlobalData>(key: K, val: GlobalData[K]) {
  globalData[key] = val
}

export function get<K extends keyof GlobalData>(key: K): GlobalData[K] {
  return globalData[key]
}

init 的作用

init 方法的定义为:

declare type PiniaWatchConfig = {
  key: string;
  type: 'pinia';
  store: any;
  onUpdate?: (val: any) => boolean;
};

declare type PromiseEntry = {
  status: PromiseStatus;
  resolve: Function;
  type?: 'pinia' | 'default';
  onUpdate?: (val: any) => boolean;
};

declare type WatchConfig = PiniaWatchConfig | DefaultWatchConfig;

declare type WatchConfigCollection = {
  [key: string]: WatchConfig;
};

/**
 *
 * @param watchObject 监听的键
 * @param target 传入的store
 * @returns
 */
export declare function init(watchObject: WatchConfigCollection): void;

具体方法为:

// App.vue 中使用
import { init } from 'custom-hooks-plus';
import { useCounterStore } from '@/store/index';

// 推荐使用pinia
init({
  Login: {
    key: 'token', // 监听global文件中globalData的token的变化
    onUpdate: (val) => {
      return !!val;
    },
  },
  UserInfo: {
    key: 'userInfo', // 监听global文件中globalData的userinfo的变化
  },
  Name: {
    key: 'userInfo.name', // 监听global文件中globalData的userinfo.name的变化
  },
  Count: {
    key: 'counter', // 监听 useCounterStore 中 state 的 counter的变化
    type: 'pinia',
    store: useCounterStore(), // type传入pinia类型需要传入store实例
    onUpdate: (val) => {
      return val === 2;
    }, // 更新条件为 val 等于 2
  },
});

使用自定义生命周期钩子

// 页面中使用
<script setup lang="ts">
import { onCustomLoad, onCustomShow } from 'custom-hooks-plus';

onCustomLoad((options) => {
  console.log('LoginUserInfo钩子执行-onCustomLoad1', options);
  console.log('globalData的token和userInfo都被修改了才会触发');
}, ['Login', 'UserInfo']);

onCustomShow(() => {
  console.log('LoginUserInfo钩子执行-onCustomShow2');
  console.log('globalData的token和userInfo都被修改了才会触发');
}, ['Login', 'UserInfo']);

onCustomShow(() => {
  console.log('UserInfoLogin钩子执行-onCustomShow3');
  console.log('globalData的token被修改了才会触发');
}, ['Login']);

</script>

可用的自定义钩子

支持的自定义钩子执行时机
onCustomLaunch对应 uniapp 的 onLaunch
onCustomLoad对应 uniapp 的 onLoad
onCustomCreated渲染时机为 Vue2 的 created
onCustomShow对应 uniapp 的 onShow
onCustomMounted对应 uniapp 的 onMounted
onCustomReady对应 uniapp 的 onReady

总结

通过 custom-hooks-plus 提供的自定义钩子,我们能够优雅地解决 uniapp 中 onLaunch 与页面生命周期的异步问题。这种方法不仅提高了代码的可读性和可维护性,还为复杂的数据依赖场景提供了灵活的解决方案。相比传统的 Promise 方案,这种声明式的方法更符合现代前端开发的理念,能够有效提升开发效率和代码质量。

开源地址