前言
Vue-router
作为单独开发SPA
的一个大模块,距离目前已经更新到了4.x
版本,那么本文就和大家一起来梳理一下基础知识
与用法
吧,最后我们也深入源码
来看一看它的实现。在这个问题之前我们应该来谈一谈为什么要设计Vue-router
这个东西。
设计理念
回到没有Router
的年代,我们的项目肯定是多个页面的,那么多页面跳转采用的肯定是跳链接
的解决方案,跳链接方案会带来这样的几个问题。
- 每访问到一个资源链接,我们会发起一次
http
请求,去服务器拿当前资源路径下的资源,传输链路
耗时。 http
请求资源回来之后,会根据W3C
规则进行解码,根据CRP
那一套规则进行页面渲染,我们这里把他称之为CRP
耗时,CRP
耗时没有办法避免,但是可以相对应减少
耗时。- 多次
访问
链接,虽然有静态资源缓存
帮助我们免去http
请求这一步,但是每一次的渲染都会引起重绘
,重排
,当然这是没有办法避免
的,一个链接目录下的资源是有限
的,所以我们采用了现代化框架
的组件化
开发,Router
的出现更加符合
规范。
基础
对于一个Vue
的SPA
项目,路由的使用通常会有几个步骤。
- 引入加载(我们就不介绍
CDN
引入了)
// 使用npm安装
npm install vue-router -D
// 安装4.x版本的Router
npm install vue-router@4 -D
- 入口文件使用(
v3.x
的用法与v4.x
的用法会有一点点差别)
// v3.x
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 创建router实例,传入routes配置
const router = new VueRouter({
routes // 配置的路由表
})
// 挂载
const app = new Vue({
router
}).$mount('#app')
// v4.x
// 创建
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(), // 路由模式,其他的使用createWebHistory
routes, // `routes: routes` 的缩写
})
// 挂载
const app = Vue.createApp({})
app.use(router)
app.mount('#app')
- 路由表的配置
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
// 定义一些路由表
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
注意:在Vue
项目中,我们可以使用$router
来访问路由实例,如果路由配置表中某个路由被激活,那么我们可以使用$route
来获取到这样的一个路由对象。当然在setup
函数里面,我们可以使用useRouter
、useRoute
来获取。
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
function pushWithQuery(query) {
router.push({
name: 'search',
query: {
...route.query,
},
})
}
},
}
基于上面这样子写,就可以在Vue
项目中使用Vue-router
了,这些都是最基本的配置,我们还必须要根据实际的业务进行一些配置。
动态路由
因为我们业务中会有这样的一个需求,展示商品 -> 根据商品展示商品详情页信息
。那么这里我们基于Vue-Router
的最基本的思路就是写一个页面组件
,通过传参
去展示。但是不同的产品我需要生成不同
的页面,所以这我们会使用到动态路由
。
// 定义路由组件
const ShowGoodsDetail = {
template: '<div>GoodsDetail</div>',
}
// v3.x
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/details/:id', component: ShowGoodsDetail }
]
})
// v4.x
const routes = [
// 动态字段以冒号开始
{ path: '/details/:id', component: ShowGoodsDetail }
]
=> details/42309881817812、details/42309881817813 都会映射到同一个url上,用一个路由组件
调试相关:我们可以用this.$route.params
,获取到动态id
传入的是什么,更多API参考。
特性:动态路由场景下如果是使用的相同的路由组件,那么组件会起到复用的效果,那么组件的生命周期函数不会触发,比如details/42309881817812 => details/42309881817813
,使用的是相同的组件,如果在组件生命周期钩子
中做了一些逻辑,那就不会走逻辑
,会报错
的,针对这样的一点我们具有两种解决方案。
watch
监听参数:表示监听路由参数,变更后会去做一些什么。beforeRouteUpdate
:表示在当前路由更新之前,会去做一些什么。
捕获404路由或者匹配所有路由
在v3.x
里面我们可以通过*
来匹配相关路由,比如。
const router = [
{ path:'*', component:ShowGoodsDetails}, // 匹配所有路由
{ path:'details-for-*', component:ShowGoodsDetails} // 匹配以`details-for-`开头的路由
]
// 获取
// 给出一个路由 { path: '/details-*' }
this.$router.push('/details-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'
在v3.x
里面的通配符*
下,会在$route.params
内部生成一个pathMatch
参数,这个参数是被匹配的那一部分,在v4.x
里面我们通过指定/:pathMatch(.*)*
来匹配
const routes = [
// 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
// 将匹配以 `/details-` 开头的所有内容,并将其放在 `$route.params.afterUser` 下
{ path: '/details-:afterUser(.*)', component: ShowGoodsDetails },
]
// 获取
this.$router.push({
name: 'NotFound',
// 保留当前路径并删除第一个字符,以避免目标 URL 以 `//` 开头。
params: { pathMatch: this.$route.path.substring(1).split('/') },
// 保留现有的查询和 hash 值,如果有的话
query: this.$route.query,
hash: this.$route.hash,
})
高级匹配:
{ path: '/:orderId(\d+)' }, // 自定义正则匹配
{ path: '/:chapters(\d+)+' }, // 匹配多个重复
{ path: '/users/:id', sensitive: true }, // 允许匹配大小写路径
{ path: '/users/:id', strict: true }, // 不允许匹配大小写路径
匹配优先级:如果一个路径能匹配多个路由,优先级按照路由配置表设置顺序。
子路由
子路由配置,由一级路由里面的children
字段提供,具体配置如下:
const routes = [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功
// UserHome 将被渲染到 User 的 <router-view> 内部
{ path: '', component: UserHome },
// ...其他子路由
],
},
]
命名规则:当一级路由具有子路由的时候,那么名称应该禅让
给子路由,一级路由不提倡书写name
字段。
编程式导航
编程式导航
中,我们可以借助实例
方法来帮助我们实现路由跳转
功能,<router-link>
的底层原理
就是创建a
标签来进行跳转的,我们前面说到可以使用$router来获取路由实例,所以我们可以这样操作:
push
const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user
replace
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: '/home' })
history
相关
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与 router.back() 相同
router.go(-1)
// 前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)
声明式与编程式的关系
<router-link :to="..."> => router.push(...)
<router-link :to="..." replace> = router.replace(...)
路由组件的使用(router-view用来解决一个视图需要多个组件渲染的情况)
v3.x
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
v4.x
<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
components: {
default: Home, // 注意,不写name的要带上default
// 它们与 `<router-view>` 上的 `name` 属性匹配
LeftSidebar,
RightSidebar,
},
},
],
})
重定向与别名
重定向
const routes = [{ path: '/home', redirect: '/' }]
const routes = [{ path: '/home', redirect: { name: 'homepage' } }]
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
// 如果直接return "xxxx",那么相当于是写死的 {path:"xxxx"}
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]
别名
const routes = [
{
path: '/users',
component: UsersLayout,
children: [
// 为这 3 个 URL 呈现 UserList
// - /users
// - /users/list
// - /people
{ path: '', component: UserList, alias: ['/people', 'list'] },
],
},
]
路由组件传参
路由组件我们有两种方式为其传参:
$route
:因为$route
是当前的一个路由对象
,里面我们可以通过params
,query
等字段为其传参。- 组件的
props
:在路由配置表里面我们可以设置props:true
,可以把component
的props
作为路由组件的默认参数,传递给路由组件。
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
},
// 函数传参
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
},
// 对象传参
{
path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]
})
关于路由的两种模式
hash
模式:hash
模式在v3.x
里面是默认的,在v4.x
里面需要手动开启。- 原理:
hash
模式是基于原生事件onhashchange
实现的,hash
变则加载不同的文件。 - 不利于SEO,因为此模式不跟服务器进行交互。
- 原理:
history
模式:history
模式在v3.x
里面是需要手动配置的,在v4.x
里面也是需要手动配置的。- 原理:
history
模式是基于浏览器事件pushState
,replaceState
操作History
历史记录栈实现的。
- 原理:
配置形式
// v3.x
const router = new VueRouter({
// 默认hash
// mode: 'history',
routes: [...]
})
// v4.x
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(), // history: createWebHistory() history模式
routes: [
//...
],
})
关于v3.x
与v4.x
中的history
模式,需要服务端做的支持
- 为什么要后端做支持?
- 因为我们知道我们做的是
SPA
,服务端上只有我们一个根目录地址,我们经过http
访问路径,我们会带回路径资源文件,如果走的是二级目录
,那么重新刷新,就会造成页面404
,因为服务器上并没有这样的路径。
- 因为我们知道我们做的是
- 解决办法
- 我们需要在
服务端
配置这样的需求,如果访问的路径下没有任何资源
,那么需要把这个路径
指向默认的index.html
页面。
- 我们需要在
- 基于原生
nodejs
解决方案。
const http = require('http')
const fs = require('fs')
const httpPort = 80
http.createServer((req, res) => {
fs.readFile('index.html', 'utf-8', (err, content) => {
if (err) {
console.log('We cannot open "index.html" file.')
}
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8'
})
res.end(content)
})
}).listen(httpPort, () => {
console.log('Server listening on: http://localhost:%s', httpPort)
})
- 基于
nginx
的解决方案
location / {
try_files $uri $uri/ /index.html;
}
路由守卫
- 全局前置路由守卫(能够处理用户进入页面之前的业务逻辑)
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// 如果用户未能验证身份,则 `next` 会被调用两次
next()
// return false
// return {name:'Login'}
})
- from:表示将要离开的页面。
- to:表示将要去的页面。
- false:表示取消当前导航。
- next:表示决定是否要继续进行当前导航操作。
- return {name:'Login'} :返回一个对象,表示重定向到Login页面。
- 全局后置路由守卫(能够处理用户离开页面之前的业务逻辑)
router.afterEach((to, from, failure) => {
if (!failure) sendToAnalytics(to.fullPath)
})
// 这里的failure表示路由导航失败,具体参考,这里不再多与述说
// https://router.vuejs.org/zh/guide/advanced/navigation-failures.html
- 路由独享的守卫(能够处理用户进入之前指定页面的逻辑)
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
独享守卫特性:和动态路由一样,如果只是在同一个路径下的query
、params
、hash
改变,那么这个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],
},
]
- 组件内的守卫(每一个组件应该有的钩子)
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
// 可以通过next回调来访问实例
next(vm => {
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
// 用于表单提交场景,刷新之前需要用户确认是否保存表单内容信息
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false) // 通过false来取消当前导航
}
},
}
扩展:如果是用的Vue3
的setup
函数,可以使用onBeforeRouteUpdate
、onBeforeRouteLeave
来达到beforeRouteUpdate
,beforeRouteLeave
的效果。
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
export default {
setup() {
// 与 beforeRouteLeave 相同,无法访问 `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// 取消导航并停留在同一页面上
if (!answer) return false
})
const userData = ref()
// 与 beforeRouteUpdate 相同, 但是setup函数中没有`this`,所以不能访问`this`
onBeforeRouteUpdate(async (to, from) => {
//仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
},
}
路由守卫的触发过程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
路由元信息meta
meta
字段,能够提供每一个路由的基本信息,在配置路由表的时候我们可以配置meta
字段,在实际的场景中我们一般可以用来做业务鉴权处理。
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// 可以利用meta字段来检测当前业务是否需要登录鉴权,如果没有登录,那就应该重定向到登录页面
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next(),不然会取消导航造成业务BUG
}
})
也可以通过TypeScript来扩展meta
字段,
关于路由中完成异步请求需求
在Vue
项目中提供了两种思路。
- 在
组件
中的生命周期
函数中获取数据。
export default {
data() {
return {
loading: false,
post: null,
error: null,
}
},
created() {
// watch 路由的参数,以便再次获取数据
this.$watch(
() => this.$route.params,
() => {
this.fetchData()
},
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
{ immediate: true }
)
},
methods: {
fetchData() {
this.error = this.post = null
this.loading = true
// replace `getPost` with your data fetching util / API wrapper
getPost(this.$route.params.id, (err, post) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
},
},
}
- 在
路由守卫
中获取数据。
export default {
data() {
return {
post: null,
error: null,
}
},
beforeRouteEnter(to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
async beforeRouteUpdate(to, from) {
this.post = null
try {
this.post = await getPost(to.params.id)
} catch (error) {
this.error = error.toString()
}
},
}
- 两种方式都可以
实现数据
的获取,在页面维度
、组件维度
,钩子函数
能够提供在流程中的一种劫持功能,区别是在页面导航完成前后的区别。
路由懒加载
- 基于
异步组件
实现懒加载。
const Foo = () =>
Promise.resolve({
/* 组件定义对象 */
template: '<div>I am async!</div>'
})
- 基于三方工具
webpack
、vite
实现懒加载。webpack
实现
const Foo = () => import('./Foo.vue') const router = new VueRouter({ routes: [{ path: '/foo', component: Foo }] }) // 基于webpack的代码分割功能,我们给每一个组件取一个webpackChunkName, 这样可以使同一个页面下的组件,打包进一个chunk // /* webpackChunkName: "group-foo" */ 魔法注释,给模块生成别名 const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue') const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
vite
实现(关于vite,我正在写vite的进阶使用,一起来学习一下吧,->> 戳)
// vite.config.js export default defineConfig({ build: { rollupOptions: { // https://rollupjs.org/guide/en/#outputmanualchunks output: { manualChunks: { 'group-user': [ './src/UserDetails', './src/UserDashboard', './src/UserProfileEdit', ], }, }, }, })
动态路由相关补充
- 业务背景:针对现有的业务,对其路由配置进行新增、删除、查询,以完成业务。
- 解决方案:
Vue-router
提供了addRoute
、removeRoute
、hasRoute
、getRoutes
来帮助我们解决问题。
router.addRoute({ path: '/about', name: 'about', component: About }) // 添加路由
router.removeRoute('about') // 移除路由
router.addRoute('admin', { path: 'settings', component: AdminSettings }) // 添加嵌套路由
router.hasRoute():检查路由是否存在。
router.getRoutes():获取一个包含所有路由记录的数组。
从vue2到vue3的迁移:router.vuejs.org/zh/guide/mi…
源码解读
源码解读分别解析vue3.x
与4.x
的部分源码,因为两个版本中的大部分API
是没有动过的,但是不同的是3.x
的版本采用的是new VueRouter
的方式,而4.x
采用的是createRouter
的方式。
3.x
的方式
import VueRouter from 'vue-router';
Vue.use(VueRouter)
const router = new VueRouter(...);
所以3.x
中,具有VueRouter
的一个类,类上有一个静态方法install
。
export declare class VueRouter {
constructor (options?: RouterOptions); // 当new VueRouter的时候,会被立即调用
app: Vue; // vm实例
mode: RouterMode; // "hash" | "history" | "abstract";
currentRoute: Route; // 当前路由对象
// 路由守卫
beforeEach (guard: NavigationGuard): Function;
beforeResolve (guard: NavigationGuard): Function;
afterEach (hook: (to: Route, from: Route) => any): Function;
// 方法
push (location: RawLocation, onComplete?: Function, onAbort?: Function): void;
replace (location: RawLocation, onComplete?: Function, onAbort?: Function): void;
go (n: number): void;
back (): void;
forward (): void;
getMatchedComponents (to?: RawLocation | Route): Component[];
onReady (cb: Function, errorCb?: Function): void;
onError (cb: Function): void;
addRoutes (routes: RouteConfig[]): void;
resolve (to: RawLocation, current?: Route, append?: boolean): {
location: Location;
route: Route;
href: string;
// backwards compat
normalizedTo: Location;
resolved: Route;
};
// 静态方法install,本身是一个插件,用于安装安装自己
static install: PluginFunction<never>;
}
既然这里说到了install
方法,那我们就一起来看看install
方法的实现,以便于以后自己写Vue
插件。
install
export let _Vue
export function install (Vue) {
// install 传入整个Vue实例
// 刚开始进来installed与_Vue都是undefined
// 这里也就做了检验,如果已经安装过了并且传入的是Vue实例,就return
if (install.installed && _Vue === Vue) return
install.installed = true // 避免二次刷新调用install方法
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 把插件混入到Vue实例中去
Vue.mixin({
// 在生命周期beforeCreate注册
beforeCreate () {
// 如果存在$options.router,遍绑定this、router并且执行init方法
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// 变成响应式数据
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
...
}
// 注册实例
registerInstance(this, this)
},
// 在生命周期destroyed销毁
destroyed () {
registerInstance(this)
}
})
// 当访问原型上的router的时候,返回整个router实例数组
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 当访问原型上的route的时候,返回当前路由对象
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// Vue注册组件<router-link>、<router-view>
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 处理组件内路由守卫
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave =
strats.beforeRouteUpdate = strats.created
}
所以VueRouter
实现了install
方法,在install
方法中把路由混入到生命周期钩子
中,并调用init
方法,处理部分逻辑,如果对混入不了解的同学,可以看看vue的mixin混入。
4.x
的方式
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(), // 路由模式,其他的使用createWebHistory
routes, // `routes: routes` 的缩写
})
createRouter
createRouter
做了一些配置路由表的合并、路由模式的处理等最后返回了一个带有install
等方法的router
,这里的install
方法跟3.x
版本的方法做的的功能差不多,只不过这里的router
是被挂在了vm
的globalProperties
上。
createRouterMatcher
执行完后,会返回的5个函数{ addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
- 创建全局路由守卫,
beforeGuards
,beforeResolveGuards
,afterGuards
,通过守卫.add
方法暴露给路由钩子。 - 创建一些普通方法,比如
push
、replace
。 - 通过
router
暴露出去,给createRouter
使用。createRouter
大致的流程如下:
上面就是v3.x
与v4.x
的创建Router
的一个区别,另外我们还可以看到关于mode
到history
的变化,那么关于v4.x
里面的三种history
。
"history"
=>createWebHistory()
export function createWebHistory(base?: string): RouterHistory {
base = normalizeBase(base)
// 创建vue-router的history对象
const historyNavigation = useHistoryStateNavigation(base)
// 创建vue-router的historyListeners侦听器
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace
)
// 实现go方法
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
// 合并routerHistory对象
const routerHistory: RouterHistory = assign(
{
// it's overridden right after
location: '',
base,
go,
createHref: createHref.bind(null, base),
},
historyNavigation,
historyListeners
)
// 劫持location属性,访问的时候返回value
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
// 劫持state属性,访问的时候返回value
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
// 返回routerHistory对象
return routerHistory // => 如果是createRouter({history:createWebHistory,routes})
}
创建的historyNavigation
包含了基于h5
的history
对象的pushState
、replaceState
方法、state
对象重新封装的push
、replace
方法和state
对象,基于window.location
处理的location
,具体长这样。
const historyNavigation = {
location, // window.location
state, // history.state
push, // => function push (...){changeLocation(...);changeLocation(...)} => history.pushState
replace // => function replace (...){changeLocation(...)} => history.replaceState
}
changeLocation
那么这几种自定义的属性,方法都是经过changeLocation
特殊处理的,我们来看一看这个方法。
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
// 是否是hash模式?
const hashIndex = base.indexOf('#')
const url =
// 是hash => createBaseLocation() + base + to
// 不是hash => base.slice(hashIndex)) + to
hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
// createBaseLocation => location.protocol + '//' + location.host
: createBaseLocation() + base + to
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state
} catch (err) {
if (__DEV__) {
warn('Error with push/replace State', err)
} else {
console.error(err)
}
// Force the navigation, this also resets the call count
location[replace ? 'replace' : 'assign'](url)
}
}
根据base
根路径计算最终的跳转url
,然后根据replace
标记决定使用history.pushState
或 history.replaceState
进行跳转。如果是hash
的话则会调用createBaseLocation
生成url
也进行history.pushState
或 history.replaceState
跳转。
useHistoryListeners
经过上面的分析,知道了怎么样跳转了,但是在函数里面,还创建了路由监听器,因为h5中,history.go
、history.forward
、history.back
会触发原生popState
事件,所以这里也对此做了处理。
function useHistoryListeners(
base: string,
historyState: ValueContainer<StateEntry>,
currentLocation: ValueContainer<HistoryLocation>,
replace: RouterHistory['replace']
) {
// 监听器事件池
let listeners: NavigationCallback[] = []
let teardowns: Array<() => void> = []
// 保存取消时候的状态
let pauseState: HistoryLocation | null = null
//
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
const to = createCurrentLocation(base, location) // 新地址
const from: HistoryLocation = currentLocation.value // 当前地址
const fromState: StateEntry = historyState.value // 当前路由state
let delta = 0 // 计步器,用来记录一次操作,to与from的次数
if (state) {
currentLocation.value = to // to路由state不为空时,更新currentLocation和historyState缓存
historyState.value = state
// 暂停监听,重置pauseState
if (pauseState && pauseState === from) {
pauseState = null
return
}
delta = fromState ? state.position - fromState.position : 0
} else {
// 为空,直接跳
replace(to)
}
// console.log({ deltaFromCurrent })
// Here we could also revert the navigation by calling history.go(-delta)
// this listener will have to be adapted to not trigger again and to wait for the url
// to be updated before triggering the listeners. Some kind of validation function would also
// need to be passed to the listeners so the navigation can be accepted
// call all listeners
// 给所有订阅者注册跳转事件回调
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta, // 步数
type: NavigationType.pop, // 类型
direction: delta // 跳转距离
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
// 停止监听
function pauseListeners() {
pauseState = currentLocation.value
}
// 注册监听回调,发布订阅
function listen(callback: NavigationCallback) {
// set up the listener and prepare teardown callbacks
listeners.push(callback)
// 执行回调
const teardown = () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1)
}
teardowns.push(teardown)
return teardown
}
// 离开、关闭页面监听器,与原生事件beforeunload绑定
function beforeUnloadListener() {
const { history } = window
if (!history.state) return
history.replaceState(
assign({}, history.state, { scroll: computeScrollPosition() }),
''
)
}
// 清空监听器,卸载监听器
function destroy() {
for (const teardown of teardowns) teardown()
teardowns = []
window.removeEventListener('popstate', popStateHandler)
window.removeEventListener('beforeunload', beforeUnloadListener)
}
// set up the listeners and prepare teardown callbacks
// 绑定popstate -> popStateHandler, beforeunload -> beforeUnloadListener
// 页面关闭触发beforeunload
window.addEventListener('popstate', popStateHandler)
window.addEventListener('beforeunload', beforeUnloadListener)
// 暴露出三个方法
return {
pauseListeners, // 停止监听
listen, // 注册监听回调
destroy, // 卸载监听器
}
}
关于路由监听器popStateHandler
会做两件事。
- 更新
history
的location
、state
。 - 将数据挂起,通知所有订阅者注册回调。
关于路由监听器beforeUnloadListener
主要是记录页面离开之前所在的位置。
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta) // 跳转
}
const routerHistory: RouterHistory = assign(
{
location: '',
base,
go,
createHref: createHref.bind(null, base),
},
historyNavigation,
historyListeners
)
// 劫持location
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
// 劫持state
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
// 暴露routerHistory
return routerHistory
"hash"
=>createWebHashHistory()
export function createWebHashHistory(base?: string): RouterHistory {
// Make sure this implementation is fine in terms of encoding, specially for IE11
// for `file://`, directly use the pathname and ignore the base
// location.pathname contains an initial `/` even at the root: `https://example.com`
base = location.host ? base || location.pathname + location.search : ''
// allow the user to provide a `#` in the middle: `/base/#/app`
// 如果您使用createWebHashhistory,那么自动在base上加一个'#'
// 在createWebHistory中,hash模式会调用createBaseLocation => url = location.protocol +
// '//' + location.host + base + to
if (!base.includes('#')) base += '#'
// https://www.baidu.com/#
if (__DEV__ && !base.endsWith('#/') && !base.endsWith('#')) {
warn(
`A hash base must end with a "#":\n"${base}" should be "${base.replace(
/#.*$/,
'#'
)}".`
)
}
return createWebHistory(base) // => 如果是createRouter({history:createWebHashHistory,routes})
}
"abstract"
=>createMemoryHistory()
另外我们看到router-link
与router-view
,也看看他们的具体实现吧,这里会调用Vue.Component
方法来注册一个组件标签。
- 基础用法:
Vue.component('router-link',{...})
或app.component('router-link',{...})
=><router-link></router-link>
。 - 原理: 戳 >>> Vue源码解析系列(十四) -- Vue.use与Vue.compoent的原理解读。
router-link
所以根据app.component
或Vue.component
的原理来说,都是接受两个参数,那么在Vue.component
中采用的是Vue.extend
来返回组件的,在app.component
中利用的是context.component
来返回组件的。
- 3.x
Vue.component('RouterLink', Link);
var Link = {
name: 'RouterLink', // 标签名
props: {
to: {
type: toTypes,
required: true
}, // 属性
tag: {
type: String,
default: 'a'
}, // 标签类型
custom: Boolean, // 默认为false,用于把<router-link><router-link>中间内容用a标签包裹起来
exact: Boolean, // 精准匹配
exactPath: Boolean,
append: Boolean, // /a -> /b => /a/b
replace: Boolean, // 替换路径,不会留下导航记录
activeClass: String, // 激活的导航类名
exactActiveClass: String, // 精准匹配时候激活的类类名
ariaCurrentValue: {
type: String,
default: 'page'
},
event: {
type: eventTypes,
default: 'click'
} // 事件
},
// h => $createElement
render (h) {
const router = this.$router; // 实例对象
const current = this.$route; // 当前路由对象
const { location, route, href } = router.resolve(
this.to,
current,
this.append
);
// 处理跳转函数
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop);
} else {
router.push(location, noop);
}
}
};
...
// 绑定事件
const on = { click: guardEvent };
if (Array.isArray(this.event)) {
this.event.forEach(e => {
on[e] = handler;
});
} else {
on[this.event] = handler;
}
const data = { class: classes };
...
// 创建虚拟节点
return h(this.tag, data, this.$slots.default)
}
};
router-view
- v3.x
var View = {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
...
const h = parent.$createElement;
const name = props.name; // 组件名
const route = parent.$route; // 当前路由对象
const cache = parent._routerViewCache || (parent._routerViewCache = {}); // 缓存路由组件
...
const component = matched && matched.components[name];
...
// 通过$createElement创建虚拟节点
return h(component, data, children)
}
};
keep-alive
export default {
name: 'keep-alive', // 组件名,存在于Vue源码中,而不是在VueRouter源码中
abstract: true,
// 属性
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
methods: {
cacheVNode() {
// 调用cacheVNode
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
// cache对缓存组件的映射
cache[keyToCache] = {
name: _getComponentName(componentOptions),
tag,
componentInstance
}
keys.push(keyToCache)
// prune oldest entry
// 如果缓存中的组件个数,超过了限制
if (this.max && keys.length > parseInt(this.max)) {
// 则从缓存中删除第一个,LRU算法。
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
// created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。
created() {
this.cache = Object.create(null)
this.keys = []
},
// destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// 组件挂载的时候会触发cacheVNode,并且监听include、exclude
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated() {
// 触发updated,调用cacheVNode
this.cacheVNode()
},
render() {
// 获取插槽里面的内容,以第一个为标准
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
// 获取componentOptions
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
// 获取组件的name,如果有name就返回name,没有就返回tag
const name = _getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
// 如果与include匹配上了,或者与exclude不匹配,表示不缓存
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
// 直接返回vnode
return vnode
}
const { cache, keys } = this
// 取到组件名,用来处理缓存命中问题
const key =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// 如果命中缓存,则从缓存中拿到组件实例
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key) // 从cache中删除当前key
keys.push(key) // 再加入到第一位
} else {
// 如果没有命中,将其设置进cache
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
// 设置组件为keepAlive
vnode.data.keepAlive = true
}
// 返回vnode => 被缓存的组件渲染不会再次触发created,mounted,会触发updated
return vnode || (slot && slot[0])
}
}
所以keep-alive
的底层实现,就是依赖于生命周期钩子函数,在created
中创建了cache
容器,在mounted
和updated
中根据include
和exclude
匹配规则对容器进行数据调动,数据调用时基于LRU
算法的。
总结
这一章我们讲解了VueRouter
的基本用法,install
方法,RouterLink
、RouterView
、KeepAlive
的实现原理等,下一章我们将会去探索更多的功能。