
参考资料:
1. 路由
根据不同的路径切换不同的页面。
1.1 SPA 单页面应用
SPA,单页面应用(SinglePage Web Application)。
MPA,多页面应用(MultiPageApplication)。
|
单页面应用(SPA) |
多页面应用(MPA) |
| 组成 |
一个外壳和多个页面片段组成 |
多个完整页面构成 |
| 资源共用(css, js) |
共用,只需在外壳部分加载 |
不共用,每个页面都需要加载 |
| 刷新方式 |
页面局部刷新或更改 |
整页刷新 |
| url模式 |
a.com/#/pageone a.com/#/pagetwo |
a.com/pageone.html a.com/pagetwo.html |
| 用户体验 |
页面片段间的切换快,用户体验良好 |
页面切换加载缓慢,流畅度不够,用户体验比较差 |
| 转场动画 |
容易实现 |
无法实现 |
| 数据传递 |
容易 |
依赖url传参、或者cookie、localStorage等 |
| 搜索引擎优化(SEO) |
需要单独方案、实现较为困难、不利于SEO检索、可利用服务器端渲染(SSR)优化 |
实现方法简易 |
| 试用范围 |
高要求的体验度、追求界面流畅的应用 |
试用于追求高度支持搜索引擎的应用 |
| 开发成本 |
较高,常需借助专业的框架 |
较低,但页面重复代码多 |
| 维护成本 |
相对容易 |
相对复杂 |
单页面应用与多页面应用的对比:https://www.cnblogs.com/zxlh1529/p/18796998
1.2 vue-router
路由可以建立 路径与组件 的映射关系。
vue-cli 创建的时候勾选 或 单独安装,同时确保 main.js 中 router 是开启的,可以在 package.json 中检查确认。
package.json
1 2 3 4 5
| "dependencies": { ... "vue-router": "^3.5.1", ... },
|
main.js
1 2 3 4 5 6 7 8 9 10 11
| import router from './router'
Vue.config.productionTip = false
new Vue({ router, render: h => h(App) }).$mount('#app')
|
1.3 路由配置
router/index.js 内的 routes 路由表配置,配置前需要在当前文件顶部进行 import 导入。
<router-view> 可以通过路由容器(类似插槽)将组件加载进来,对应访问路径配置的组件。
如目录 views 下创建有多个组件:
1 2 3 4 5 6 7 8
| . ├── src/ │ ├── router/ │ │ └── index.js │ ├── views/ │ │ ├── Center.vue │ │ ├── Cinemas.vue │ │ └── Films.vue
|
Center.vue | Cinemas.vue | Films.vue
1 2 3 4 5
| <template> <div> center | cinemas | films </div> </template>
|
router/index.js
import 导入新增的路由组件
routes 配置表添加对应的路径 path 和组件 component,建立路径与组件的关联
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
| import Vue from 'vue' import VueRouter from 'vue-router'
import Films from '@/views/Films.vue' import Center from '@/views/Center.vue' import Cinemas from '@/views/Cinemas.vue'
Vue.use(VueRouter)
const routes = [ { path: '/films', component: Films }, { path: '/center', component: Center }, { path: '/cinemas', component: Cinemas }, ]
const router = new VueRouter({ routes })
export default router
|
App.vue
1 2 3 4 5 6 7 8
| /* dom */ <template> <div> hello app-{{ myname }} <!-- 路由容器(类似插槽) --> <router-view></router-view> </div> </template>
|
效果:
访问不同的路径得到不同的组件加载渲染的内容:
1 2 3
| http://localhost:8080/#/cinemas hello app-jerry cinemas
|
1 2 3
| http://localhost:8080/#/center hello app-jerry center
|
1 2 3
| http://localhost:8080/#/films hello app-jerry films
|
1.4 重定向
routes 中配置 path 为根目录,对应 redirect 到具体的目录。
- 配置无顺序要求。
path: '*', 时可以防止随意访问路径导致页面没有组件可加载,即无内容显示的问题
1 2 3 4 5 6 7 8
| const routes = [ { path: '*', redirect: '/films' } ]
|
1.5 <router-link> 声明式导航
1.5.1 常规使用
编程式导航:js跳转,即location.href="url" 跳转到具体的url。
声明式导航:之前使用 a 链接跳转,vue中使用 <router-link> 实现跳转和高亮。
参数
- to,目标页面
- active-class,该属性默认不需要添加,如果添加则是指定当前激活的a标签的类,用于高亮
原理
- hash 路由:
- location.hash 切换,window.onhashchange 监听路径的切换
- history 路由:
- history.pushState 切换,window.onpopstate 监听路径的切换
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div> hello app-{{ myname }} <ul> <li> <a href="/#/films">电影</a> </li> <li> <a href="/#/cinemas">影院</a> </li> <li> <a href="/#/center">我的</a> </li> </ul>
<!-- 路由容器(类似插槽) --> <router-view></router-view> </div> </template> ...
|
使用声明式导航 router-link:
1 2 3 4 5
| <ul> <li> <router-link to="/films">电影</router-link> </li> </ul>
|
1 2
| <!-- 也可以对 active 类进行重命名 --> <router-link to="/films" active-class="jerry-active">电影</router-link>
|
实际HTML在点击时会自动转为 a 标签并添加加 class:
1
| <a href="#/films" class="router-link-exact-active router-link-active" aria-current="page">电影</a>
|
App.vue 就可以对其进行高亮显示设置:
1 2 3 4 5
| <style lang="scss" scoped> .router-link-active{ color: red; } </style>
|
1.5.2 版本差异
vue-router 3 和之前,比如 不需要li标签包裹,但是可以指定 tag 让其渲染成为 li 标签包裹
1 2 3 4
| <ul> <router-link to="/films" tag="li">电影</router-link> <router-link to="/cinemas">影院</router-link> </ul>
|
vue-router 4 后支持,更强大的扩展能力,使用 custom + v-slot 来调用navigate函数确定是否isActive选中,参考
1 2 3 4 5 6 7 8 9 10 11 12
| <ul> <router-link to="/films" custom v-slot="{navigate, isActive}"> <li @click="navigate" :class="isActive ? 'router-link-active' : ''"> 电影-{{isActive}} </li> </router-link> <router-link to="/cinemas" custom v-slot="{navigate, isActive}"> <li @click="navigate" :class="isActive ? 'router-link-active' : ''"> 影院-{{isActive}} </li> </router-link> </ul>
|
1.6 嵌套路由
1.6.1 真·二级路径
目录结构为真实父子关系的组件,路径也是父子关系,子组件对应二级路径。
1 2 3 4 5 6 7 8 9 10
| . ├── src/ │ ├── router/ │ │ └── index.js │ ├── views/ │ │ ├── films/ │ │ │ ├── Comingsoon.vue │ │ │ └── Nowplaying.vue │ │ ├── ... │ │ └── Films.vue
|
views/Films.vue (<router-view> 父组件标签用于二级声明式导航组件渲染,渲染内容对应路由表中 children)
1 2 3 4 5 6 7 8
| <template> <div> <div style="height: 200px; background: yellow">大轮播</div> <div>二级声明式导航</div> <!-- router-view 二级声明式导航组件对象渲染位置 --> <router-view></router-view> </div> </template>
|
views/films/Comingsoon.vue | views/films/Nowplaying.vue
1 2 3 4 5
| <template> <div> Coming soon... | Now playing... </div> </template>
|
router/index.js (包含二级路径在父路径访问时的默认重定向)
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
| ...
import Films from '@/views/Films.vue' import Comingsoon from '@/views/films/Comingsoon.vue' import Nowplaying from '@/views/films/Nowplaying.vue' ...
const routes = [ { path: '/films', component: Films, children: [ { path: '/films/nowplaying', component: Nowplaying }, { path: '/films/comingsoon', component: Comingsoon }, { path: '/films', redirect: '/films/nowplaying' } ] }, ... ] ...
|
效果:

1.6.2 假·二级路径
目录结构为同级目录结构,没有子级,只是访问路径上看起来是二级路径。
1 2 3 4 5 6 7
| . ├── src/ │ ├── router/ │ │ └── index.js │ ├── views/ │ │ ├── Search.vue │ │ └── Cinemas.vue
|
views/Cinemas.vue | views/Search.vue
1 2 3 4 5
| <template> <div> cinemas... | search... </div> </template>
|
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ... import Cinemas from '@/views/Cinemas.vue' import Search from '@/views/Search.vue' ...
const routes = [ ... { path: '/cinemas', component: Cinemas }, { path: '/cinemas/search', component: Search }, ... ] ...
|
效果:

1.7 $router 编程式导航
this.$router.push(path) 编程式导航,path 使用的是路由配置的相对路经。
- this.$router 等价于 main.js 中初始化的 router 对象。
示例:
views/films/Nowplaying.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
| <template> <div> Now playing... <ul> <li v-for='item in datalist' :key='item' @click='handleChangePage'> {{item}} </li> </ul> </div> </template>
<script> export default { data() { return { datalist: ['111', '222', '333'] } }, methods: { handleChangePage() { // 编程式导航 - dom写法 // location.href = '/#/detail' // 编程式导航 - vue写法 this.$router.push('/detail') //未传参数,仅跳转 } } } </script>
|
router/index.js
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
| ... import Detail from '@/views/Detail.vue' import Nowplaying from '@/views/films/Nowplaying.vue' ...
const routes = [ { path: '/films', component: Films, children: [ { path: '/films/nowplaying', component: Nowplaying }, { path: '/films/comingsoon', component: Comingsoon }, { path: '/films', redirect: '/films/nowplaying' } ] }, { path: '/detail', component: Detail }, ...
|
1.8 动态路由
典型应用:列表跳转详情。
1.路由:path: '/detail/:id', 动态二级路由,id可以自定义命名,匹配的路径必须拼接了id的值才能访问
2.传参:注意是 $router,.push()方法等价于 location.href 的页面路径跳转。
1
| this.$router.push(`/detail/${id}`)
|
3.接参:注意是 $route,参数在对象 params 中。
router/index.js
1 2 3 4 5 6 7 8
| const routes = [ { path: '/detail/:id', component: Detail }, ... ]
|
编程式导航-列表跳转详情传参:views/films/Nowplaying.vue
1 2 3 4 5 6 7 8 9 10 11 12
| ... <script> export default { ... methods: { handleChangePage(id) { // 1. vue编程式导航写法 + 字符串模版, id通过绑定的点击事件函数参数传进来 this.$router.push(`/detail/${id}`) } } } </script>
|
详情页接收参数:views/Detail.vue
1 2 3 4 5 6 7 8 9
| <script> export default { created() { // 当前匹配的路由 - 详情页 console.log("详情页", this.$route.params.id) // axios 利用id发请求到详情接口,获取详情数据,布局页面 } } </script>
|
1.9 命名路由
路由配置表中给路由指定 name 属性,可以通过 name 进行跳转或重定向。
router/index.js
1 2 3 4 5 6 7 8 9
| const routes = [ { name: 'filmDetail', path: '/detail/:id', component: Detail }, ... ]
|
通过命名路由跳转:views/films/Nowplaying.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ... <script> export default { ... methods: { handleChangePage(id) { // 2.通过命名路由跳转 + 传参id this.$router.push({ name: 'filmDetail', params: { id // id名字一样,简写 } }) } } } </script>
|
1.10 路由模式 ×2
hash 哈希路由模式,路径会带# 号分隔。
histroy History路由模式,路径不带# 号。参考官方说明和解决方案
router/index.js
1 2 3 4 5 6 7
| ... const router = new VueRouter({ mode: 'history', routes }) ...
|
理由有二:
- 路径好看,专业
- 分享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.11 路由拦截
场景:用户是否登陆的拦截判定。
1.11.1 全局路由拦截
meta 路由元信息,该字段在路由配置表中 routes 数组中可以自定义命名字段,标记拦截使用。
router.beforeEach((to, from, next) => {...} 全局拦截(路由守卫)配置逻辑。
- to - 目标页面
- from - 来源页面
- next() - 放行,继续往下走;如果 next(path) 则进入指定页面,如
next('/login')
router/index.js
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
| const routes = [ ... { path: '/center', component: Center, meta: { isLoginRequired: true } }, { path: '/login', component: Login }, ... ] ...
router.beforeEach((to, from, next) => { if (to.meta.isLoginRequired) { if (localStorage.getItem('token')) { next() } else { next({ path: '/login', query: { redirect: to.fullPath } }) } } else { next() } }) ...
|
views/Login.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
| <template> <div> Need Login..... <button @click="handleLogin">登陆</button> </div> </template>
<script> export default { methods: { handleLogin() { // 模拟登陆 setTimeout(() => { // 登陆完成后拿到token存起来 localStorage.setItem('token', '后端返回的token字段') // this.$router.back() // 登陆完返回上一页 // 1.获取query字段 // console.log(this.$route.query.redirect) // 2.跳转到源页面(跳转被拦截到登陆页前的页面) this.$router.push(this.$route.query.redirect) }, 500) } } } </script>
|
效果:

1.11.2 局部路由拦截
beforeEnter 路由独享守卫,即局部路由拦截。
1 2 3 4 5 6 7 8 9 10
| const routes = [ { path: '/users/:id', component: UserDetails, beforeEnter: (to, from) => { return false }, }, ]
|
1.11.3 组件内的拦截
组件内的守卫,也叫路由的生命周期。
可以为路由组件添加以下配置:
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
views/Center.vue - 我的页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script> export default { beforeRouteEnter(to, from, next) { // 在渲染该组件的对应路由被验证前调用 // 不能获取组件实例 `this` ! // 因为当守卫执行时,组件实例还没被创建! if (localStorage.getItem('token')) { next() } else { next('/login') } }, } </script>
|
1.12 路由懒加载
场景:当网站的模块和体量较大时,打包后js包会变很大,影响页面首屏加载。
方案:路由懒加载 - 作为项目变大后的优化手段。
- 把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件。
- 基于底层的 webpack 代码分隔功能,npm run build 编译打包出来后,js文件就会分离,以实现访问才加载。
- 项目足够小的时候,也没必要走懒加载
实现:compontent: () => import('@views/xxx.vue') 路由配置表中函数式写法。
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import Vue from 'vue' ...
...
const routes = [ ... { path: '/center', component: () => import('@/views/Center.vue'), meta: { isLoginRequired: true } }, ... ] ...
|
