Code前端首页关于Code前端联系我们

Vue3单页应用/原生Web没有onShow?uni-app和原生小程序的onShow咋对应原生Vue3?

terry 43分钟前 阅读数 13 #Vue
文章标签 Vue3 onShowapp onShow

刚上手Vue3做项目,尤其是从uni-app、原生微信小程序这类跨端/小程序开发转过来的朋友,大概率第一反应就是找onShow——毕竟小程序里页面每次切到前台、重新激活都会触发,太好用了,但翻遍Vue3的Composition API和Options API文档,居然找不到内置的onShow钩子?这时候别慌,不是Vue3“偷懒”,而是小程序和原生Web的渲染、路由机制完全不一样,咱们得先搞懂onShow到底是啥,再在Vue3里找对应的或者自己实现的方案。

小程序的onShow本质是什么?

要解决Vue3的“无onShow焦虑”,得先明白原生小程序和跨端框架里的onShow到底在监听什么。 其实小程序的页面是「多页应用」的变种,更准确说是「混合栈式页面管理」:每个页面都有独立的栈、独立的生命周期,切换的时候旧页面不会被销毁(除非手动清空或者栈太深被自动回收),只是被压入后台“休眠”;新页面激活、旧页面从后台弹出来、用户下拉刷新之后页面重新可见、或者分享回来打开当前页面,都会触发onShow,总结下来,它的核心触发条件有两个:

  1. 页面可见性发生变化,从不可见变为可见
  2. 路由发生切换,回到当前页面(但不是完全重新加载,不是onLoad/onMount那种)

这俩条件也是我们在Vue3原生Web里模拟onShow的核心依据——原生Web是单页应用(SPA),没有小程序那种独立页面栈,路由切换本质是组件的挂载/卸载/复用,页面可见性是靠浏览器原生API控制的,所以不能直接照搬小程序的钩子,但可以分别对应这两个条件做组合。

uni-app里的Vue3 onShow和原生小程序的有啥区别?

很多朋友可能先接触的是uni-app的Vue3版本,里面明明有onShow啊?这其实是uni-app做的跨端适配,不是Vue3原生的。 uni-app的底层逻辑是:编译到小程序端时,直接把我们写的Vue3 onShow钩子转换成对应小程序平台的onShow;编译到H5/原生App(Webview渲染模式)时,自己封装了一套类似小程序的页面栈管理逻辑,用浏览器的原生可见性API和路由钩子组合起来模拟onShow。 不过注意,如果是uni-app的Vue3版本用了Vite构建的H5纯Vue开发模式(就是不编译到其他端,只保留uni-app的部分UI组件库但不用它的跨端路由和生命周期),那uni-app自带的onShow也用不了,还是得用下面原生Vue3的方法。

原生Vue3 SPA怎么分别实现「可见性激活」和「路由返回激活」的监听?

刚才说了,小程序onShow是两个条件的组合,我们在Vue3里可以分开实现这两个功能,然后根据业务需求选择单独用还是合并用——这样反而比小程序的单一钩子更灵活,比如有些业务只需要监听页面切回前台(比如视频暂停后切回来继续播),有些只需要监听路由从子页面返回(比如修改了用户信息后回到个人中心刷新数据)。

监听「页面可见性变化」:原生API watchEffect+visibilitychange

这个功能不管是Vue3还是原生JS都能用,Vue3只是用Composition API封装得更优雅,组件销毁时还能自动取消监听,不用自己手动写removeEventListener。 核心原理就是:监听浏览器的visibilitychange事件,当document.visibilityState变成visible的时候,触发我们想要的逻辑;变成hidden的时候,可能也需要做清理(比如暂停定时器、视频)。 举个Composition API的例子,放在你的页面组件或者全局组件里都可以:

import { watchEffect, onUnmounted } from 'vue'
export default {
  setup() {
    // 定义切到前台的逻辑
    const handlePageShow = () => {
      console.log('页面切到前台了,刷新下未读消息吧')
      // 这里放你的业务代码,比如调用接口、刷新数据、继续播放视频
    }
    // 定义切到后台的逻辑
    const handlePageHide = () => {
      console.log('页面切到后台了,暂停一下视频/定时器')
      // 这里放清理代码
    }
    // watchEffect会自动追踪内部依赖?不,这里我们只是监听事件,但watchEffect会在组件卸载时自动清理里面的副作用
    // 不过为了更稳妥,也可以用下面的写法,手动绑定事件再手动清理,但watchEffect更Vue3一点
    let visibilityHandler
    watchEffect((onInvalidate) => {
      visibilityHandler = () => {
        if (document.visibilityState === 'visible') {
          handlePageShow()
        } else if (document.visibilityState === 'hidden') {
          handlePageHide()
        }
      }
      document.addEventListener('visibilitychange', visibilityHandler)
      // onInvalidate是watchEffect提供的,组件卸载或者依赖变化时会自动执行
      onInvalidate(() => {
        document.removeEventListener('visibilitychange', visibilityHandler)
      })
    })
    return {}
  }
}

如果是Options API的写法,也可以用mountedunmounted生命周期配合:

export default {
  mounted() {
    document.addEventListener('visibilitychange', this.handleVisibilityChange)
  },
  unmounted() {
    document.removeEventListener('visibilitychange', this.handleVisibilityChange)
  },
  methods: {
    handleVisibilityChange() {
      if (document.visibilityState === 'visible') {
        console.log('页面切到前台了')
      }
    }
  }
}

监听「路由返回激活」:Vue Router的钩子组合

这个功能得依赖Vue Router 4.x(Vue3配套的路由版本),因为原生Web的路由切换全靠它。 这里得先搞清楚Vue Router的几个钩子:

  • onActivated/activated:只对用<keep-alive>缓存的组件有用,当缓存的组件重新被激活(从<keep-alive>的缓存里拿出来渲染)的时候触发——这个有点像小程序里页面不被销毁只是压栈又弹出来的情况,但前提是你必须把组件用<keep-alive>包起来。
  • onBeforeRouteUpdate/beforeRouteUpdate:路由参数或者路径变化,但组件复用的时候触发(比如从/user/1跳到/user/2,User组件没销毁,只是参数变了)——这个不是我们常说的“返回激活”,但也算路由相关的组件激活。
  • onBeforeRouteLeave/beforeRouteLeave:离开当前路由前触发,不是激活。
  • onBeforeRouteEnter/beforeRouteEnter:进入当前路由前触发,这时候组件还没挂载,不能直接用this或者setup里的变量——这个更像小程序的onLoad

那没有<keep-alive>的时候,怎么监听路由从子页面返回呢?比如你从/home跳到/home/detail,再从/home/detail跳回/home,这时候如果Home组件没被<keep-alive>缓存,会重新执行onMounted,但很多时候我们不想让Home组件每次都重新挂载(比如不想重新加载整个首页的静态资源,只想刷新某个数据模块),或者就算重新挂载了,也想区分是第一次打开首页还是从子页面返回的。 这时候可以用useRoutefullPathpath配合watch,或者用useRouterafterEach钩子,再存一下上一个路由的信息来判断。

方案1:用+onActivated(适合不想组件重新挂载的场景)

这个是最接近小程序onShow(从栈里弹出)的方案,不过你得先配置好<keep-alive>,比如在App.vue里:

<template>
  <!-- include是只缓存名字匹配的组件,exclude是不缓存,max是最多缓存多少个 -->
  <router-view v-slot="{ Component }">
    <keep-alive include="Home,UserCenter">
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>

注意哦,include里的名字必须和组件的name属性一致,不管是Composition API还是Options API,都得显式声明name(Options API直接写,Composition API可以用<script setup name="Home">的写法,不过有些旧版本的Volar或者Vite插件可能需要单独配置,或者用defineOptions)。 然后在Home组件的setup里写:

import { onActivated } from 'vue'
export default {
  // 必须写name,否则include匹配不到
  name: 'Home',
  setup() {
    // 组件第一次挂载的时候会触发onMounted,之后每次从缓存里激活(比如从子页面返回、切换标签页但keep-alive缓存了)都会触发onActivated
    onActivated(() => {
      console.log('Home组件被激活了,刷新一下推荐列表吧')
      // 这里放你的业务代码
    })
    return {}
  }
}

如果是<script setup>的写法,旧版本可以这么弄:

<script setup>
import { onActivated, defineOptions } from 'vue'
// 显式声明组件name
defineOptions({
  name: 'Home'
})
onActivated(() => {
  console.log('Home组件激活了')
})
</script>

新版本的Vue 3.3+直接支持<script setup name="Home">,更方便。

方案2:不用keep-alive,用watch+useRoute+useRouter判断上一个路由(适合组件必须重新挂载但要区分进入方式的场景)

有时候组件不能用keep-alive缓存(比如每次打开都必须重置某些状态),但我们还是想知道是不是从子页面返回的,比如从详情页返回列表页,只刷新列表的部分状态,不重置筛选条件。 这时候可以在全局的路由守卫里存一下上一个路由的信息,或者在当前组件里用useRouterafterEach钩子存上一个路由,再用watch监听当前路由的变化。 举个全局存上一个路由的例子,先在main.js或者单独的router/index.js里配置:

import { createRouter, createWebHistory } from 'vue-router'
const routes = [/* 你的路由配置 */]
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})
// 全局前置守卫里存上一个路由
let previousRoute = null
router.beforeEach((to, from, next) => {
  previousRoute = from
  next()
})
// 把previousRoute挂载到router实例上,方便组件里用
router.previousRoute = previousRoute
export default router

然后在你的Home组件里:

import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
export default {
  name: 'Home',
  setup() {
    const router = useRouter()
    onMounted(() => {
      // 注意哦,全局前置守卫里的previousRoute是闭包变量,组件里直接取router.previousRoute有时候会有延迟,最好在afterEach里实时更新,或者下面换一种写法
      // 更稳妥的是在当前组件里单独监听afterEach
    })
    return {}
  }
}

刚才那个全局闭包的写法可能有问题,因为多个组件同时取的话,可能取到同一个值,或者更新不及时,所以最好在当前组件里用useRouterafterEach钩子,配合onUnmounted取消监听:

import { onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
export default {
  name: 'Home',
  setup() {
    const router = useRouter()
    const currentRoute = useRoute()
    // 定义一个变量存进入这个组件之前的上一个路由
    let lastRouteBeforeEnter = null
    // 监听路由变化,只在当前路由匹配的时候处理
    const removeAfterEach = router.afterEach((to, from) => {
      // 如果to是当前路由,说明是第一次进入或者返回当前路由
      if (to.path === currentRoute.path) {
        lastRouteBeforeEnter = from
        // 这里可以直接触发业务逻辑,不用等onMounted?不对,如果是第一次进入,afterEach在onMounted之前触发,这时候组件可能还没准备好
        // 所以可以把lastRouteBeforeEnter存起来,在onMounted里判断
      }
    })
    onMounted(() => {
      // 如果lastRouteBeforeEnter存在,且不是空的根路径(根路径有时候是初始加载的from),或者lastRouteBeforeEnter的path是子页面的path
      if (lastRouteBeforeEnter && lastRouteBeforeEnter.path !== '/') {
        console.log('是从其他页面返回的Home,刷新部分数据')
      } else {
        console.log('第一次打开Home,加载全部数据')
      }
    })
    // 组件销毁时取消监听afterEach,防止内存泄漏
    onUnmounted(() => {
      removeAfterEach()
    })
    return {}
  }
}

怎么把这两个条件组合起来,实现完全像小程序onShow的钩子?

刚才我们分开实现了「可见性激活」和「路由返回激活」,如果业务里需要和小程序完全一样的效果(不管是切回前台还是路由返回,只要页面可见且是当前路由就触发),可以把这两个功能封装成一个自定义的Composition API钩子,比如叫useOnShow。 这样以后用的时候直接调用就行,不用每次都写一堆重复代码。 举个封装好的useOnShow钩子的例子,放在src/hooks/useOnShow.js里:

import { watchEffect, onUnmounted, getCurrentInstance } from 'vue'
import { useRouter, useRoute } from 'vue-router'
export function useOnShow(callback) {
  // 检查是否在Vue组件的setup里调用
  const instance = getCurrentInstance()
  if (!instance) {
    throw new Error('useOnShow必须在Vue组件的setup里调用')
  }
  const router = useRouter()
  const currentRoute = useRoute()
  // 定义上一个路由的变量
  let lastRoute = null
  // 定义页面是否是当前可见的路由
  let isCurrentRouteActive = false
  // 1. 先处理路由相关的激活:不管有没有keep-alive,只要回到当前路由就设置isCurrentRouteActive为true
  const removeAfterEach = router.afterEach((to, from) => {
    lastRoute = from
    isCurrentRouteActive = to.path === currentRoute.path
    if (isCurrentRouteActive) {
      // 触发回调,传上一个路由和触发类型
      callback?.({ type: 'route', from: lastRoute })
    }
  })
  // 2. 再处理页面可见性相关的激活:只有当页面是当前可见的路由时,切回前台才触发回调
  let visibilityHandler
  watchEffect((onInvalidate) => {
    visibilityHandler = () => {
      if (document.visibilityState === 'visible' && isCurrentRouteActive) {
        callback?.({ type: 'visibility', from: null })
      }
    }
    document.addEventListener('visibilitychange', visibilityHandler)
    onInvalidate(() => {
      document.removeEventListener('visibilitychange', visibilityHandler)
    })
  })
  // 3. 组件挂载时,先设置isCurrentRouteActive为true,因为刚挂载的时候肯定是当前路由
  // 不过要注意,如果是keep-alive的组件,第一次挂载会触发onMounted,之后激活会触发onActivated,所以这里要区分
  // 不过没关系,我们的afterEach已经处理了,组件挂载时afterEach也会触发
  // 为了更稳妥,可以在instance的mounted钩子或者activated钩子里再设置一下
  const { onMounted, onActivated } = instance.proxy.$options
  // 保存原来的mounted和activated钩子
  const originalMounted = onMounted
  const originalActivated = onActivated
  // 重写mounted钩子
  instance.proxy.$options.onMounted = () => {
    isCurrentRouteActive = true
    // 调用原来的mounted钩子
    originalMounted?.call(instance.proxy)
  }
  // 重写activated钩子
  instance.proxy.$options.activated = () => {
    isCurrentRouteActive = true
    // 调用原来的activated钩子
    originalActivated?.call(instance.proxy)
  }
  // 4. 组件卸载时取消所有监听
  onUnmounted(() => {
    removeAfterEach()
  })
}

然后在你的页面组件里直接用:

<script setup name="Home">
import { useOnShow } from '@/hooks/useOnShow'
// 调用自定义钩子,传回调函数
useOnShow(({ type, from }) => {
  console.log('触发了自定义onShow,类型是:', type)
  if (type === 'route') {
    console.log('是从路由:', from.path, '返回的')
    // 这里放路由返回的业务代码
  } else if (type === 'visibility') {
    console.log('是从后台切回来的')
    // 这里放页面可见的业务代码
  }
})
</script>

这个自定义钩子已经兼容了有keep-alive和没有keep-alive的情况,还区分了触发类型,业务代码可以根据不同的触发类型做不同的处理,比小程序的单一onShow更灵活。

总结一下

  • 原生Vue3 SPA没有内置的onShow钩子,因为它是单页应用,没有小程序那种独立的页面栈;
  • 小程序的onShow本质是监听「可见性变化」和「路由返回激活」两个条件;
  • uni-app的Vue3 onShow是跨端适配的,不是Vue3原生的;
  • 原生Vue3里可以分别用visibilitychange事件监听可见性,用Vue Router的钩子(onActivatedafterEachwatch)监听路由返回;
  • 如果需要完全像小程序的onShow,可以把这两个功能封装成自定义的Composition API钩子useOnShow

现在应该不会再找Vue3的onShow找得焦头烂额了吧?根据自己的业务需求选择合适的方案就行,不用盲目追求和小程序一模一样的钩子,Vue3的灵活性本来就体现在这里嘛。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门