前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版

Vue 路由 vue-router 用法完全梳理(十一)

首页2018-08-28 15:30:32Front-End
Vuevue-router前端路由

第一次做单页应用,我卡在一个很蠢的问题上:地址栏明明变了,页面为什么没刷新?后来才反应过来,这正是前端路由要干的事。把「URL 变化」和「向服务器要一个新页面」这两件事解绑,改成由前端自己决定该渲染哪个组件。理清这一层之后,vue-router 的那堆 API 就不再是零散的配置项了,它们各自对应着一类真实需求。这篇把路由基础、动态匹配、嵌套、编程式跳转、命名视图、传参这几块串起来讲一遍,顺带把原文里几处写错的配置改对。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 前端路由是什么,它的优点和代价各在哪
  • hash 和 history 两种模式的差别,以及 history 模式为什么会 404
  • 动态路由匹配,商品详情页这类场景怎么配
  • 嵌套路由和命名视图分别解决什么布局问题
  • 编程式导航的几种写法,push 和 go 的区别
  • query 和 params 两种传参方式,为什么一般建议用 query
  • 重定向和别名的区别,别名不是跳转
  • Vue Router 4 的写法变化,老项目迁移要动哪里

# 一、路由基础

# 1.1 什么是前端路由

路由是根据不同的 url 地址展示不同的内容或页面。

前端路由就是把「不同的路由对应不同的内容或页面」这个任务交给前端来做。之前是通过服务端根据 url 的不同返回不同的页面实现的,现在这一步挪到了浏览器里。

实现上靠的是两套浏览器能力。一套是 hash,URL 里 # 后面的部分变化不会触发页面请求,监听 hashchange 事件就能知道该切哪个组件;另一套是 HTML5 的 History API,pushState 和 replaceState 可以在不刷新页面的前提下改地址栏,配合 popstate 事件感知前进后退。

# 1.2 什么时候使用前端路由

单页面应用里,大部分结构不变,只改变内容的场景。

比如后台管理系统,左侧菜单和顶部导航永远在,切菜单只换右侧那块内容区。这种结构用前端路由收益最大,因为不变的部分完全不需要重新渲染。

反过来,如果每个页面的结构差异很大、SEO 又很重要(比如内容站、电商详情页),前端路由就不一定划算。

# 1.3 前端路由的优点和代价

优点很直接:用户体验好,不需要每次都从服务器全部获取,切换是瞬时的。

代价这块,原文列了三条,这里逐条说清楚。

第一条是不利于 SEO。这是真的,因为搜索引擎爬虫拿到的是一个几乎空的 HTML 加一堆 JS。虽然现在主流爬虫具备执行 JS 的能力,但成本高、时机不可控,重视自然流量的站点还是得上服务端渲染。

第二条原文写的是「使用浏览器的前进后退键时会重新发送请求」。这条要修正一下:前进后退本身不会触发整页刷新,路由切换是在前端完成的。真正会重复请求的是数据,如果组件在 created 里发请求且没做缓存,每次回到这个路由都会再请求一次。所以问题不在路由机制,在数据层没有缓存,keep-alive 或者一个简单的内存缓存就能解决。

第三条是滚动位置。SPA 切换路由时页面不会重新加载,浏览器原生的滚动恢复也就不生效了,从列表页进详情再返回,回到的是顶部而不是原来那个位置。vue-router 提供了 scrollBehavior 配置来手动处理这件事,配合 keep-alive 记录 scrollTop 是常见做法。

# 二、用 vue-router 构建 SPA

# 2.1 从最小可运行的例子开始

在项目 src 目录下的 main.js 里注册插件:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)
@前端进阶之旅: 代码已经复制到剪贴板

vue-router 底层就是对 History API 的封装,地址后面跟 # 用的是 hash 模式。

先定义两个模板:

<div id="box">
</div>
<!--定义模版-->
<template id="a">
    <div>
        第一个router
    </div>
</template>
<template id="b">
    <div>
        第二个router
    </div>
</template>
@前端进阶之旅: 代码已经复制到剪贴板

再把路由表和实例接起来:

// 定义路由表
var routes = [
    {
        path: "/one",
        component: { template: "#a" }
    },
    {
        path: "/two",
        component: { template: "#b" }
    }
];
// 创建路由实例
var router = new VueRouter({
    routes
});
// 挂载到根实例上
new Vue({
    el: "#box",
    router
});
@前端进阶之旅: 代码已经复制到剪贴板

最后在模板里放入口和出口:

<div id="box">
    <router-link to="/one">One</router-link>
    <router-link to="/two">Two</router-link>
    <router-view></router-view>
</div>
@前端进阶之旅: 代码已经复制到剪贴板

两个组件的分工是这样的:

  • router-link 默认会被渲染成一个 a 标签,to 是我们定义的路由路径
  • router-view 是出口,路由匹配到的组件将渲染在这里

为什么不直接用 a 标签?因为原生 a 标签点击会触发浏览器导航,整页刷新,SPA 的状态全丢。router-link 内部拦掉了默认行为,改成调用路由的跳转方法,同时还会自动给当前激活的链接加上 router-link-active 类名,做导航高亮不用自己写逻辑。

# 2.2 hash 模式和 history 模式

在 new Router 中指定 mode 为 history 即可去掉 #,这样地址看起来更接近传统页面:

new Router({
    mode: "history",
    routes: []
})
@前端进阶之旅: 代码已经复制到剪贴板

这里有个坑要注意,而且是上线才会暴露的那种。history 模式下地址栏是真实路径,用户在 /goods/123 这个页面按 F5,浏览器会真的向服务器请求 /goods/123 这个资源,而服务器上根本没有这个文件,返回 404。解决办法是在服务器上配一条兜底规则,把所有未匹配到静态资源的请求都指向 index.html。Nginx 里就是 try_files $uri $uri/ /index.html; 这一行。

hash 模式没有这个问题,因为 # 后面的内容压根不会发给服务器。代价是地址不好看,而且某些第三方分享、微信 JS-SDK 签名遇到 hash 会有额外麻烦。

跳转的两种方式,模板里用 router-link,代码里用 this.$router.push:

// router-link 跳转标签,当 a 标签使用,to 必须是一个绝对地址
// <router-link to="/goods/title"></router-link>

// 或者用编程式导航
this.$router.push({ path: "/goods/title" })
@前端进阶之旅: 代码已经复制到剪贴板

# 2.3 动态路由匹配

通过变化的地址去加载不同的信息,路径里用冒号声明参数段:

模式 匹配路径 $route.params
/user/:username /user/poetries {username:"poetries"}
/user/:username/post/:post_id /user/poetries/post/123 {username:"poetries",post_id:"123"}

原文表格第二行的匹配结果写成了 evan,和左边的路径对不上,这里改回一致的。另外注意 post_id 拿到的是字符串 "123" 而不是数字,URL 里没有类型这回事,要数字得自己转。

典型应用场景是商城的详情页,要变换商品的 id,根据商品的 id 去查对应商品的信息。

这里还有一个高频问题:从 /user/a 跳到 /user/b,两个路径匹配的是同一个组件,Vue 会复用组件实例而不是销毁重建,所以 created 和 mounted 都不会再跑一遍,你在里面写的请求也就不会重新发。解决办法是 watch 一下 $route,或者给 router-view 加一个以完整路径为值的 key 强制重建。前者性能好,后者写起来省事。

# 2.4 嵌套路由

嵌套路由就是路由里面再套路由,对应的是「页面里有一块区域自己还要切换」的布局:

new Router({
    mode: "history",
    routes: [
        {
            path: "/goods",
            name: "GoodsList",
            component: GoodsList,
            children: [ // 定义子路由
               {
                   path: "title", // 最终形式 /goods/title
                   name: "title",
                   component: Title
               }
            ]
        }
    ]
})
@前端进阶之旅: 代码已经复制到剪贴板

有一点容易漏:父组件 GoodsList 的模板里必须自己放一个 router-view,子路由的组件才有地方渲染。忘了放的话,地址变了但页面上什么都不显示,也不报错。

子路由的 path 不以斜杠开头时是相对父路径拼接的,写成 /title 就变成了绝对路径,会直接挂在根上。这个区别在配置多级菜单的时候经常出问题。

# 2.5 编程式导航

通过 JavaScript 来实现页面跳转:

// 方式一,直接传路径字符串
this.$router.push("/cart")

// 方式二,传对象
this.$router.push({ path: "/cart" })

// 方式三,路径里拼查询参数
this.$router.push({ path: "/cart?a=123" })

// 或者用 query 字段,更推荐
this.$router.push({ path: "/cart", query: { a: 123 } })

// 方式四,在历史记录里前进后退
this.$router.go(1)   // 前进一步
this.$router.go(-1)  // 后退一步,等价于 this.$router.back()
@前端进阶之旅: 代码已经复制到剪贴板

push 和 go 是两类东西。push 是往历史栈里压一条新记录,用户按返回键能回到原来的页面;go 是在已有的历史栈里移动。还有一个 replace,它替换当前记录而不是新增,登录成功后跳首页最适合用它,这样用户按返回不会又回到登录页。

拿传过去的参数:

this.$router.push("/cart?goodsId=123")
@前端进阶之旅: 代码已经复制到剪贴板
<!--在页面上拿 goodsId-->
<span>{{$route.query.goodsId}}</span>
@前端进阶之旅: 代码已经复制到剪贴板

原文这里写的是 $.route.query,中间多了一个点,是个笔误,正确的是 $route。顺带把两个容易混的对象说清楚:$router 是路由实例,用来做跳转这类动作;$route 是当前路由信息对象,用来读 path、params、query 这些数据。一个动词一个名词,记这个就不会搞反。

# 2.6 命名路由

有时通过一个名称来标识一个路由更方便,特别是在链接一个路由或者执行跳转的时候。可以在创建 Router 实例时,在 routes 配置中给某个路由设置名称:

new Router({
    mode: "history",
    routes: [
        {
            path: "/cart/:cartId",
            name: "cart",
            component: Cart
        }
    ]
})
@前端进阶之旅: 代码已经复制到剪贴板

之前的跳转方式是写死路径:

fe
  • 一、路由基础
    • 1.1 什么是前端路由
    • 1.2 什么时候使用前端路由
    • 1.3 前端路由的优点和代价
  • 二、用 vue-router 构建 SPA
    • 2.1 从最小可运行的例子开始
    • 2.2 hash 模式和 history 模式
    • 2.3 动态路由匹配
    • 2.4 嵌套路由
    • 2.5 编程式导航
    • 2.6 命名路由
    • 2.7 命名视图
    • 2.8 重定向和别名
    • 2.9 列表进详情的传参
  • 三、Vue Router 4 有哪些变化
  • 总结
  • 参考

← vue-axios封装请求实战 拦截器与错误统一处理(十二)Vue 计算属性与 watch 数据监听详解(十) →