uni-app 中实现 onLaunch 异步回调后执行 onLoad

859 阅读4分钟

问题描述

在使用 uni-app 开发项目时,会遇到需要在 onLaunch 中请求接口返回结果,并且此结果在项目各个页面的 onLoad 中都有可能使用到的需求,比如微信小程序在 onLaunch 中进行登录后取得 openid 并获得 token,项目各页面需要带上该 token 请求其他接口。

问题原因

在onLaunch 中的请求是异步的,也就是说在执行 onLaunch 后页面 onLoad 就开始执行了,而不会等待 onLaunch 异步返回数据后再执行,这就导致了页面无法拿到 onLaunch 中异步获取的数据。

解决问题

知道问题原因之后,解决起来就容易了。作为资深白嫖党,先是搜索了相关资料,发现了下面的解决方案。

解决方案一

既然在onLaunch中请求是异步的原因导致这个问题,那改成同步的不就行了,这里利用Promise来解决这个问题。步骤如下。

步骤一

在 main.js 中增加如下代码:

vue2

Vue.prototype.$onLaunched = new Promise(resolve => {
    Vue.prototype.$isResolve = resolve
})

vue3

app.config.globalProperties.$onLaunched = new Promise(resolve => {
  app.config.globalProperties.$isResolve = resolve
})

步骤二

在 App.vue 的 onLaunch 中增加代码 this.$isResolve(),具体如下:

vue2

onLaunch () {
    uni.login({
        provider: 'weixin',
        success: loginRes => {
            login({ // 该接口为我们自己写的获取 openid/token 的接口,请替换成自己的
                code: loginRes.code
            }).then(res => {
                try {
                    console.info(res.data.token)
                    uni.setStorageSync('token', res.data.token)
                    this.$isResolve()
                } catch (e) {
                    console.error(e)
                }
            })
        }
    })
}

vue3

import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { getCurrentInstance } from 'vue'

// 1. 获取当前实例上下文
const { proxy } = getCurrentInstance()

// 2. 定义生命周期
onLaunch(() => {
    console.log('App Launch')
    uni.login({
        provider: 'weixin',
        success: (loginRes) => {
            console.info('loginRes',loginRes)
            // 模拟接口请求
            setTimeout(()=>{
                // 3. 通过 proxy 访问全局挂载的 $isResolve 并执行
                if (proxy && proxy.$isResolve) {
                    proxy.$isResolve()
                } else {
                    console.warn('$isResolve not found on globalProperties')
                }
            },3000)
        },
        fail: (err) => {
            console.error('Login failed', err)
            // 即使登录失败,通常也需要释放 Promise,防止页面一直卡住
            // 视业务需求而定,如果必须登录成功才继续,则不调用
            if (proxy && proxy.$isResolve) {
                proxy.$isResolve()
            }
        }
    })
})

this.$isResolve() 的具体作用是解决(resolve)之前定义的 $onLaunched Promise。在这个上下文中,$onLaunched 是一个全局的 Promise 对象,它似乎被用于等待应用启动过程中的一些异步操作完成。

当你调用 this.$isResolve() 时,实际上是调用了 Promise 的 resolve 函数,这会导致 $onLaunched Promise 从等待状态变为解决状态。一旦 Promise 被解决,与之相关联的 .then() 方法中的回调函数就会被触发执行。

说白了就是我在onLoad处调用了一个承诺函数,然后把这个承诺函数的resolve方法传到了onLaunch中执行,所以当onLaunch中执行完毕后,onLoad中的承诺函数会放行,且承诺函数一旦被 resolve 或 reject,它的状态就永久确定了,所以其他页面onLoad中await该承诺函数也会被立刻放行。

步骤三

在页面 onLoad 中增加代码 await this.$onLaunched,具体如下:

vue2

async onLoad(option) {
    await this.$onLaunched
​
    let token = ''
    try {
        token = uni.getStorageSync('token')
    } catch(e) {
        console.error(e)
    }
​
    // 下面就可以使用 token 调用其他相关接口
}

vue3

import { onLoad } from '@dcloudio/uni-app'
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()

onLoad(async()=>{
    await proxy.$onLaunched
    console.log('onLoad')
})

有了这个解决方案,我就开始在实际项目中是用来了。但随着项目的复杂度增加,发现这个方案使用起来有一些弊端。每个页面都需要在 onLoad 中增加代码代码也太烦人了。

有没有更优雅的方案呢?继续查找资料,有个解决方案是定制一个页面钩子,然后注册全局的异步任务,定义钩子的触发条件,满足条件时即可自动执行页面里相关的钩子。相关方案见参考资料2。

但这个方案我也不太满意,仍然需要在页面添加一些函数去响应请求。后面突然想到,可以监听路由变动,在路由跳转之前完成请求。

解决方案二(推荐)

正好项目中用到了uni-simple-router插件,提供了全局前置守卫事件beforeEach,其本质是代理了所有的生命周期,让生命周期更加可控,这样就可以很好的解决我们面临的问题了。步骤如下: 好项目中用到了uni-simple-router插件,提供了全局前置触发事件beforeEach,其本质是代理了所有的生命周期,让生命周期更加可靠,这样就可以很好的解决我们面临的问题了。步骤如下:

步骤一

在 route.js 增加如下代码:

// 登录(可放在公共函数里面)
const login = () => {
    return new Promise(function(resolve, reject) {
        uni.login({
            provider: 'weixin',
            success: loginRes => {
                login({ // 该接口为我们自己写的获取 openid/token 的接口,请替换成自己的
                    code: loginRes.code
                }).then(res => {
                    resolve(res);
                }, err => {
                    reject(err);
                })
            },
            fail: err => {
                reject(err);
            }
        });
    });
}
​
// 获取token(可放在公共函数里面)
const getToken = () => {
    let token = ''
    try {
        token = uni.getStorageSync('token')
    } catch(e) {
        console.error(e)
    }
    return token;
}
​
// 是否登录
let hasLogin = false;
router.beforeEach(async (to, from, next) => {
    // 首次进来,没有登录并且token不存在先请求数据
    if(!hasLogin&&!getToken()){
        const res = await login();
        try {
            console.info(res.data.token)
            uni.setStorageSync('token', res.data.token)
            hasLogin = true
        } catch (e) {
            console.error(e)
        }
    }
    next()
})

步骤二

在页面 onLoad 中直接就可以获取 token 并使用,具体如下:

javascript复制代码onLoad(option) {
    let token = ''
    try {
        token = uni.getStorageSync('token')
    } catch(e) {
        console.error(e)
    }
​
    // 下面就可以使用 token 调用其他相关接口
}

这个解决方案就灵活很多,只需要在 route.js 中写入代码,其他任意地方都可以调用。不用担心新增页面忘记相关方法的引入,更加灵活自由。

由于这个解决方案基于uni-simple-router插件,在使用前需要引入这个插件。如果不想引入插件,可以自行实现代码生命周期功能。 In this paper,we studythe relationship between the factors of the如果不想引入插件,可以自行实现代码生命周期功能。

参考文档juejin.cn/post/710084…