前端路由
前端路由是后来发展到SPA(单页应用)时才出现的概念。 SPA 就是一个WEB项目只有一个 HTML 页面,一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转。
前端路由在SPA项目中是必不可少的,页面的跳转、刷新都与路由有关,通过不同的url显示相应的页面。
优点:前后端的彻底分离,不刷新页面,用户体验较好,页面持久性较好。
后端路由
当在地址栏切换不同的url时,都会向服务器发送一个请求,服务器接收并响应这个请求,在服务端拼接好html文件返回给页面来展示。
优点:减轻了前端的压力,html都由后端拼接;
缺点:依赖于网络,网速慢,用户体验很差,项目比较庞大时,服务器端压力较大,
不能在地址栏输入指定的url访问相应的模块,前后端不分离。
路由模式
前端路由实现起来其实很简单,本质是监听 URL 的变化,然后匹配路由规则,在不刷新的情况下显示相应的页面。
hash模式(对应HashHistory)
- 把前端路由的路径用井号 # 拼接在真实 url 后面的模式,但是会覆盖锚点定位元素的功能,通过监听 URL 的哈希部分变化,相应地更新页面的内容。
- 前端路由的处理完全在客户端进行,在路由发生变化时,只会改变 URL 中的哈希部分(井号 # 后面的路径),且不会向服务器发送新的请求,而是触发 onhashchange 事件。
- hash 只有#符号之前的内容才会包含在请求中被发送到后端,如果 nginx 没有匹配得到当前的 url 也没关系。hash 永远不会提交到 server 端。
- hash值的改变,都会在浏览器的访问历史中增加一个记录,所以可以通过浏览器的回退、前进按钮控制hash的切换。
- hash 路由不会造成 404 页面的问题,因为所有路由信息都在客户端进行解析和处理,服务器只负责提供应用的初始 HTML 页面和静态资源,不需要关心路由的匹配问题。
// onhashchage事件,可以在window对象上监听这个事件
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL)
let hash = location.hash.slice(1)
}
- 通过location.hash修改hash值,触发更新。
- 通过监听hashchange事件监听浏览器前进或者后退,触发更新。
history模式 (对应HTML5History)
- 是 html5 新推出的功能,比 Hash url 更美观
- 在 history 模式下浏览器在刷新页面时,会按照路径发送真实的资源请求。如果 nginx 没有匹配得到当前的 url ,就会出现 404 的页面。
- 在使用 history 模式时,需要通过服务端支持允许地址可访问,如果没有设置,就很容易导致出现 404 的局面。
- 改变url: history 提供了 pushState 和 replaceState 两个方法来记录路由状态,这两个方法只改变 URL 不会引起页面刷新。
- 监听url变化:通过 onpopstate 事件监听history变化,在点击浏览器的前进或者后退功能时触发,在onpopstate 事件中根据状态信息加载对应的页面内容。
history.replaceState({}, null, '/b') // 替换路由
history.pushState({}, null, '/a') // 路由压栈,记录浏览器的历史栈 不刷新页面
history.back() // 返回
history.forward() // 前进
history.go(-2) // 后退2次
history.pushState 修改浏览器地址,而页面的加载是通过 onpopstate 事件监听实现,加载对应的页面内容,完成页面更新。
// 页面加载完毕 first.html
history.pushState({page: 1}, "", "first.html");
window.onpopstate = function(event) {
// 根据当前 URL 加载对应页面
loadPage(location.pathname);
};
// 点击跳转到 second.html
history.pushState({page: 2}, "", "second.html");
function loadPage(url) {
// 加载 url 对应页面内容
// 渲染页面
}
onpopstate 事件是浏览器历史导航的核心事件,它标识了页面状态的变化时机。通过监听这个时机,根据最新的状态信息更新页面
当使用 history.pushState() 或 history.replaceState() 方法修改浏览器的历史记录时,不会直接触发 onpopstate 事件。
但是,可以在调用这些方法时将数据存储在历史记录条目的状态对象中, onpopstate 事件在处理程序中访问该状态对象。这样,就可以在不触发 onpopstate 事件的情况下更新页面内容,并获取到相应的状态值。
history 模式下 404 页面的处理
在 history 模式下,浏览器会向服务器发起请求,服务器根据请求的路径进行匹配:
如果服务器无法找到与请求路径匹配的资源或路由处理器,服务器可以返回 /404 路由,跳转到项目中配置的 404 页面,指示该路径未找到。
对于使用历史路由模式的单页应用(SPA),通常会在服务器配置中添加一个通配符路由,将所有非静态资源的请求都重定向到主页或一个自定义的 404 页面,以保证在前端处理路由时不会出现真正的 404 错误页面。
在项目中配置对应的 404 页面:
export const publicRoutes = [
{
path: '/404',
component: () => import('src/views/404/index'),
},
]
vueRouter
Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,允许你在 Vue 应用中构建单页面应用(SPA),并且提供了灵活的路由配置和导航功能。让用 Vue.js 构建单页应用变得轻而易举。功能包括:
- 路由映射:可以将 url 映射到 Vue组件,实现不同 url 对应不同的页面内容。
- 嵌套路由映射:可以在路由下定义子路由,实现更复杂的页面结构和嵌套组件的渲染。
- 动态路由:通过路由参数传递数据。你可以在路由配置中定义带有参数的路由路径,并通过 $route.params 获取传递的参数。
- 模块化、基于组件的路由配置:路由配置是基于组件的,每个路由都可以指定一个 Vue 组件作为其页面内容,将路由配置拆分为多个模块,在需要的地方引入。。
- 路由参数、查询、通配符:通过路由参数传递数据,实现页面间的数据传递和动态展示。
- 导航守卫:Vue Router 提供了全局的导航守卫和路由级别的导航守卫,可以在路由跳转前后执行一些操作,如验证用户权限、加载数据等。
- 展示由 Vue.js 的过渡系统提供的过渡效果:可以为路由组件添加过渡效果,使页面切换更加平滑和有动感。
- 细致的导航控制:可以通过编程式导航(通过 JavaScript 控制路由跳转)和声明式导航(通过 组件实现跳转)实现页面的跳转。
- 路由模式设置:Vue Router 支持两种路由模式:HTML5 history 模式或 hash 模式
- 可定制的滚动行为:当页面切换时,Vue Router 可以自动处理滚动位置。定制滚动行为,例如滚动到页面顶部或指定的元素位置。
- URL 的正确编码:Vue Router 会自动对 URL 进行正确的编码
路由组件
- **router-link:**通过 router-link 创建链接 其本质是
a
标签,这使得 Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码。 - **router-view:**router-view 将显示与 url 对应的组件。
$router
、$route
$route: 是当前路由信息对象,获取和当前路由有关的信息。 route 为属性是只读的,里面的属性是 immutable (不可变) 的,不过可以通过 watch 监听路由的变化。
fullPath: "" // 当前路由完整路径,包含查询参数和 hash 的完整路径
hash: "" // 当前路由的 hash 值 (锚点)
matched: [] // 包含当前路由的所有嵌套路径片段的路由记录
meta: {} // 路由文件中自赋值的meta信息
name: "" // 路由名称
params: {} // 一个 key/value 对象,包含了动态片段和全匹配片段就是一个空对象。
path: "" // 字符串,对应当前路由的路径
query: {} // 一个 key/value 对象,表示 URL 查询参数。跟随在路径后用'?'带的参数
$router 是 vueRouter 实例对象,是一个全局路由对象,通过 this.$router 访问路由器, 可以获取整个路由文件或使用路由提供的方法。
// 导航守卫
router.beforeEach((to, from, next) => {
/* 必须调用 `next` */
})
router.beforeResolve((to, from, next) => {
/* 必须调用 `next` */
})
router.afterEach((to, from) => {})
动态导航到新路由
router.push
router.replace
router.go
router.back
router.forward
routes 是 router 路由实例用来配置路由对象 可以使用路由懒加载(动态加载路由)的方式
- 把不同路由对应的组件分割成不同的代码块,当路由被访问时才去加载对应的组件 即为路由的懒加载,可以加快项目的加载速度,提高效率
const router = new VueRouter({
routes: [
{
path: '/home',
name: 'Home',
component:() = import('../views/home')
}
]
})
vueRouter的使用
页面中路由展示位置
<div id="app">
<!-- 添加路由 -->
<!-- 会被渲染为 <a href="#/home"></a> -->
<router-link to="/home">Home</router-link>
<router-link to="/login">Login</router-link>
<!-- 展示路由的内容 -->
<router-view></router-view>
</div>
路由模块 引入 vue-router,使用 Vue.use(VueRouter) 注册路由插件 定义路由数组,并将数组传入VueRouter 实例,并将实例暴露出去
import Vue from 'vue'
import VueRouter from 'vue-router'
import { hasVisitPermission, isWhiteList } from './permission'
// 注册路由组件
Vue.use(VueRouter)
// 创建路由: 每一个路由规则都是一个对象
const routers =[
// path 路由的地址
// component 路由的所展示的组件
{
path: '/',
// 当访问 '/'的时候 路由重定向 到新的地址 '/home'
redirect: '/home',
},
{
path: '/home',
component: home,
},
{
path: '/login',
component: login,
},
],
// 实例化 VueRouter 路由
const router = new VueRouter({
mode: 'history',
base: '/',
routers
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 清除面包屑导航数据
store.commit('common/SET_BREAD_NAV', [])
// 是否白名单
if (isWhiteList(to)) {
next()
} else {
// 未登录,先登录
try {
if (!store.state.user.userInfo) {
await store.dispatch('user/getUserInfo')
}
// 登录后判断,是否有访问页面的权限
if (!hasVisitPermission(to, store.state.user.userInfo)) {
next({ path: '/404' })
} else {
next()
}
} catch (err) {
$error(err)
}
}
})
export default router
在 main.js 上挂载路由 将VueRouter实例引入到main.js,并注册到根Vue实例上
import router from './router'
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
动态路由
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。我们可以在 vueRrouter 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果。
- 动态路由的创建,主要是使用 path 属性过程中,使用动态路径参数,路径参数 用冒号 : 表示。
当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.query 的形式暴露出来。因此,我们可以通过更新 User 的模板来呈现当前的用户 ID:
_vue-router _通过配置 _params _和 _query _来实现动态路由
params 传参
-
必须使用 命名路由 name 传值
-
参数不会显示在 url 上
-
浏览器强制刷新时传参会被清空
// 传递参数
this.$router.push({
name: Home,
params: {
number: 1 ,
code: '999'
}
})
// 接收参数
const p = this.$route.params
query 传参
- 可以用 name 也可以使用 path 传参
- 传递的参数会显示在 url 上
- 页面刷新是传参不会丢失
// 方式一:路由拼接
this.$router.push('/home?username=xixi&age=18')
// 方式二:name + query 传参
this.$router.push({
name: Home,
query: {
username: 'xixi',
age: 18
}
})
// 方式三:path + name 传参
this.$router.push({
path: '/home',
query: {
username: 'xixi',
age: 18
}
})
// 接收参数
const q = this.$route.query
keep-alive
keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
和 transition 相似,keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
keep-alive 可以设置以下props属性:
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
- max - 数字。最多可以缓存多少组件实例
在不缓存组件实例的情况下,每次切换都会重新 render,执行整个生命周期,每次切换时,重新 render,重新请求,必然不满足需求。 会消耗大量的性能
keep-alive 的基本使用
只是在进入当前路由的第一次render,来回切换不会重新执行生命周期,且能缓存router-view的数据。 通过 include 来判断是否匹配缓存的组件名称: 匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配
<keep-alive>
<router-view></router-view>
</keep-alive>
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
路由配置 keepAlive
在路由中设置 keepAlive 属性判断是否需要缓存
{
path: 'list',
name: 'itemList', // 列表页
component (resolve) {
require(['@/pages/item/list'], resolve)
},
meta: {
keepAlive: true,
compName: 'ItemList'
title: '列表页'
}
}
{
path: 'management/class_detail/:id/:activeIndex/:status',
name: 'class_detail',
meta: {
title: '开班详情',
keepAlive: true,
compName: 'ClassInfoDetail',
hideInMenu: true,
},
component: () => import('src/views/classManage/class_detail.vue'),
},
使用
<div id="app" class='wrapper'>
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
keepAlive 对生命周期的影响
设置缓存后组件加载的生命周期会新增 actived 与 deactived
- 首次进入组件时也会触发 actived 钩子函数:beforeRouteEnter > beforeCreate > created> beforeMount > beforeRouteEnter 的 next 回调> mounted > activated > ... ... > beforeRouteLeave > deactivated
- 再次进入组件时直接获取actived的组件内容:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated
keep-alive 组件监听 include 及 exclude 的缓存规则,若发生变化则执行 pruneCache (遍历cache 的name判断是否需要缓存,否则将其剔除) 且 keep-alive 中没有 template,而是用了 render,在组件渲染的时候会自动执行 render 函数,
- 若命中缓存则直接从缓存中拿 vnode 的组件实例,
- 若未命中缓存且未被缓存过则将该组件存入缓存,
- 当缓存数量超出最大缓存数量时,删除缓存中的第一个组件。
动态路由缓存的的具体表现在:
- 由动态路由配置的路由只能缓存一份数据。
- keep-alive 动态路由只有第一个会有完整的生命周期,之后的路由只会触发 actived 和 deactivated这两个钩子。
- 一旦更改动态路由的某个路由数据,期所有同路由下的动态路由数据都会同步更新。
如何删除 keep-alive 中的缓存
vue2 中清除路由缓存
在组件内可以通过 this 获取 vuerouter 的缓存
vm.$vnode.parent.componentInstance.cache
或者通过 ref 获取 外级 dom
<template>
<el-container id="app-wrapper">
<Aside />
<el-container>
<el-header id="app-header" height="45px">
<Header @removeCacheRoute="removeCacheRoute" />
</el-header>
<!-- {{ includeViews }} -->
<el-main id="app-main">
<keep-alive :include="includeViews">
<router-view ref="routerViewRef" :key="key" />
</keep-alive>
</el-main>
</el-container>
</el-container>
</template>
<script>
import Aside from './components/Aside'
import Header from './components/Header'
import { mapGetters } from 'vuex'
export default {
name: 'Layout',
components: {
Aside,
Header,
},
data () {
return {
}
},
computed: {
...mapGetters(['cacheRoute', 'excludeRoute']),
includeViews () {
return this.cacheRoute.map(item => item.compName)
},
key () {
return this.$route.fullPath
},
},
methods: {
removeCacheRoute (fullPath) {
const cache = this.$refs.routerViewRef.$vnode.parent.componentInstance.cache
delete cache[fullPath]
},
},
}
</script>
路由守卫
导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
通俗来讲:路由守卫就是路由跳转过程中的一些生命周期函数(钩子函数),我们可以利用这些钩子函数帮我们实现一些需求。
路由守卫又具体分为 全局路由守卫、独享守卫 及 组件路由守卫。
全局路由守卫
- 全局前置守卫router.beforeEach
- 全局解析守卫:router.beforeResolve
- 全局后置守卫:router.afterEach
beforeEach(to,from, next)
在路由跳转前触发,参数包括to,from,next 三个,这个钩子作用主要是用于登录验证。
前置守卫也可以理解为一个路由拦截器,也就是说所有的路由在跳转前都要先被前置守卫拦截。
router.beforeEach(async (to, from, next) => {
// 清除面包屑导航数据
store.commit('common/SET_BREAD_NAV', [])
// 是否白名单
if (isWhiteList(to)) {
next()
} else {
// 未登录,先登录
try {
if (!store.state.user.userInfo) {
await store.dispatch('user/getUserInfo')
// 登录后判断,是否有角色, 无角色 到平台默认页
if (!store.state.user.userInfo.permissions || !store.state.user.userInfo.permissions.length) {
next({ path: '/noPermission' })
}
}
// 登录后判断,是否有访问页面的权限
if (!hasVisitPermission(to, store.state.user.userInfo)) {
next({ path: '/404' })
} else {
next()
}
} catch (err) {
$error(err)
}
}
})
beforeResolve(to,from, next)
在每次导航时都会触发,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。
即在 beforeEach 和 组件内 beforeRouteEnter 之后,afterEach之前调用
router.beforeResolve 是获取数据或执行任何其他操作的理想位置
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
afterEach(to,from)
和beforeEach相反,他是在路由跳转完成后触发,参数包括to, from 由于此时路由已经完成跳转 所以不会再有next。
全局后置守卫对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
router.afterEach((to, from) => {
// 在路由完成跳转后执行,实现分析、更改页面标题、声明页面等辅助功能
sendToAnalytics(to.fullPath)
})
独享路由守卫
beforeEnter(to,from, next) 独享路由守卫可以直接在路由配置上定义,但是它只在进入路由时触发,不会在 params、query 或 hash 改变时触发
const routes = [
{
path: '/users/:id',
component: UserDetails,
// 在路由配置中定义守卫
beforeEnter: (to, from,next) => {
next()
},
},
]
或是使用数组的方式传递给 beforeEnter ,有利于实现路由守卫的重用
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
组件路由守卫
在组件内使用的钩子函数,类似于组件的生命周期, 钩子函数执行的顺序包括
- beforeRouteEnter(to,from, next) -- 进入前
- beforeRouteUpdate(to,from, next) -- 路由变化时
- beforeRouteLeave(to,from, next) -- 离开后
组件内路由守卫的执行时机:
<template>
...
</template>
export default{
data(){
//...
},
// 在渲染该组件的对应路由被验证前调用
beforeRouteEnter (to, from, next) {
// 此时 不能获取组件实例 this
// 因为当守卫执行前,组件实例还没被创建
next((vm)=>{
// next 回调 在 组件 beforeMount 之后执行 此时组件实例已创建,
// 可以通过 vm 访问组件实例
console.log('A组件中的路由守卫==>> beforeRouteEnter 中next 回调 vm', vm)
)
},
// 可用于检测路由的变化
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用 此时组件已挂载完可以访问组件实例 `this`
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
console.log('组件中的路由守卫==>> beforeRouteUpdate')
next()
},
// 在导航离开渲染该组件的对应路由时调用
beforeRouteLeave (to, from, next) {
// 可以访问组件实例 `this`
console.log('A组件中的路由守卫==>> beforeRouteLeave')
next()
}
}
<style>
...
</style>
注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持 传递回调,因为没有必要了
路由守卫触发流程
页面加载时路由守卫触发顺序:
- 触发全局的路由守卫 beforeEach
- 组件在路由配置的独享路由 beforeEnter
- 进入组件中的 beforeRouteEnter,此时无法获取组件对象
- 触发全局解析守卫 beforeResolve
- 此时路由完成跳转 触发全局后置守卫 afterEach
- 组件的挂载 beforeCreate --> created --> beforeMount
- 路由守卫 beforeRouterEnter 中的 next回调, 此时能够获取到组件实例 vm
- 完成组件的挂载 mounted
当点击切换路由时: A页面跳转至B页面触发的生命周期及路由守卫顺序:
- 导航被触发进入其他路由。
- 在离开的路由组件中调用 beforeRouteLeave 。
- 调用全局的前置路由守卫 beforeEach 。
- 在重用的组件里调用 beforeRouteUpdate 守卫。
- 调用被激活组件的路由配置中调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件中调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫。
- 导航被确认。
- 调用全局后置路由 afterEach 钩子。
- 触发 DOM 更新,激活组件的创建及挂载 beforeCreate (新)-->created (新)-->beforeMount(新) 。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
- 失活组件的销毁 beforeDestory(旧)-->destoryed(旧)
- 激活组件的挂载 mounted(新)
当路由更新时:触发 beforeRouteUpdate
注意: 但凡涉及到有next参数的钩子,必须调用next() 才能继续往下执行下一个钩子,否则路由跳转等会停止。
vueRouter 实现原理
vueRouter 实现的原理就是 监听浏览器中 url 的 hash值变化,并切换对应的组件