Skip to content

Vue Router 3 使用笔记 ​

汇总 Vue 2 项目中 vue-router 3.x 的安装、路由配置、导航与传参方式,以及导航守卫、路由元信息、过渡动效、数据获取、滚动行为与懒加载等进阶用法。

一、安装 ​

bash
cnpm install --save vue-router

二、基本使用 ​

路由相关代码拆成三部分:routes.js 维护路由表、index.js 创建 router 实例、main.js 把实例注入根实例。

1. routes.js ​

定义路由表:

js
const hello = () => import("../components/HelloWorld.vue");

const routes = [
  {
    path: "/",
    redirect: "/a",
  },
  {
    path: "/a",
    component: hello, // 注意是 component,不是 components
  },
];

export default routes;

2. index.js ​

创建 VueRouter 实例并导出:

js
import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";

Vue.use(VueRouter);

const router = new VueRouter({
  routes,
});

export default router;

3. main.js ​

把 router 注入根实例,让整个应用都具备路由功能:

js
import router from "./router/index";

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

注意:路由内容不显示,且控制台报 Cannot read property '$createElement' of undefined,通常是因为 routes.js 中把 component 错拼成了 components。带 s 的 components 是「命名视图」的写法,普通路由只接受 component。

三、声明式与编程式导航 ​

  • 声明式导航:在模板中通过 <router-link :to="..."> 完成跳转。
  • 编程式导航:在脚本中通过 this.$router.push(...) 完成跳转。
html
<router-link to="/register">注册</router-link>
js
this.$router.push("/register");

四、嵌套路由 ​

App.vue 中的 <router-view> 是顶层出口,渲染最高级路由匹配到的组件。如果希望 home 页左侧导航栏保持不变、右侧内容随路由切换,就可以在 home 路由下再嵌套一层子路由:

js
{
  path: '/',
  component: Home,
  name: 'Home',
  children: [
    { path: '/fleetStatus', meta: 'fleetStatus', component: FleetStatus },
  ],
}

五、命名路由 ​

使用场景:当子路由路径过长时,用一个名称来标识路由会更方便。

1. 定义路由 ​

js
{
  name: 'register',
  component: register,
}

2. 传参 ​

js
this.$router.push({
  name: "register",
  params: {
    plan: "private",
  },
});

3. 获取参数 ​

js
this.$route.params.plan;

此时 URL 仍然是 /register。

注意:params 不会拼接在 URL 上,刷新页面会丢失数据。

六、命名视图 ​

如果想同级展示多个视图,而不是嵌套展示,就可以使用命名视图。典型场景:首页需要一个绿色的 header,其他页面的头部则用红色的 header。

如果 <router-view> 没有设置名字,那么默认为 default。

1. 完整示例 ​

下面示例通过 CDN 引入 Vue 与 Vue Router,无需构建工具:

html
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 导入 vue 路由的包 -->
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

页面结构:

html
<div id="app">
  <!-- 默认容器 -->
  <router-view></router-view>
  <!-- 使用 css3 的 flex 布局,包起来 -->
  <div class="container">
    <!-- 给容器指定 name 名称,加载对应组件 -->
    <router-view name="left"></router-view>
    <router-view name="main"></router-view>
  </div>
</div>

<template id="headerBox">
  <h1 class="header">headerBox 组件</h1>
</template>
<template id="leftBox">
  <h1 class="left">leftBox 组件</h1>
</template>
<template id="mainBox">
  <h1 class="main">mainBox 组件</h1>
</template>

样式:

css
* {
  margin: 0;
  padding: 0;
}

.header {
  height: 80px;
  background-color: orange;
}

.container {
  display: flex;
  height: 600px;
}

.left {
  background-color: lightgreen;
  flex: 2;
}

.main {
  background-color: lightpink;
  flex: 8;
}

脚本:

js
var headerBox = {
  template: "#headerBox",
};
var leftBox = {
  template: "#leftBox",
};
var mainBox = {
  template: "#mainBox",
};

var router = new VueRouter({
  routes: [
    {
      path: "/",
      // components(复数)同时展示多个组件
      components: {
        default: headerBox, // 默认容器放 headerBox 组件
        left: leftBox, // name 为 left 的容器放 leftBox 组件
        main: mainBox, // name 为 main 的容器放 mainBox 组件
      },
    },
  ],
});

var vm = new Vue({
  el: "#app",
  data: {},
  router,
});

2. 嵌套命名视图 ​

嵌套命名视图可以用来实现复杂布局。

text
/settings/emails                       /settings/profile
+-----------------------------------+  +------------------------------+
| UserSettings                      |  | UserSettings                 |
| +-----+-------------------------+ |  | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | |  | | Nav | UserProfile        | |
| |     +-------------------------+ |  | |     +--------------------+ |
| |     |                         | |  | |     | UserProfilePreview | |
| +-----+-------------------------+ |  | +-----+--------------------+ |
+-----------------------------------+  +------------------------------+
  • Nav 只是一个常规组件。
  • UserSettings 是一个视图组件。
  • UserEmailsSubscriptions、UserProfile、UserProfilePreview 是嵌套的视图组件。
html
<!-- UserSettings.vue -->
<div>
  <h1>User Settings</h1>
  <NavBar />
  <router-view />
  <router-view name="helper" />
</div>
js
{
  path: '/settings',
  // 也可以在顶级路由就配置命名视图
  component: UserSettings,
  children: [
    {
      path: 'emails',
      component: UserEmailsSubscriptions,
    },
    {
      path: 'profile',
      components: {
        default: UserProfile,
        helper: UserProfilePreview,
      },
    },
  ],
}

七、重定向与别名 ​

1. 重定向 ​

重定向是路由替换:当用户访问 /a 时,URL 会被替换成 /b,然后匹配到路由 /b。

从 /a 重定向到 /b:

js
const router = new VueRouter({
  routes: [{ path: "/a", redirect: "/b" }],
});

重定向的目标也可以是一个命名路由:

js
const router = new VueRouter({
  routes: [{ path: "/a", redirect: { name: "foo" } }],
});

重定向的目标还可以是一个方法:

js
const router = new VueRouter({
  routes: [
    {
      path: "/a",
      redirect: (to) => {
        // 方法接收目标路由作为参数
        // return 重定向的字符串 / 路径对象
      },
    },
  ],
});

2. 别名 ​

别名是路由映射:/a 的别名是 /b,意味着用户访问 /b 时,URL 保持为 /b,但路由匹配的是 /a,就像用户访问 /a 一样。

js
const router = new VueRouter({
  routes: [{ path: "/a", component: A, alias: "/b" }],
});

八、路由传参 ​

1. query 传参 ​

js
// 路由定义
{
  path: '/register',
  component: register,
}

// 页面传参
this.$router.push({
  path: '/register',
  query: {
    plan: 'private',
  },
})

// 页面获取
this.$route.query.plan

2. 动态路由匹配 ​

js
// 路由定义
{
  path: '/register/:plan',
  component: register,
}

// 页面传参
this.$router.push(`/register/private`)

// 页面获取
this.$route.params.plan

拓展:在组件中使用 $route 会使组件与其对应路由形成高度耦合,组件只能在特定 URL 上使用,限制了灵活性。可以使用 props 属性进行解耦:

js
// 路由定义
{
  path: '/register/:plan',
  component: register,
  props: true, // 设为 true 后,$route.params 会被设置为组件属性
}

// 页面传参
this.$router.push(`/register/private`)

// 页面获取
props: {
  plan: {
    type: String,
    default: '',
  },
}

3. 命名路由传参 ​

js
// 路由定义
{
  name: 'register',
  component: register,
}

// 页面传参
this.$router.push({
  name: 'register',
  params: {
    plan: 'private',
  },
})

// 页面获取
this.$route.params.plan

注意:参数不会拼接在路由后面,刷新页面会丢失数据。

4. 几种传参方式对比 ​

方式路由定义传参获取URL 形式
querypath: '/register'push({ path, query })$route.query.plan/register?plan=xxx
动态路由path: '/register/:plan'push('/register/private')$route.params.plan/register/private
命名路由name: 'register'push({ name, params })$route.params.plan/register(不含参数)

九、导航守卫 ​

1. 全局前置守卫 router.beforeEach ​

使用场景:处理危险情况,例如用户跳过登录页面直接访问内页,导致即使不输入账号密码也能访问到里面的页面。

js
router.beforeEach((to, from, next) => {
  if (to.path === "/login") {
    Vue.$cookies.keys().forEach((item) => Vue.$cookies.remove(item));
  }
  const token = Vue.$cookies.get("token");

  if (!token && to.path !== "/login") {
    next({ path: "/login" });
  } else {
    next();
  }
});

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行的,此时导航在所有守卫 resolve 完之前一直处于等待状态。

注意:必须调用 next 方法,否则钩子不会被 resolve。

在全局前置钩子中获取 vuex 的值(此时组件未挂载,拿不到 this.$store):

js
router.app.$options.store;

2. 路由独享守卫 beforeEnter ​

可以在路由配置上直接定义 beforeEnter 守卫:

js
const router = new VueRouter({
  routes: [
    {
      path: "/foo",
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      },
    },
  ],
});

这些守卫与全局前置守卫的方法参数是一样的。

3. 组件内守卫 ​

js
export default {
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不能获取组件实例 this,因为守卫执行前组件实例还没被创建
    // 可以传一个回调给 next 来访问组件实例,在导航被确认时执行回调,
    // 并把组件实例作为回调方法的参数
    next((vm) => {
      vm.$data.tab = to.meta.tab ? to.meta.tab : 0;
    });
  },

  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但该组件被复用时调用
    // 例如带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转时,
    // 由于渲染的是同一个 Foo 组件,组件实例会被复用,这个钩子就会在此情况下被调用
    // 可以访问组件实例 this
  },

  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 this
    // 离开守卫通常用来禁止用户在还未保存修改前突然离开,可通过 next(false) 取消导航
    const answer = window.confirm(
      "Do you really want to leave? you have unsaved changes!",
    );
    if (answer) {
      next();
    } else {
      next(false);
    }
  },
};

十、路由元信息 ​

定义路由时添加 meta 字段,即可配置路由元信息。

1. $route.matched ​

一个数组,包含当前路由的所有嵌套路径片段的路由记录:

js
const router = new VueRouter({
  routes: [
    // 下面的对象就是路由记录
    {
      path: "/foo",
      component: Foo,
      children: [
        // 这也是个路由记录
        { path: "bar", component: Bar },
      ],
    },
  ],
});

上面的路由将匹配到两个对象,一个为 foo,一个为 bar。

2. 用 meta 控制登录鉴权 ​

使用情景:一些页面不用登录可直接访问,但评论等功能或特定页面需要登录才可查看。

解决思路:通过设置路由元信息 meta: { requiresAuth: true } 决定该路由是否需要登录,再由全局前置守卫统一控制。

js
// 定义路由
routes: [
  {
    path: "/download",
    meta: { requiresAuth: true },
    component: download,
  },
];
js
/**
 * 路由全局前置守卫
 * 鉴权功能:判断页面 meta 中 requiresAuth 字段是否需要登录(true:需登录,false:不用登录)
 * 需登录页面判断登录状态,跳转
 */
router.beforeEach((to, from, next) => {
  if (to.matched.some((record) => record.meta.requiresAuth)) {
    const userAuth = Cookies.get("user");
    if (!userAuth) {
      // 非登录状态
      next({ path: "/login" });
    } else {
      next();
    }
  } else {
    next();
  }
});

十一、过渡动效 ​

1. 全局过渡 ​

html
<transition name="fade" mode="out-in">
  <router-view></router-view>
</transition>
css
.fade-enter-active,
.fade-leave-active {
  transition: all 0.2s ease;
}

.fade-enter,
.fade-leave-active {
  opacity: 0;
}

2. 单个路由的过渡 ​

在路由的 components 之外,还可以给单个 <router-view> 包裹 <transition>,只让该视图拥有过渡效果。

3. 基于路由的动态过渡 ​

html
<!-- 使用动态的 transition name -->
<transition :name="transitionName">
  <router-view></router-view>
</transition>
js
// 接着在父组件内 watch $route 决定使用哪种过渡
watch: {
  '$route'(to, from) {
    const toDepth = to.path.split('/').length
    const fromDepth = from.path.split('/').length
    this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
  },
},

十二、数据获取 ​

1. 在导航完成后获取数据 ​

在 created 生命周期钩子中获取数据。

2. 在导航完成前获取数据 ​

js
export default {
  data() {
    return {
      geoData: {},
    };
  },

  beforeRouteEnter(to, from, next) {
    // 不能获取组件实例 this,因为守卫执行前组件实例还没被创建
    // 在回调中获取组件实例
    localData.getAirData().then((res) => {
      next((vm) => {
        vm.$data.geoData = res.data;
      });
    });
  },
};

在为后面的视图获取数据时,用户会停留在当前界面,因此建议在数据获取期间显示进度条或别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。

十三、滚动行为 ​

注意:这个功能只在支持 history.pushState 的浏览器中可用。

默认情况下,切换路由时滚动条会回到顶部。savedPosition 当且仅当触发导航前进/后退按钮时才可用。

1. 模仿浏览器原生滚动 ​

js
const router = new Router({
  routes,
  scrollBehavior(to, from, savedPosition) {
    // return 期望滚动到的位置
    if (savedPosition) {
      console.log("a", savedPosition);
      return savedPosition;
    }
    return { x: 0, y: 0 };
  },
});

当用户通过前进/后退切换路由时,保持滚动条位置不变,否则定位到页面顶部。

2. 模拟「滚动到锚点」 ​

js
scrollBehavior(to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash,
    }
  }
}

3. 利用路由元信息细颗粒度控制滚动 ​

js
const scrollBehavior = (to, from, savedPosition) => {
  if (savedPosition) {
    // savedPosition 只在 popstate 导航时可用
    return savedPosition;
  }

  const position = {};
  // 新导航:返回 selector 即可滚动到锚点
  if (to.hash) {
    position.selector = to.hash;
  }
  // 如果 meta 中有 scrollToTop
  if (to.matched.some((m) => m.meta.scrollToTop)) {
    // 未提供 selector,或 selector 未匹配到任何元素时,会使用坐标定位
    position.x = 0;
    position.y = 0;
  }
  // 返回假值或空对象时,保持当前滚动位置
  return position;
};

4. 异步滚动 ​

js
scrollBehavior(to, from, savedPosition) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ x: 0, y: 0 })
    }, 500)
  })
}

十四、路由懒加载 ​

1. import 与 import() 的区别 ​

  • import * from '':在编译阶段执行,同步执行。
  • import(相对路径):异步执行,返回 Promise 对象。
js
const Components1 = () => import("../components/Components1");

2. 给打包后的 JS 文件命名 ​

通过注释语法修改打包后的 js 文件名:

js
const Login = () =>
  import(/* webpackChunkName: "Login" */ "@/views/AppLogin.vue");

3. 把组件按组分块 ​

在使用命名视图时,一个路由下可能有多个组件,若想把这多个组件打包到同一个 js 文件,可以通过注释语法实现:

js
const Foo = () => import(/* webpackChunkName: "group-foo" */ "./Foo.vue");
const Bar = () => import(/* webpackChunkName: "group-foo" */ "./Bar.vue");
const Baz = () => import(/* webpackChunkName: "group-foo" */ "./Baz.vue");

这样三个组件的 js 代码都会打包到名为 group-foo.js 的文件中。

十五、Router 实例方法 ​

1. router.go(n) ​

参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。

2. router.back() ​

后退一步,相当于 this.$router.go(-1)。