如何利用`Vue Router`的`beforeRouteEnter`与`beforeRouteLeave`?

Vue Router 的 beforeRouteEnterbeforeRouteLeave: 导航守卫的艺术

大家好,今天我们来深入探讨 Vue Router 中两个非常重要的导航守卫:beforeRouteEnterbeforeRouteLeave。它们是组件内守卫,允许我们在路由进入和离开组件时执行特定的逻辑。理解并灵活运用这两个守卫,能让我们更好地控制路由行为,实现更复杂和精细的用户体验。

导航守卫概述

在深入 beforeRouteEnterbeforeRouteLeave 之前,我们先简单回顾一下 Vue Router 的导航守卫体系。导航守卫本质上是在路由跳转过程中执行的函数,它们可以决定路由是否应该被执行,以及在路由执行前后执行一些副作用。Vue Router 提供了三种类型的导航守卫:

  • 全局守卫: 影响整个应用的路由。包括 beforeEachbeforeResolveafterEach
  • 路由独享守卫: 只对单个路由生效。包括 beforeEnter
  • 组件内守卫: 定义在组件内部,与组件的生命周期紧密关联。包括 beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

beforeRouteEnter: 进入组件前的守卫

beforeRouteEnter 守卫在路由被确认之前,即组件实例被创建之前调用。由于此时组件实例尚未创建,因此不能直接访问 this。但是,我们可以通过回调函数 next 来访问组件实例。

语法:

beforeRouteEnter(to, from, next) {
  // 在渲染该组件的对应路由被 confirm 前调用
  // 不!能!获取组件实例 `this`,因为当守卫执行前,组件实例还没被创建
  // 不过,你可以通过传一个回调给 `next` 来访问组件实例。
  // 在导航被 confirm 的时候执行回调,并且把组件实例作为回调函数的参数。
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

参数:

  • to: 即将要进入的目标 路由对象。
  • from: 当前导航正要离开的路由对象。
  • next: 一个函数,表示如何进行下一步操作。

    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (有效)。
    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航会被中断,然后进行一个新的导航。
    • next(error): (2.4.0+) 如果传入 next 一个 Error 实例,那么导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

使用场景:

  1. 获取数据并传递给组件: 在组件渲染之前,可以先获取必要的数据,然后通过 next 函数将数据传递给组件。

    <template>
      <div>
        <h1>{{ message }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          message: 'Loading...'
        };
      },
      beforeRouteEnter(to, from, next) {
        // 模拟异步获取数据
        setTimeout(() => {
          const data = 'Hello from beforeRouteEnter!';
          next(vm => {
            vm.message = data;
          });
        }, 1000);
      }
    };
    </script>
  2. 验证权限: 在进入需要特定权限的页面之前,可以先验证用户是否具有相应的权限。

    beforeRouteEnter(to, from, next) {
      // 模拟权限验证
      const hasPermission = localStorage.getItem('token');
      if (hasPermission) {
        next(); // 允许进入
      } else {
        next('/login'); // 跳转到登录页面
      }
    }
  3. 滚动到页面指定位置: 虽然 Vue Router 提供了 scrollBehavior 选项来控制滚动行为,但在某些情况下,我们可能需要在组件进入时进行更精细的滚动控制。

    beforeRouteEnter(to, from, next) {
      next(vm => {
        // 等待组件渲染完成后滚动到指定位置
        vm.$nextTick(() => {
          const element = document.getElementById(to.hash.slice(1));
          if (element) {
            element.scrollIntoView({ behavior: 'smooth' });
          }
        });
      });
    }
  4. 处理查询参数: 在进入组件之前,可以先处理 URL 中的查询参数,并将处理后的数据传递给组件。

    beforeRouteEnter(to, from, next) {
      const processedQuery = { ...to.query };
      if (processedQuery.sort) {
        processedQuery.sort = processedQuery.sort.toUpperCase();
      }
      next(vm => {
        vm.queryParameters = processedQuery;
      });
    }
    data() {
      return {
        queryParameters: {}
      }
    },

注意事项:

  • 由于无法直接访问 this,因此需要在 next 回调函数中访问组件实例。
  • beforeRouteEnter 只在路由进入时执行一次。如果需要在路由更新时执行某些逻辑,可以使用 beforeRouteUpdate 守卫。
  • 如果使用了 next(false) 中断导航,浏览器 URL 会恢复到 from 路由对应的地址。
  • 如果使用了 next('/')next({ path: '/' }) 跳转到其他路由,会触发新的导航流程。

beforeRouteLeave: 离开组件前的守卫

beforeRouteLeave 守卫在路由离开时,即组件即将被卸载之前调用。与 beforeRouteEnter 不同,beforeRouteLeave 可以直接访问组件实例 this

语法:

beforeRouteLeave(to, from, next) {
  // 导航离开该组件的对应路由时调用
  // 可以访问组件实例 `this`
  next()
}

参数:

  • to: 即将要进入的目标 路由对象。
  • from: 当前导航正要离开的路由对象。
  • next: 一个函数,表示如何进行下一步操作,与 beforeRouteEnter 中的 next 函数功能相同。

使用场景:

  1. 确认离开: 在用户离开页面之前,可以弹出一个确认框,询问用户是否确定离开。

    <template>
      <div>
        <h1>Edit Page</h1>
        <button @click="saveChanges">Save Changes</button>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          isDirty: false // 标记是否已修改
        };
      },
      watch: {
        // 监听数据变化,标记为已修改
        '$data': {
          handler() {
            this.isDirty = true;
          },
          deep: true
        }
      },
      methods: {
        saveChanges() {
          // 保存修改
          this.isDirty = false;
        }
      },
      beforeRouteLeave(to, from, next) {
        if (this.isDirty) {
          const confirmLeave = window.confirm('确定要离开此页面吗?您所做的更改可能会丢失。');
          if (confirmLeave) {
            next(); // 允许离开
          } else {
            next(false); // 阻止离开
          }
        } else {
          next(); // 允许离开
        }
      }
    };
    </script>
  2. 保存数据: 在用户离开页面之前,可以自动保存用户的数据。

    beforeRouteLeave(to, from, next) {
      // 模拟保存数据
      localStorage.setItem('userData', JSON.stringify(this.userData));
      next();
    }
  3. 清理资源: 在组件卸载之前,可以清理组件使用的资源,例如取消订阅事件、关闭定时器等。

    beforeRouteLeave(to, from, next) {
      // 清理定时器
      clearInterval(this.timer);
      next();
    }
  4. 停止正在进行的请求: 如果组件在离开时有正在进行的 API 请求,可以使用 beforeRouteLeave 来取消这些请求,避免不必要的资源消耗和潜在的错误。

    beforeRouteLeave(to, from, next) {
      if (this.currentRequest) {
        this.currentRequest.cancel('Navigation cancelled'); // 使用 axios 的 cancelToken
      }
      next();
    }

注意事项:

  • beforeRouteLeave 可以直接访问 this,因此可以方便地访问组件的状态和方法。
  • 如果使用了 next(false) 阻止导航,浏览器 URL 不会发生改变。
  • 应该尽量避免在 beforeRouteLeave 中执行耗时的操作,以免影响用户体验。

案例分析:一个复杂表单的导航守卫

假设我们有一个复杂的表单组件,用户可以在该组件中填写各种信息。我们需要在用户离开页面之前,进行以下操作:

  1. 如果表单已修改,弹出确认框,询问用户是否确定离开。
  2. 如果用户选择保存数据,则自动保存数据。
  3. 如果用户正在上传文件,则取消上传。
<template>
  <div>
    <h1>Complex Form</h1>
    <form>
      <input type="text" v-model="formData.name" placeholder="Name">
      <textarea v-model="formData.description" placeholder="Description"></textarea>
      <input type="file" @change="uploadFile">
      <button @click="saveData">Save</button>
    </form>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      formData: {
        name: '',
        description: ''
      },
      isDirty: false,
      uploading: false,
      uploadSource: null,
    };
  },
  watch: {
    formData: {
      handler() {
        this.isDirty = true;
      },
      deep: true
    }
  },
  methods: {
    async uploadFile(event) {
      const file = event.target.files[0];
      if (!file) return;

      this.uploading = true;
      this.isDirty = true;
      const formData = new FormData();
      formData.append('file', file);
       this.uploadSource = axios.CancelToken.source();

      try {
        const response = await axios.post('/api/upload', formData, {
          cancelToken: this.uploadSource.token,
          onUploadProgress: (progressEvent) => {
            const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            console.log(`Upload Progress: ${progress}%`);
          }
        });
        console.log('Upload successful:', response.data);
      } catch (error) {
        if (axios.isCancel(error)) {
          console.log('Upload cancelled:', error.message);
        } else {
          console.error('Upload failed:', error);
        }
      } finally {
        this.uploading = false;
      }
    },
    saveData() {
      // 模拟保存数据
      console.log('Saving data:', this.formData);
      this.isDirty = false;
    }
  },
  beforeRouteLeave(to, from, next) {
    if (this.isDirty) {
      const confirmLeave = window.confirm('确定要离开此页面吗?您所做的更改可能会丢失。');
      if (confirmLeave) {
        if(this.uploading && this.uploadSource){
          this.uploadSource.cancel('Navigation cancelled upload.');
        }
        next();
      } else {
        next(false);
      }
    } else {
       if(this.uploading && this.uploadSource){
          this.uploadSource.cancel('Navigation cancelled upload.');
        }
      next();
    }
  }
};
</script>

在这个例子中,我们使用了 isDirty 标志来跟踪表单是否已修改。如果表单已修改,则弹出确认框。如果用户确定离开,并且正在上传文件,则取消上传。

beforeRouteEnterbeforeRouteLeave 的对比

为了更好地理解这两个守卫,我们将其进行对比:

特性 beforeRouteEnter beforeRouteLeave
执行时机 组件实例创建之前 组件实例卸载之前
是否可以访问 this
主要用途 获取数据、验证权限、滚动到指定位置 确认离开、保存数据、清理资源、取消请求

常见问题与解决方案

  1. beforeRouteEnter 中无法访问 this

    这是 beforeRouteEnter 的一个重要特性。通过 next 回调函数访问组件实例。

  2. beforeRouteLeave 中执行耗时操作导致页面卡顿?

    尽量避免在 beforeRouteLeave 中执行耗时操作。如果必须执行,可以考虑使用 setTimeout 将操作延迟到下一个事件循环中执行。

  3. 如何处理异步操作?

    可以使用 async/awaitPromise 来处理异步操作。例如,在 beforeRouteEnter 中获取数据:

    async beforeRouteEnter(to, from, next) {
      try {
        const data = await fetchData();
        next(vm => {
          vm.data = data;
        });
      } catch (error) {
        // 处理错误
        next(false); // 阻止导航
      }
    }
  4. 如何在 beforeRouteLeave 中访问 tofrom 路由对象?

    可以直接通过参数访问 tofrom 路由对象。

总结:导航守卫的应用之道

beforeRouteEnterbeforeRouteLeave 是 Vue Router 中非常强大的导航守卫,它们允许我们在组件生命周期的关键时刻执行特定的逻辑。通过灵活运用这两个守卫,我们可以更好地控制路由行为,提升用户体验,并实现更复杂的功能。理解它们的特性和使用场景,能让你在 Vue 应用开发中更加游刃有余。

掌握beforeRouteEnterbeforeRouteLeave,能够更加精确地控制组件的路由生命周期,在适当的时机执行必要的操作,从而提升应用的健壮性和用户体验。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注