09-Vue3路由

image-20200723170734421

参考资料:

本文全部基于 vue3。

  • Chrome 插件:Vue.js devtools 7.7.7 监控组件、路由等数据流

一、路由 router

背景:Vue等单页面应用的优缺点

优点

  1. 单页应用的内容的改变不需要重新加载整个页面,web应用更具响应性和更令人着迷。
  2. 单页应用没有页面之间的切换,就不会出现“白屏现象”,也不会出现假死并有“闪烁”现象
  3. 单页应用相对服务器压力小,服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力会提高几倍。
  4. 良好的前后端分离。后端不再负责模板渲染、输出页面工作,后端API通用化,即同一套后端程序代码,不用修改就可以用于Web界面、手机、平板等多种客户端。

缺点

  1. 首次加载耗时比较多。
  2. SEO问题,不利于百度,360等搜索引擎收录。
  3. 容易造成CSS命名冲突。
  4. 前进、后退、地址栏、书签等,都需要程序进行管理,页面的复杂度很高,需要一定的技能水平和开发成本高。

1.1 vue-router

  1. 安装:npm i vue-router@4

    • 注意1:写 style 记得安装 sass:npm i sass 使用 <style lang="scss" scoped>
    • 注意2:style.css 全局样式记得注释或删除(临时)
  2. 创建 router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import Films from '../views/Films.vue'
import Cinemas from '../views/Cinemas.vue'
import Center from '../views/Center.vue'

const routes = [
{ path: '/films', component: Films },
{ path: '/cinemas', component: Cinemas },
{ path: '/center', alias: '/wode', component: Center }, // alias 别名,也可访问
]

const router = createRouter({
// history: createWebHistory(), // 路由模式:history 模式,url 路径不带 #
history: createWebHashHistory(), // 路由模式:hash 模式,url 路径带 #
routes, // routes: routes, 的缩写
})

export default router
  1. 项目入口 main.js 中导入并使用 router
1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import './style.css'
// import App from './App.vue'
import App from './vueRouter/App.vue'
import router from './vueRouter/router' //导入路由插件

createApp(App)
.use(router) //使用路由插件
.mount('#app')
  1. App.vue 中使用全局路由容器组件
1
2
3
4
5
6
7
<template>
<div>
app
<!-- 全局路由容器组件 -->
<router-view></router-view>
</div>
</template>

1.2 redirect 重定向

router/index.js

1
2
3
4
5
const routes = [
{ ... },
{ path: '/', redirect: '/films' }, // 通过 path 路径重定向
//{ path: '/', redirect: { name: 'films' } }, // 通过 name 属性重定向
]

1.3 404 NotFound

router/index.js

1
2
3
4
const routes = [
{ ... },
{ path: '/:pathMath(.*)*', component: NotFound }, // 前面都未匹配上时,即路径最终不匹配则加载404组件
]

1.4 router-view 路由容器标签

1
2
3
4
5
6
<template>
<div>
<!-- 全局路由容器组件 -->
<router-view></router-view>
</div>
</template>

声明式导航 :和 a 标签一样的效果,但 router-link 可以自动匹配路由模式,不会渲染为 a 标签,且更便于设置高亮显示。

声明式导航使用方式(v-solt 插槽方式自动处理点击高亮):

1
2
3
<router-link custom to="/films" v-slot="{isActive, navigate}">
<li :class="isActive ? 'jerry-active' : ''" @click="navigate">电影</li>
</router-link>

如 Tabbar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div class="tabbar">
<ul>
<router-link custom to="/films" v-slot="{isActive, navigate}">
<li :class="isActive ? 'jerry-active' : ''" @click="navigate">电影</li>
</router-link>
<router-link custom to="/cinemas" v-slot="{isActive, navigate}">
<li :class="isActive ? 'jerry-active' : ''" @click="navigate">影院</li>
</router-link>
<router-link custom to="/center" v-slot="{isActive, navigate}">
<li :class="isActive ? 'jerry-active' : ''" @click="navigate">我的</li>
</router-link>
</ul>
</div>
</template>

<style lang="scss" scoped>
.tabbar {
position: fixed;
bottom: 0;
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
ul {
display: flex;
li {
flex: 1;
}
}
}
.jerry-active{
color: red;
}
</style>

1.6 嵌套路由

路由中通过 children 属性进行配置,需要通过 router-view 路由容器标签在 dom 中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const routes = [
{
path: '/films',
component: Films,
name: 'films',
children: [
{
path: '/films/nowplaying', //必须加父路径
component: Nowplaying
},
{
path: '/films/comingsoon',
component: Comingsoon
},
{
path: '/films',
redirect: '/films/nowplaying' //组件内部重定向,默认进入此路径
}
]
},
...
]

dom使用:

1
2
3
4
5
6
7
8
<template>
<div>
films
<!-- 对应路由: /films/nowplaying 或 /films/comingsoon -->
<router-view></router-view>
</div>
</template>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div>
<ul>
<router-link custom to="/films/nowplaying" v-slot="{ isActive, navigate }">
<li @click="navigate">
<!-- 高亮样式拆解到 span 上,更具灵活性 -->
<span :class="isActive ? 'jerry-active' : ''">正在热映</span>
</li>
</router-link>
<router-link custom to="/films/comingsoon" v-slot="{ isActive, navigate }">
<li @click="navigate">
<span :class="isActive ? 'jerry-active' : ''">即将上映</span>
</li>
</router-link>
</ul>

<!-- 对应路由配置: /films/nowplaying 或 /films/comingsoon -->
<router-view></router-view>
</div>
</template>


<style lang="scss" scoped>
ul {
display: flex;
height: 50px;
line-height: 50px;
text-align: 50px;
li{
flex: 1;
text-align: center;
}
}
.jerry-active{
color: red;
border-bottom: 2px solid red;
padding-bottom: 10px;
}
</style>

效果:

chrome-capture-2026-01-05

1.7 router.push() 编程式导航

声明式(:to 动态跳转) 编程式
<router-link :to="..."> router.push(...)
1
2
3
4
router.push('/path')  //字符串路径
router.push({ path: '/path' }) //带有路径的对象
router.push({ name: 'name', params: { key: 'value' } }) //命名的路由,并加上参数,让路由建立url
router.push({ path: '/path', query: { key: 'value' } }) //待查询参数,结果是 /path?key=value

1.8 动态路由匹配(★)

同一个路由中设置多个路径参数,它们会映射到 $router.params 相应的字段。如:

匹配模式 匹配路径 $router.params
/users/:username /users/jerry {username: 'jerry'}
/users/:username/posts/:postId /users/jerry/posts/123 {username: 'jerry', postId: '123'}

router/index.js

1
2
3
4
5
6
7
8
const routes = [
{
name: 'Detail' //用于跳转带参数时的【对象方式】
path: '/detail/:myid', //动态匹配 myid 字段: /detail/123456,接参时使用 myid
component: Detail
},
...
]

跳转带参-选项式写法:

1
2
3
this.$router.push(`/detail/${id}`)  //路径方式
this.$router.push({ name:'Detail', params: { myid: id }}) //对象方式&params
this.$router.push({ path:'/detail', query: { myid: id }}) //对象方式&query - (匹配路由格式 path:'/detail' )

页面接参-选项式写法:

1
2
this.$route.params.myid  //接参数对应:路径方式 or 对象方式&params
this.$route.query.myid //接参数对应:对象方式&query

前进返回-选项式写法:

1
2
this.$router.back()     //返回上一页:等价于 this.$router.go(-1)
this.$router.forward() //前进下一页:等价于 this.$router.go(1) -- 条件:有历史记录时

特殊情况

比如 详情页的猜你喜欢,是 详情页跳转详情页 时,因为 mounted 只会触发一次,会导致新的 详情id 无法拿到。

原因:组件没有销毁,即没有重新加载,所以不会重新出发 mounted

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>电影详情页(ID:{{ currentId }})</div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router' // 导入路由工具

const route = useRoute() // 1. 获取路由实例(响应式)
const currentId = ref(route.params.id) // 2. 存储当前详情ID

// 3. 监听路由中id的变化
watch( () => route.params.id, // 监听目标:路由参数里的id(用函数返回响应式值)
(newId, oldId) => { // 变化后的回调(新id、旧id)
if (newId) {
currentId.value = newId
fetchMovieData(newId) // 核心逻辑:id变化后重新请求详情数据(示例用console模拟)
}
},
{ immediate: true } // 可选:初始化时立即执行一次(避免首次进入不触发)
)
// 模拟:根据id请求详情数据的函数
const fetchMovieData = (id) => {
console.log('axios请求电影详情,ID:', id)
}
</script>

1.9 路由模式 hash | history

1
2
3
4
5
const router = createRouter({
// history: createWebHistory(), // 路由模式:history 模式,url 路径不带 #
history: createWebHashHistory(), // 路由模式:hash 模式,url 路径带 #
routes,
})

使用 history 模式理由有二:

  1. 路径好看,专业
  2. 分享url时,路径不会被截断。

风险问题(官方说明):

不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。

所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

【总结】如果后端没有配置默认返回index.html,就可能会出现404的情况。

【解决】nginx 配置加这一句:

1
2
3
location / {
try_files $uri $uri/ /index.html;
}

1.10 路由拦截/守卫

全局拦截(全局守卫)

  • router.beforeEach() 前置钩子,多用于登陆认证
  • router.afterEach() 后置钩子,多用于用户行为数据上传用于分析
1
2
3
4
5
6
7
8
9
10
11
12
const router = createRouter({...})
//全局拦截:next() 放行继续执行
router.beforeEach( async (to, from, next) => {
let isAuthenticated = await localStorage.getItem("token")
//console.log(to.fullPath) // to.fullPath 可以用于放行路径的白名单-即未登录可访问路径(不推荐)
//如果路由 name不是Login 并且 未登陆认证 并且 需要认证
if (to.name !== 'Login' && !isAuthenticated && to.meta.isLoginRequired /* && to.fullPath不在白名单内 */) {
next({ name: 'Login' })
} else {
next()
}
})

meta 标签:

1
2
3
4
5
6
7
8
9
10
11
const routes = [
...
{
path: '/center',
component: Center,
meta: {
isLoginRequired: true // 自定义字段,自行命名,标记拦截使用
}
},
...
]

局部拦截组件内守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'

// 与 beforeRouteLeave 相同,无法访问 `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm("你确定要离开页面吗?确定-离开, 取消-停留当前页面")
if (!answer) return false // 取消导航并停留在同一页面上
})

const userData = ref()

// 与 beforeRouteUpdate 相同,无法访问 `this`
onBeforeRouteUpdate(async (to, from) => {
//仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
</script>

组合式api中没有 beforeRouteEnter(to, from) {...} 因此可以结合起来,如果真的需要,可以写两个 script(一个普通的选项式的,一个组合式)。

1.11 路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。——属于一种项目变大时的优化方案

router/index.js

1
2
3
4
5
6
7
8
9
10
11
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成(方式1)
const UserDetails = () => import('./views/UserDetails.vue')

const routes = [
// 方式1:使用
{ path: '/users/:id', component: UserDetails }
// 方式2:在路由定义里直接使用它
{ path: '/users/:id', component: () => import('./views/UserDetails.vue') },
]

1.12 路由-组合式api写法(★)

路由组合式api:https://router.vuejs.org/zh/guide/advanced/composition-api.html

路由跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()
const pushWithQuery = (query) => {
router.push({
name: 'search',
query: {
...route.query,
...query,
},
})
}
</script>

监听参数更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'

const route = useRoute()
const userData = ref()
// 当参数更改时获取用户信息
watch(
() => route.params.id,
async newId => {
userData.value = await fetchUser(newId)
}
)
</script>

09-Vue3路由
https://janycode.github.io/2022/05/22/04_大前端/04_Vue/09-Vue3路由/
作者
Jerry(姜源)
发布于
2022年5月22日
许可协议