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

Vue3怎么监听路由路径变化?watch route path踩过哪些坑怎么解决?

terry 20小时前 阅读数 254 #Vue

做前端单页应用开发的朋友都知道,路由切换是最常用的交互场景之一,很多时候页面的侧边栏高亮、数据重新请求、状态重置这些操作,都要跟着路由路径的变而动,在Vue2里,我们要么用beforeRouteUpdate、afterEach这些路由钩子,要么直接watch $route.path就搞定了,但到了Vue3,路由用的是vue-router4,API有不小的变动,很多刚上手的朋友甚至熟手都可能在watch route path这件事上踩点小坑,今天就把核心操作和容易踩的几个问题聊透,保证看完就能上手,遇到坑也能快速解决。

Vue3监听路由路径的两种主流方式

既然是主流,那就得是官方文档里推荐或者大家项目里用得最多的,一种是直接监听路由实例的path,另一种是用vue-router4新增的useRoute钩子结合watch/watchEffect,先给大家讲清楚这两种怎么写,什么时候用哪种。

直接监听useRoute().path(最常用,精准高效)

这是目前大家最喜欢用的方式,因为vue-router4提供了useRoute这个响应式API,它返回的就是当前路由的响应式对象,我们可以直接用Composition API里的watch去监听这个对象的path属性,甚至可以配置成只监听path变化,而忽略query、params这些其他属性的变动,非常灵活。

给大家看个最简单的例子,比如在一个侧边栏组件里,高亮当前选中的菜单项:

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
// 当前选中的菜单项索引或者路由路径
const activeMenu = ref('/')
// 获取响应式的当前路由对象
const route = useRoute()
// 只监听route.path的变化,immediate设为true是为了页面刚加载时也能执行一次高亮逻辑
watch(
  () => route.path,
  (newPath) => {
    activeMenu.value = newPath
    // 这里还可以加其他逻辑,比如滚动到顶部
    window.scrollTo({ top: 0, behavior: 'smooth' })
  },
  { immediate: true }
)
</script>

这种方式的优势是精准,只响应path的变动,比如同一个页面只是切换了query参数(比如从/list?page=1跳到/list?page=2),这时候侧边栏不需要重新高亮,也不需要重新请求列表以外的全局数据,就不会触发watch的回调,能减少不必要的性能损耗。

监听整个useRoute对象(适合需要同时看其他属性的场景)

有些场景下,我们不仅需要知道path变了,还得知道query或者params有没有跟着变,比如搜索结果页,path是/search,但query里的keyword变了也得重新请求数据,这时候就可以直接监听整个useRoute对象,不过要注意,这时候必须开启deep选项,因为useRoute返回的是一个嵌套对象,默认watch是浅监听的。

给大家看个搜索结果页的例子:

<script setup>
import { ref, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getSearchResults } from '@/api/search'
const searchResults = ref([])
const loading = ref(false)
const route = useRoute()
const router = useRouter()
// 封装的请求搜索结果的函数
const fetchResults = async () => {
  loading.value = true
  try {
    // 同时用route.query.keyword和route.query.page
    const res = await getSearchResults({
      keyword: route.query.keyword || '',
      page: route.query.page || 1
    })
    searchResults.value = res.data
  } catch (err) {
    console.error('搜索失败', err)
  } finally {
    loading.value = false
  }
}
// 页面刚加载时执行一次
onMounted(() => {
  fetchResults()
})
// 监听整个route对象,开启deep,这样path、query、params变了都会触发
watch(
  route,
  () => {
    fetchResults()
  },
  { deep: true }
)
</script>

不过这里要提醒大家,如果只需要看path或者某几个特定的属性,尽量别开deep监听整个route对象,因为嵌套对象的深监听会消耗更多的性能,比如路由只是切换了一下params里的某个非关键值,或者其他一些内部属性(当然一般内部属性不会随便变,但深监听本身就比浅监听或精准监听单个属性慢一点),所以还是按需监听比较好。

Vue3 watch route path最容易踩的4个坑及解决方案

聊完了主流的写法,接下来就是大家最关心的避坑环节了,这几个坑都是我自己或者身边同事在项目里踩过的,非常真实,也很有代表性。

坑1:监听了useRouter返回的router对象,半天没反应

刚从Vue2转过来的朋友特别容易犯这个错,Vue2里是直接watch $route或者$route.path,到了Vue3里,useRouter返回的是全局的路由操作对象(用来push、replace、back这些),而useRoute才是返回当前页面的响应式路由对象,所以监听router对象或者它的任何属性都不会有反应,除非你手动修改了router的配置(但一般没人会这么做)。

解决方案很简单,把useRouter换成useRoute就行,别搞混这两个钩子的作用。

坑2:开启了immediate,但第一次执行时拿不到页面组件的ref或者DOM元素

这也是一个挺常见的坑,比如我们在watch的回调里想操作一个页面的DOM元素,比如滚动到某个位置,或者给某个ref赋值,但第一次immediate执行的时候,页面的DOM还没渲染完,ref也还是undefined,这时候就会报错或者操作失效。

解决方案有两个,看你的需求:

  1. 如果只是想在DOM渲染完后再执行第一次高亮或者其他DOM操作,可以把watch的immediate去掉,然后把第一次的逻辑放到onMounted里单独执行,这样就能保证DOM已经渲染好了。

    <script setup>
    import { ref, watch, onMounted } from 'vue'
    import { useRoute } from 'vue-router'
    const scrollContainer = ref(null)
    const route = useRoute()
    const handleRouteChange = () => {
      if (scrollContainer.value) {
        scrollContainer.value.scrollTop = 0
      }
    }
    onMounted(() => {
      handleRouteChange()
    })
    watch(
      () => route.path,
      () => {
        handleRouteChange()
      }
    )
    </script>
  2. 如果逻辑里既有数据处理又有DOM操作,数据处理可以immediate执行,DOM操作单独放在nextTick里,这样不管是第一次还是后续路由切换,都能保证数据处理先做,DOM渲染完再做操作。

    <script setup>
    import { ref, watch, nextTick } from 'vue'
    import { useRoute } from 'vue-router'
    const activeMenu = ref('/')
    const scrollContainer = ref(null)
    const route = useRoute()
    watch(
      () => route.path,
      (newPath) => {
        activeMenu.value = newPath
        // 把DOM操作放在nextTick里
        nextTick(() => {
          if (scrollContainer.value) {
            scrollContainer.value.scrollTop = 0
          }
        })
      },
      { immediate: true }
    )
    </script>

坑3:同一个组件复用的时候,watch的回调没有触发

这个坑其实在Vue2里也有,不过到了Vue3里,可能因为大家对Composition API的依赖注入或者组件复用的理解不一样,更容易遇到,比如我们有一个详情页组件,path是/detail/:id,从/detail/1跳到/detail/2,这时候Vue3默认是复用这个详情页组件的,不会销毁再重建,所以onMounted只会执行一次,但如果我们的watch只监听了path,或者监听方式不对,会不会触发呢?

其实用刚才讲的第一种方式(精准监听useRoute().path)或者第二种方式(监听整个useRoute加deep),只要配置正确,复用的时候回调是会触发的,因为path或者整个route对象确实变了,那为什么有些朋友说没触发呢?一般是这两个原因:

  1. 监听的不是响应式的属性,而是一开始就取了route.path的一个普通字符串赋值给ref了,比如下面这种写法:

    <script setup>
    import { ref, watch } from 'vue'
    import { useRoute } from 'vue-router'
    const route = useRoute()
    // 这是错误的写法!currentPath只是一个普通的字符串,不是响应式的,它的初始值是第一次的path,之后不会变
    const currentPath = ref(route.path)
    // 这里监听currentPath,当然只有你手动修改currentPath的时候才会触发,路由变了不会
    watch(currentPath, (newPath) => {
      console.log('路径变了', newPath)
    })
    </script>

    解决方案就是别这么写,要么直接监听() => route.path,要么用computed把route.path包一层(不过没必要,直接监听getter更方便)。

  2. 在keep-alive缓存的组件里,没有配合activated钩子或者其他处理,不过这个和watch route path本身没太大关系,主要是keep-alive缓存的组件onMounted/onUnmounted不会再执行,activated/deactivated会执行,但只要route是响应式的,watch还是会正常触发的。

坑4:监听了路由变化后,数据请求的竞态问题没处理

这个坑不是Vue3或者vue-router4特有的,但在做路由切换监听数据请求的时候特别容易遇到,尤其是网络不好的时候,比如我们从/list?page=1跳到/list?page=3,page=1的请求因为网络慢还没回来,page=3的请求先回来了,然后过了一会儿page=1的请求才回来,把已经渲染好的page=3的数据覆盖了,这就叫“竞态问题”。

这个问题必须得处理,不然用户体验会非常差,解决方案也有好几种,常用的有两种:

  1. 用AbortController中断上一次的请求,这是目前比较推荐的原生方式,不需要引入额外的库。

    <script setup>
    import { ref, watch } from 'vue'
    import { useRoute } from 'vue-router'
    const listData = ref([])
    const loading = ref(false)
    const route = useRoute()
    // 用来存储上一次请求的AbortController
    let abortController = null
    const fetchList = async () => {
      // 如果上一次的请求还没完成,先中断它
      if (abortController) {
        abortController.abort()
      }
      // 创建新的AbortController
      abortController = new AbortController()
      const signal = abortController.signal
      loading.value = true
      try {
        // 把signal传给fetch或者axios(axios用cancelToken或者同样支持signal)
        const res = await fetch(`/api/list?page=${route.query.page || 1}`, { signal })
        const data = await res.json()
        listData.value = data
      } catch (err) {
        // 如果是主动中断的请求,不用报错
        if (err.name !== 'AbortError') {
          console.error('请求列表失败', err)
        }
      } finally {
        // 只有当前请求的signal不是aborted状态,才把loading设为false
        // 不然如果上一次的请求被中断了,finally也会执行,会提前把loading关掉
        if (!signal.aborted) {
          loading.value = false
        }
      }
    }
    watch(
      () => route.query.page,
      () => {
        fetchList()
      },
      { immediate: true }
    )
    </script>
  2. 用防抖或者节流,但这个不太适合分页或者关键词搜索这种需要即时响应的场景,因为防抖会延迟请求,节流会漏掉中间的请求,所以还是推荐用AbortController。

总结一下Vue3 watch route path的最佳实践

最后给大家总结一下,什么时候用什么方式,要注意什么:

  1. 只需要监听路由路径的变化:直接用watch(() => useRoute().path, ..., { immediate: 按需 }),精准高效。
  2. 需要同时监听path、query、params中的多个属性:可以用watch监听对应的getter数组,比如watch([() => route.path, () => route.query.keyword], ...),这样比deep监听整个route对象性能更好;如果所有属性都要监听,再用watch(route, ..., { deep: true })。
  3. 别搞混useRouter和useRoute:useRouter是操作路由的,useRoute是获取当前响应式路由的。
  4. 处理好immediate和DOM操作的关系:可以把DOM操作放在nextTick里,或者把第一次的逻辑放在onMounted里。
  5. 一定要处理数据请求的竞态问题:用AbortController中断上一次的请求,这是最好的方式。

好了,关于Vue3 watch route path的内容就聊到这里了,相信大家看完这篇文章,不仅能熟练掌握监听路由路径的方法,还能避开那些常见的坑,开发起来更顺利,如果还有其他问题,欢迎在评论区留言交流。

版权声明

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

热门