
参考资料:
本文全部基于 vue3。
- Chrome 插件:
Vue.js devtools 7.7.7 监控组件、路由等数据流
一、路由 router
背景:Vue等单页面应用的优缺点
优点
- 单页应用的内容的改变不需要重新加载整个页面,web应用更具响应性和更令人着迷。
- 单页应用没有页面之间的切换,就不会出现“白屏现象”,也不会出现假死并有“闪烁”现象
- 单页应用相对服务器压力小,服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力会提高几倍。
- 良好的前后端分离。后端不再负责模板渲染、输出页面工作,后端API通用化,即同一套后端程序代码,不用修改就可以用于Web界面、手机、平板等多种客户端。
缺点
- 首次加载耗时比较多。
- SEO问题,不利于百度,360等搜索引擎收录。
- 容易造成CSS命名冲突。
- 前进、后退、地址栏、书签等,都需要程序进行管理,页面的复杂度很高,需要一定的技能水平和开发成本高。
1.1 vue-router
安装:npm i vue-router@4
- 注意1:写 style 记得安装 sass:npm i sass 使用
<style lang="scss" scoped>
- 注意2:style.css 全局样式记得注释或删除(临时)
创建 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 }, ]
const router = createRouter({ history: createWebHashHistory(), routes, })
export default router
|
- 项目入口 main.js 中导入并使用 router
1 2 3 4 5 6 7 8 9
| import { createApp } from 'vue' import './style.css'
import App from './vueRouter/App.vue' import router from './vueRouter/router'
createApp(App) .use(router) .mount('#app')
|
- 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' }, ]
|
1.3 404 NotFound
router/index.js
1 2 3 4
| const routes = [ { ... }, { path: '/:pathMath(.*)*', component: NotFound }, ]
|
1.4 router-view 路由容器标签
1 2 3 4 5 6
| <template> <div> <!-- 全局路由容器组件 --> <router-view></router-view> </div> </template>
|
1.5 router-link 声明式导航
声明式导航 :和 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>
|
效果:

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' } }) router.push({ path: '/path', query: { 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', component: Detail }, ... ]
|
跳转带参-选项式写法:
1 2 3
| this.$router.push(`/detail/${id}`) this.$router.push({ name:'Detail', params: { myid: id }}) this.$router.push({ path:'/detail', query: { myid: id }})
|
页面接参-选项式写法:
1 2
| this.$route.params.myid this.$route.query.myid
|
前进返回-选项式写法:
1 2
| this.$router.back() this.$router.forward()
|
特殊情况:
比如 详情页的猜你喜欢,是 详情页跳转详情页 时,因为 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: createWebHashHistory(), routes, })
|
使用 history 模式理由有二:
- 路径好看,专业
- 分享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({...})
router.beforeEach( async (to, from, next) => { let isAuthenticated = await localStorage.getItem("token") if (to.name !== 'Login' && !isAuthenticated && to.meta.isLoginRequired ) { 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
|
const UserDetails = () => import('./views/UserDetails.vue')
const routes = [ { path: '/users/:id', component: UserDetails } { 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>
|