vue-router 功能
嵌套路由
指在一个路由配置中嵌套另一个路由配置,从而允许在单页面应用中创建复杂的页面结构和组件组合。这种嵌套关系允许在父路由组件内部通过子路由来访问和展示子组件。
例如在页面点击不同的选项卡切换不同的路由来展示不同的内容时。
动态路由参数
Vue Router 支持动态路由参数,如 /user/:id,其中 :id 是一个动态参数。
通过 $route.params 可以访问这些动态参数的值。
路由传递数据
Vue Router 支持通过路由参数、查询参数和通配符来传递数据。
供路由守卫
提供路由守卫,允许你在导航过程中执行自定义逻辑来拦截导航并改变其行为。
HTML5 history 和 hash 模式
Vue Router 支持两种 URL 模式:HTML5 history 模式和 hash 模式。
HTML5 history 模式使用浏览器的历史记录 API 来管理 URL,而 hash 模式则使用 URL 的 hash 部分来模拟页面跳转。
在支持 HTML5 history API 的现代浏览器中,推荐使用 HTML5 history 模式以获得更好的 URL 体验。
URL 的正确编码
Vue Router 会自动对 URL 进行正确的编码和解码,以确保 URL 的有效性和可读性。
当你需要传递特殊字符或 URL 片段时,Vue Router 会确保这些字符被正确地处理。
过渡效果
可以使用 Vue 的 <transition> 组件来定义过渡效果。
自动激活 CSS 类的链接
Vue Router 会自动为当前激活的链接添加一个 CSS 类名,这通常用于高亮当前激活的导航链接。
你可以通过配置 linkActiveClass 选项来自定义这个类名。
<!--方法一-->
<router-link to='/' active-class="active" >首页</router-link>
<!--方法二-->
<script>
const router = new VueRouter({
routes,
linkActiveClass: 'active'
});
</script>
可定制的滚动行为
Vue Router 允许你定制页面跳转时的滚动行为。
你可以通过配置 scrollBehavior 函数来定义滚动行为。
例如切换到新路由时,页面要滚动到顶部或保持原先的滚动位置。
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { top: 0 }
},
})
vue-router 跳转方式
声明式导航
通过内置组件 router-link 跳转
<router-link :to="/home"></router-link>
编程式导航
通过调用 router 实例的方法跳转
// 使用 push 方法跳转
this.$router.push({
name: 'name',
params: {...}
});
this.$router.push({path: '/path'});
this.$router.push('/path');
// 使用 repalce 方法跳转
this.$router.replace({
path: '/path',
params: {...} // 或 query
});
vue-router 组件
<router-link>
-
功能:用于导航,即渲染一个链接,当点击时,导航到由 to 属性指定的 URL。
-
示例:
<router-link to="/home">Home</router-link> -
它会渲染为一个
<a>标签,但如果你希望它渲染为其他标签,你可以使用 tag 属性,如<router-link to="/home" tag="li">Home</router-link>。
<router-link> 的一些属性:
-
to
必填,标识目标路由的链接,当被点击后,内部会立刻把 to 的值传到 router.push(),所以这个值可以是一个字符串或者描述目标路由的对象 -
repalce
默认值 false,若设置的话,当点击时,会调用 router.replace() 而不是 router.push() -
append
设置 append 属性后,则在当前 (相对) 路径前添加基路径。例如,我们从 /a 导航到一个相对路径 b,如果没有配置 append,则路径为 /b,如果配了,则为 /a/b -
tag
让<router-link>渲染成 tag 设置的标签,如 tag="li",就会渲染成<li>跳转</li> -
active-class
默认值为 router-link-acitve,设置链接激活时使用的 CSS 类名,默认值可以通过路由的构造选项 linkActiveClass 来全局配置 -
exact-active-class
默认值为 router-link-exact-active,设置链接被精确匹配的时候应该激活的 class,默认值可以通过路由构造函数选项 linkExactActiveClass 进行全局配置 -
exact
是否精确匹配,默认为 false -
event
声明可以用来触发导航的事件,可以是一个字符串或是一个包含字符串的数组,默认是 click
<router-view>
-
功能:组件的出口,即路由匹配到的组件将渲染在这里。
-
它可以理解为是一个占位符,Vue Router 会根据当前路由规则动态地替换显示相应的组件。
-
通常放在 App 组件的模板中,作为整个应用的布局中心。
vue-router 两种模式
Hash 模式
URL 形式:
Hash 模式下的 URL 包含一个 # 符号,后面跟着路由信息。例如:example.com/#/home,其中 /home 是路由路径,位于 # 后面。
原理:
Hash 模式主要利用了浏览器对 URL 中 # 符号的特殊处理。在浏览器中,# 后面的内容被视为锚点(anchor),浏览器会尝试滚动到与锚点 ID 相匹配的元素位置。但是,在 Vue Router 中,这个行为被拦截并用于路由的匹配。具体来说,当 URL 的 hash 值发生变化时(无论是通过用户点击链接还是通过 JavaScript 修改),浏览器不会向服务器发送请求,而是会触发一个 hashchange 事件。Vue Router 监听这个事件,解析新的 hash 值,并根据路由配置找到对应的组件进行渲染。
特点:
-
兼容性好:由于 # 是 URL 的一部分,而且被所有现代浏览器原生支持,因此 hash 模式在所有现代浏览器中都能正常工作,并且对于旧版浏览器也具有良好的兼容性。
-
部署简单:hash 模式不需要服务器进行任何特殊的配置,因为 # 后面的内容不会被发送到服务器。
-
SEO 性能较差:搜索引擎在抓取网页时通常会忽略 # 后面的内容,因此 hash 模式下的页面在 SEO(搜索引擎优化)方面可能不如 history 模式。
-
URL 美观性差:URL 中包含 # 符号,可能会给用户带来不美观的感觉,尤其是对于不熟悉单页面应用(SPA)的用户来说。
总结
Hash 模式下当 URL 的 hash 值发生变化时(包括声明式导航、编程式导航、以及手动在地址栏改变 hash 值并按下回车)会触发浏览器的 hashchange 事件,Vue Router 监听这个事件,根据 hash 值找到对应的路由组件进行渲染,在这个过程中浏览器不会向服务器发起请求。
History 模式
URL 形式:
History 模式下的 URL 看起来更加整洁,没有 # 符号,呈现常规的路径结构。例如:example.com/home。
原理:
History 模式利用了 HTML5 的 History API 来实现 URL 的跳转和状态的管理。这个 API 包括 history.pushState() 和 history.replaceState() 方法,它们允许开发者在浏览器的历史记录栈中添加或替换记录,同时更新当前 URL。
Vue Router 在 History 模式下,当用户点击一个路由链接或者通过编程方式调用 router.push('/new-path') 时,Vue Router 会使用 pushState() 或 replaceState() 方法更新浏览器的历史记录和当前 URL。
同时,Vue Router 还会监听 popstate 事件来捕获浏览器的前进后退操作,并根据新的 URL 计算出对应的路由,进而动态加载对应的组件。
特点:
-
URL 美观性好:没有 # 符号,URL 看起来更加整洁和直观,符合用户对传统网站链接的预期。
-
用户体验更好:由于 URL 更加接近传统的网站链接,用户可能会感觉更加自然和舒适。
-
SEO 性能较好:搜索引擎可以更容易地抓取和索引 history 模式下的页面,因为 URL 更加直观和语义化。
-
需要服务器配置:为了支持 History 模式,服务器需要进行相应的配置,以确保在直接访问 URL 时能够返回应用的入口页面(通常是 index.html),以便 Vue Router 能够接管并解析正确的路由。
-
兼容性问题:虽然大多数现代浏览器都支持 HTML5 History API,但在一些旧版浏览器中可能不被支持,需要特别注意。
History 模式 url 改变时 浏览器会发起 http 请求去服务器获取新内容吗?
在 Vue Router 的 History 模式下,当 URL 改变时,浏览器通常不会直接发起 HTTP 请求去服务器获取新内容。这是因为 History 模式主要利用了 HTML5 的 History API(如 history.pushState() 和 history.replaceState() 方法)来更改浏览器的 URL,而不会导致页面重新加载。这些 API 允许开发者在浏览器的历史记录中添加或替换记录,同时更新地址栏中的 URL,而不会触发页面刷新或向服务器发送请求。
Vue Router 在 History 模式下,会监听 URL 的变化(通常是通过 Vue Router 自身的导航逻辑或监听浏览器的 popstate 事件来实现),并根据新的 URL 匹配路由表,然后动态地渲染对应的组件。这个过程中,并没有涉及到向服务器发送 HTTP 请求以获取新内容。
然而,需要注意的是,虽然 Vue Router 本身不会因 URL 改变而发起 HTTP 请求,但如果路由对应的组件中有需要从服务器获取数据的逻辑(例如,在组件的 created、mounted 钩子中调用 API 获取数据),那么这些请求会在组件被渲染时触发。但这些请求是由组件内部的逻辑控制的,与 Vue Router 的 History 模式或 URL 改变本身没有直接关系。
此外,还需要考虑服务器配置的问题。在 History 模式下,由于 URL 不包含 # 符号,直接访问这些 URL 时需要服务器能够正确地处理这些请求。通常,这需要在服务器上配置一个重定向规则,将所有请求都重定向到应用的入口文件(如 index.html),以便 Vue Router 能够接管并解析正确的路由。如果服务器配置不正确,直接访问非根路径的 URL 可能会导致 404 错误。
总结
History 模式下,当使用声明式导航或编程式导航时,Vue Router 会调用浏览器的 History API【 history.pushState() 和 history.replaceState() 】来改变浏览器的 URL 和历史记录,并根据 URL 找到对应的路由组件进行渲染,在这个过程中浏览器不会向服务器发起请求。
但是手动在地址栏改变 url 值并按下回车则会向服务器发起请求,这和 hash 模式有所不同。
注意该一级标题(vue-router 两种模式)中所说的服务器指 Web 服务器,如 Nginx、Apach 等,而不是后端服务器,如 Node.js、Java、PHP 等应用服务器
vue-router 懒加载
Vue 路由懒加载是指在用户访问某个路由时,才动态加载该路由对应的组件代码,而不是在应用程序初始化时就加载所有组件。这种按需加载的方式可以有效提高页面加载速度和应用程序性能。
懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载。
未用懒加载:
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
routes: [
{
path:'/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
webpack 动态导入实现懒加载
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: resolve=>require(["@/components/HelloWorld"] , resolve)
}
]
})
ES6 的 import 方法
最常用
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: ()=>import("@/components/HelloWorld")
}
]
})
vue-router 中的导航钩子
作用:拦截导航,完成或取消路由跳转。
有三种方式可以植入路由导航过程中:
-
全局守卫:全局的
-
路由独享守卫:单个路由独享的
-
组件内守卫:组件级的
全局守卫
全局前置守卫:beforeEach
在路由跳转前触发
router.beforeEach((to, from, next) => {
// 必须调用next
})
全局解析守卫:beforeResolve
在路由确认之前,组件已经解析,但组件还未被挂载或激活时。很少用到。
router.beforeResolve((to, from, next) => {
// 必须调用next
})
全局后置钩子:afterEach
在路由跳转完成后触发
router.afterEach((to, from) => {
// 没有next
})
路由独享守卫:beforeEnter
beforeEnter() 单个路由独享的导航钩子,它是在路由配置上直接进行定义的。
const router = new VueRouter({
routes: [
{
path: '/home',
beforeEnter: (to, from, next) => {
//...
}
}
]
})
组件内守卫
beforeRouteEnter
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
next(vm => {
// 通过 `vm` 访问组件实例
})
}
beforeRouteEnter 守卫不能访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
注意:只有 beforeRouteEnter 支持给 next 传递回调。
beforeRouteUpdate
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
}
beforeRouteLeave
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
导航守卫三个参数的含义
-
to:即将要进入的目标路由对象
-
from:当前导航正要离开的路由对象
-
next:一定要调用该方法来 resolve 这个钩子,不然路由跳转不过去
- next():进入下一个路由
- next(false):中断当前的导航
- next('/') 或 next({path: '/'}):当前导航被中断,进行新的一个导航
项目中常常能看到next('/login') 、next(to)或者next({ ...to, replace: true })这又是啥意思呢?
next('/login')不是说直接去/login路由,而是中断这一次路由守卫的操作,又进入一次路由守卫,就像嵌套一样,一层路由守卫,然后又是一层路由守卫 ... 直到遇到next()时才执行放行操作。
很多人在使用动态添加路由 addRoutes() 会遇到下面的情况:
在addRoutes()之后第一次访问被添加的路由会白屏,这是因为刚刚addRoutes()就立刻访问被添加的路由,然而此时addRoutes()没有执行结束,因而找不到刚刚被添加的路由导致白屏。因此需要从新访问一次路由才行。
该如何解决这个问题 ?
此时就要使用next({ ...to, replace: true })来确保addRoutes()时动态添加的路由已经被完全加载上去。
next({ ...to, replace: true })中的replace: true只是一个设置信息,告诉VUE本次操作后,不能通过浏览器后退按钮,返回前一个路由。
因此next({ ...to, replace: true })可以写成next({ ...to }),不过你应该不希望用户在addRoutes()还没有完成的时候,可以点击浏览器回退按钮搞事情吧。
其实next({ ...to })的执行很简单,它会判断:
如果参数to不能找到对应的路由的话,就再执行一次beforeEach((to, from, next)直到其中的next({ ...to})能找到对应的路由为止。
也就是说此时addRoutes()已经完成啦,找到对应的路由之后,接下来将执行前往对应路由的beforeEach((to, from, next) ,因此需要用代码来判断这一次是否就是前往对应路由的beforeEach((to, from, next),如果是,就执行next()放行。
如果守卫中没有正确的放行出口的话,会一直next({ ...to})进入死循环 !!!
因此你还需要确保在当addRoutes()已经完成时,所执行到的这一次beforeEach((to, from, next)中有一个正确的next()方向出口。
完整的导航解析流程
1、导航被触发 2、在失活的组件里调用离开守卫 3、调用全局的 beforeEach 守卫 4、在重用的组件里调用 beforeRouteUpdate 守卫 5、在路由配置里调用 beforEnter 6、解析异步路由组件 7、在被激活的组件里调用 beforeRouteEnter 8、调用全局的 beforeResolve 守卫 9、导航被确认 10、调用全局的 afterEach 钩子 11、触发 DOM 更新 12、在创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数
route 和 router 的区别
-
this.$route 是当前路由信息对象,包括 path、params、hash、query、fullPath、matched、name 等路由信息参数
-
this.$router 是路由实例对象,包括了路由的跳转方式 push()、go(),钩子函数等
query 和 params 传参区别
-
query 刷新页面参数不会消失,params 传参页面参数会消失,可以考虑本地存储解决
-
query 传参会显示在 url 地址上,params 传参不会显示地址上
this.$router.push({
path: '/test',
query: { id: 123 }
});
/*
url 格式:http://xxx/test?id=123
模板内获取数据:this.$route.query.id
*/
this.$router.push({
name: 'test',
params: { id: 123 }
});
/*
url 格式:http://xxx/test
模板内获取数据:this.$route.params.id
*/
this.$router.push({
path: '/path/${id}'
});
/*
url 格式:http://xxx/test/123
模板内获取数据:this.$route.params.id
组件路由:/test:id
*/
注意:
当使用 this.$router.push() 或 this.$router.replace() 时,params 不能与 path 直接一起使用来传递参数,因为 params 是与命名路由关联的。
query 可以与 path 一起使用,这些参数会被添加到 URL 的查询字符串中。
如何监听路由参数的变化
有两种方法可以监听路由参数的变化,但是只能用在包含 <router-view/> 的组件内
-
watch 监听 $route 对象
watch: { $route(to, from) { console.log(to, from) } } -
调用组件内的守卫 beforeRouteUpdate
beforeRouteUpdate(to, from, next) { console.log(to, from) next() }
addRoutes()
是一个用于动态添加路由到已存在的路由表的方法。这在一些特定的场景中非常有用,比如当你需要根据用户的权限动态生成路由,或者在应用运行时根据某些条件添加新的路由。
addRoutes 方法接收一个路由配置数组作为参数,这些路由配置与你在 Vue Router 中通常定义的路由配置相同。你可以直接传入一个静态的路由配置数组,或者根据条件动态生成它。
import NewComponent from '@/components/NewComponent'
const newRoutes = [
{ path: '/new-path', component: NewComponent },
{ pathL '/test', component: resolve => require([`@/${Test}.vue`], resolve)}
// ... 其他路由配置
]
router.addRoutes(newRoutes)
虽然 addRoutes 本身并不直接支持异步加载路由,但你可以结合路由懒加载的方法来实现异步加载路由的效果。
完整路由代码示例
import Vue from 'vue'
import Router from 'vue-router'
import store from '@/store/index'
import { getToken, removeToken } from "@/utils/index";
Vue.use(Router)
export const routes = [
{
path: '/login',
component: () => import("@/views/login/index"),
hidden: true
},
{
path: '/404',
component: () => import("@/views/error-page/404"),
hidden: true
},{
path: '/tableTest',
component: () => import("@/components/TableMain/test"),
hidden: true
}
]
/**
* 替换初始方法
export default new Router({
mode: 'history', // 使 url 不带 # 号
routes
})
*/
const createRouter = () => new Router({
mode: "history",
scrollBehavior: () => ({ y: 0 }),
routes
})
const router = createRouter()
export default router
/*
解决面包屑点击报错
Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation
重写 Router 上的 push 方法,对其使用 catch 捕捉异常
*/
const VueRouterPush = Router.prototype.push
Router.prototype.push = function push(to) {
return VueRouterPush.call(this, to).catch(err => err)
}
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher
}
const whiteList = ['/login']
router.beforeEach(async (to, from, next) => {
console.log("to:",to)
console.log("from:",from)
const token = getToken()
if (token) {
if (to.path === '/login') {
next({ path: '/' })
} else {
const routers = store.state.routers
if (routers && routers.length) {
// 需要添加到tags中的内容
const tag = {
name: to.name, url: to.path, meta: to.meta
}
store.commit("addTags", tag)
next()
} else {
try {
await store.dispatch("getRoutersByToken", token);
next({
...to,
replace: true
})
} catch (error) {
resetRouter();
removeToken()
store.commit('removeRouters')
next('/login')
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next("/login")
}
}
})