): Matcher {
// 创建映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 添加动态路由
function addRoutes(routes){…}
// 计算新路径
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {…}
// … 后面的一些方法暂不展开
return {
match,
addRoutes
}
}
createMatcher
接受俩参数,分别是 routes
,这个就是我们平时在 router.js
定义的路由表配置,然后还有一个参数是 router
他是 new vueRouter
返回的实例。
createRouteMap
下面这句代码是在创建一张 path-record
,name-record
的映射表,我们将代码定位到 create-route-map.js
源码地址 (https://github.com/vuejs/vue-router/blob/dev/src/create-route-map.js)
export function createRouteMap (
routes: Array,
oldPathList?: Array,
oldPathMap?: Dictionary,
oldNameMap?: Dictionary
): {
pathList: Array,
pathMap: Dictionary,
nameMap: Dictionary
} {
// 记录所有的 path
const pathList: Array = oldPathList || []
// 记录 path-RouteRecord 的 Map
const pathMap: Dictionary = oldPathMap || Object.create(null)
// 记录 name-RouteRecord 的 Map
const nameMap: Dictionary = oldNameMap || Object.create(null)
// 遍历所有的 route 生成对应映射表
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// 调整优先级
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === ‘*’) {
pathList.push(pathList.splice(i, 1)[0])
l–
i–
}
}
return {
pathList,
pathMap,
nameMap
}
}
createRouteMap
需要传入路由配置,支持传入旧路径数组和旧的 Map
这一步是为后面递归和 addRoutes
做好准备。首先用三个变量记录 path
,pathMap
,nameMap
,接着我们来看 addRouteRecord
这个核心方法。这一块代码太多了,列举几个重要的步骤
// 解析路径
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
// 拼接路径
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 记录路由信息的关键对象,后续会依此建立映射表
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// route 对应的组件
components: route.components || { default: route.component },
// 组件实例
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
使用 recod
对象 记录路由配置有利于后续路径切换时计算出新路径,这里的 path
其实是通过传入父级 record
对象的path
和当前 path
拼接出来的 。然后 regex
使用一个库将 path
解析为正则表达式。如果 route
有子节点就递归调用 addRouteRecord
// 如果有 children 递归调用 addRouteRecord
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(${matchAs}/${child.path}
)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
最后映射两张表,并将 record·path
保存进 pathList
,nameMap
逻辑相似就不列举了
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
废了这么大劲将 pathList
和 pathMap
和 nameMap
抽出来是为啥呢? 首先 pathList
是记录路由配置所有的 path
,然后 pathMap
和 nameMap
方便我们传入 path
或者 name
快速定位到一个 record
,然后辅助后续路径切换计算路由的。
addRoutes
这是在 vue2.2.0
之后新添加的 api
,或许很多情况路由并不是写死的,需要动态添加路由。有了前面的 createRouteMap
的基础上我们只需要传入 routes
即可,他就能在原基础上修改
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
并且看到在 createMathcer
最后返回了这个方法,所以我们就可以使用这个方法
return {
match,
addRoutes
}
match
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
…
}
接下来就是 match
方法,它接收 3 个参数,其中 raw
是 RawLocation
类型,它可以是一个 url
字符串,也可以是一个 Location
对象;currentRoute
是 Route
类型,它表示当前的路径;redirectedFrom
和重定向相关。match
方法返回的是一个路径,它的作用是根据传入的 raw
和当前的路径 currentRoute
计算出一个新的路径并返回。至于他是如何计算出这条路径的,可以详细看一下如何计算出location
的 normalizeLocation
方法和 _createRoute
方法。
小结
-
createMatcher
: 根据路由的配置描述建立映射表,包括路径、名称到路由record
的映射关系, 最重要的就是createRouteMap
这个方法,这里也是动态路由匹配和嵌套路由的原理。 -
addRoutes
: 动态添加路由配置 -
match
: 根据传入的raw
和当前的路径currentRoute
计算出一个新的路径并返回。
路由模式
vue-router
支持三种路由模式(mode):hash
、history
、abstract
,其中 abstract
是在非浏览器环境下使用的路由模式 源码地址 (https://github.com/vuejs/vue-router/blob/dev/src/index.js)。
这一部分在前面初始化 vueRouter
对象时提到过,首先拿到配置项的模式,然后根据当前传入的配置判断当前浏览器是否支持这种模式,默认 IE9
以下会降级为 hash
。然后根据不同的模式去初始化不同的 history
实例。
// 一般分两种模式 hash 和 history 路由 第三种是抽象模式不常用
let mode = options.mode || ‘hash’
// 判断当前传入的配置是否能使用 history 模式
this.fallback = mode === ‘history’ && !supportsPushState && options.fallback !== false
// 降级处理
if (this.fallback) {
mode = ‘hash’
}
if (!inBrowser) {
mode = ‘abstract’
}
this.mode = mode
// 根据模式实例化不同的 history history 对象会对路由进行管理 继承于 history class
switch (mode) {
case ‘history’:
this.history = new HTML5History(this, options.base)
break
case ‘hash’:
this.history = new HashHistory(this, options.base, this.fallback)
break
case ‘abstract’:
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== ‘production’) {
assert(false, invalid mode: ${mode}
)
}
}
小结
vue-router
支持三种路由模式,hash
、history
和 abstract
。默认为 hash
,如果当前浏览器不支持 history
则会做降级处理,然后完成 history
的初始化。
路由切换
切换 url 主要是调用了 push
方法,下面以哈希模式为例,分析push
方法实现的原理 。push
方法切换路由的实现原理 源码地址 (https://github.com/vuejs/vue-router/blob/dev/src/history/hash.js)
首先在 src/index.js
下找到 vueRouter
定义的 push
方法
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== ‘undefined’) {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
接着我们需要定位到 history/hash.js
。这里首先获取到当前路径然后调用了 transitionTo
做路径切换,在回调函数当中执行 pushHash
这个核心方法。
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
// 路径切换的回调函数中调用 pushHash
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
而 pushHash
方法在做完浏览器兼容判断后调用的 pushState
方法,将 url
传入
export function pushState (url?: string, replace?: boolean) {
const history = window.history
try {
// 调用浏览器原生的 history 的 pushState 接口或者 replaceState 接口,pushState 方法会将 url 入栈
if (replace) {
history.replaceState({ key: _key }, ‘’, url)
} else {
_key = genKey()
history.pushState({ key: _key }, ‘’, url)
}
} catch (e) {
window.locationreplace ? ‘replace’ : ‘assign’
}
}
可以发现,push
底层调用了浏览器原生的 history
的 pushState
和 replaceState
方法,不是 replace
模式 会将 url 推历史栈当中。
另外提一嘴拼接哈希的原理
源码位置 (https://github.com/vuejs/vue-router/blob/dev/src/history/hash.js)
初始化 HashHistory
时,构造函数会执行 ensureSlash
这个方法
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
…
ensureSlash()
}
…
}
这个方法首先调用 getHash
,然后执行 replaceHash()
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === ‘/’) {
return true
}
replaceHash(‘/’ + path)
return false
}
下面是这几个方法
export function getHash (): string {
const href = window.location.href
const index = href.indexOf(‘#’)
return index === -1 ? ‘’ : href.slice(index + 1)
}
// 真正拼接哈希的方法
function getUrl (path) {
const href = window.location.href
const i = href.indexOf(‘#’)
const base = i >= 0 ? href.slice(0, i) : href
return ${base}#${path}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
举个例子来说: 假设当前URL是 http://localhost:8080
,path
为空,执行 replcaeHash('/' + path)
,然后内部执行 getUrl
计算出 url
为http://localhost:8080/#/
,最后执行 pushState(url,true)
,就大功告成了!
小结
hash
模式的 push
方法会调用路径切换方法 transitionTo
,接着在回调函数中调用pushHash
方法,这个方法调用的 pushState
方法底层是调用了浏览器原生 history
的方法。push
和 replace
的区别就在于一个将 url
推入了历史栈,一个没有,最直观的体现就是 replace
模式下浏览器点击后退不会回到上一个路由去 ,另一个则可以。
router-view & router-link
vue-router
在 install
时全局注册了两个组件一个是 router-view
一个是 router-link
,这两个组件都是典型的函数式组件。源码地址 (https://github.com/vuejs/vue-router/tree/dev/src/components)
router-view
首先在 router
组件执行 beforeCreate
这个钩子时,把 this._route
转为了响应式的一个对象
Vue.util.defineReactive(this, ‘_route’, this._router.history.current)
所以说每次路由切换都会触发 router-view
重新 render
从而渲染出新的视图。
核心的 render
函数作用请看代码注释
render (_, { props, children, parent, data }) {
…
// 通过 depth 由 router-view 组件向上遍历直到根组件,遇到其他的 router-view 组件则路由深度+1 这里的 depth 最直接的作用就是帮助找到对应的 record
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
// parent.$vnode.data.routerView 为 true 则代表向上寻找的组件也存在嵌套的 router-view
if (parent.KaTeX parse error: Expected 'EOF', got '&' at position 7: vnode &̲& parent.vnode.data.routerView) {
depth++
}
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
if (inactive) {
return h(cache[name], data, children)
}
// 通过 matched 记录寻找出对应的 RouteRecord
const matched = route.matched[depth]
if (!matched) {
cache[name] = null
return h()
}
// 通过 RouteRecord 找到 component
const component = cache[name] = matched.components[name]
// 往父组件注册 registerRouteInstance 方法
data.registerRouteInstance = (vm, val) => {
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
// 渲染组件
return h(component, data, children)
}
触发更新也就是 setter
的调用,位于 src/index.js
,当修改 _route
就会触发更新。
history.listen(route => {
this.apps.forEach((app) => {
// 触发 setter
app._route = route
})
})
router-link
分析几个重要的部分:
- 设置
active
路由样式
router-link
之所以可以添加 router-link-active
和 router-link-exact-active
这两个 class
去修改样式,是因为在执行 render
函数时,会根据当前的路由状态,给渲染出来的 active
元素添加 class
render (h: Function) {
…
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
// Support global empty active class
const activeClassFallback = globalActiveClass == null
? ‘router-link-active’
: globalActiveClass
const exactActiveClassFallback = globalExactActiveClass == null
? ‘router-link-exact-active’
: globalExactActiveClass
…
}
router-link
默认渲染为a
标签,如果不是会去向上查找出第一个a
标签
if (this.tag === ‘a’) {
data.on = on
data.attrs = { href }
} else {
// find the first child and apply listener and href
const a = findAnchor(this.$slots.default)
if (a) {
// in case the is a static node
a.isStatic = false
const aData = (a.data = extend({}, a.data))
aData.on = on
const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
aAttrs.href = href
} else {
// 不存在则渲染本身元素
data.on = on
}
}
- 切换路由,触发相应事件
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
// replace路由
router.replace(location)
} else {
// push 路由
router.push(location)
}
}
}
权限控制动态路由原理分析
我相信,开发过后台项目的同学经常会碰到以下的场景: 一个系统分为不同的角色,然后不同的角色对应不同的操作菜单和操作权限。例如: 教师可以查询教师自己的个人信息查询然后还可以查询操作学生的信息和学生的成绩系统、学生用户只允许查询个人成绩和信息,不允许更改。在 vue2.2.0
之前还没有加入 addRoutes
这个 API 是十分困难的的。
目前主流的路由权限控制的方式是:
-
登录时获取
token
保存到本地,接着前端会携带token
再调用获取用户信息的接口获取当前用户的角色信息。 -
前端再根据当前的角色计算出相应的路由表拼接到常规路由表后面。
登录生成动态路由全过程
了解 如何控制动态路由之后,下面是一张全过程流程图
前端在 beforeEach
中判断:
-
缓存中存在 JWT 令牌
-
- 访问
/login
: 重定向到首页/
- 访问
-
访问
/login
以外的路由: 首次访问,获取用户角色信息,然后生成动态路由,然后访问以replace
模式访问/xxx
路由。这种模式用户在登录之后不会在history
存放记录 -
不存在 JWT 令牌
-
- 路由在白名单中: 正常访问
/xxx
路由
- 路由在白名单中: 正常访问
-
不在白名单中: 重定向到
/login
页面
结合框架源码分析
下面结合 vue-element-admin
的源码分析该框架中如何处理路由逻辑的。
路由访问逻辑分析
首先可以定位到和入口文件 main.js
同级的 permission.js
, 全局路由守卫处理就在此。源码地址 (https://github.com/251205668/student-admin-template/blob/master/src/permission.js)
const whiteList = [‘/login’, ‘/register’] // 路由白名单,不会重定向
// 全局路由守卫
router.beforeEach(async(to, from, next) => {
NProgress.start() //路由加载进度条
// 设置 meta 标题
document.title = getPageTitle(to.meta.title)
// 判断 token 是否存在
const hasToken = getToken()
if (hasToken) {
if (to.path === ‘/login’) {
// 有 token 跳转首页
next({ path: ‘/’ })
NProgress.done()
} else {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取动态路由,添加到路由表中
const { roles } = await store.dispatch(‘user/getInfo’)
const accessRoutes = await store.dispatch(‘permission/generateRoutes’, roles)
router.addRoutes(accessRoutes)
// 使用 replace 访问路由,不会在 history 中留下记录,登录到 dashbord 时回退空白页面
next({ …to, replace: true })
} catch (error) {
next(‘/login’)
NProgress.done()
}
}
}
} else {
// 无 token
// 白名单不用重定向 直接访问
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
// 携带参数为重定向到前往的路径
next(/login?redirect=${to.path}
)
NProgress.done()
}
}
})
这里的代码我都添加了注释方便大家好去理解,总结为一句话就是访问路由 /xxx
,首先需要校验 token
是否存在,如果有就判断是否访问的是登录路由,走的不是登录路由则需要判断该用户是否是第一访问首页,然后生成动态路由,如果走的是登录路由则直接定位到首页,如果没有 token
就去检查路由是否在白名单(任何情况都能访问的路由),在的话就访问,否则重定向回登录页面。
下面是经过全局守卫后路由变化的截图
结合Vuex生成动态路由
下面就是分析这一步 const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
是怎么把路由生成出来的。源码地址 (https://github.com/251205668/student-admin-template/blob/master/src/store/modules/permission.js)
首先 vue-element-admin
中路由是分为两种的:
-
constantRoutes: 不需要权限判断的路由
-
asyncRoutes: 需要动态判断权限的路由
// 无需校验身份路由
export const constantRoutes = [
{
path: ‘/login’,
component: () => import(‘@/views/login/index’),
hidden: true
}
…
],
// 需要校验身份路由
export const asyncRoutes = [
// 学生角色路由
{
path: ‘/student’,
name: ‘student’,
component: Layout,
meta: { title: ‘学生信息查询’, icon: ‘documentation’, roles: [‘student’] },
children: [
{
path: ‘info’,
component: () => import(‘@/views/student/info’),
name: ‘studentInfo’,
meta: { title: ‘信息查询’, icon: ‘form’ }
},
{
path: ‘score’,
component: () => import(‘@/views/student/score’),
name: ‘studentScore’,
meta: { title: ‘成绩查询’, icon: ‘score’ }
}
]
}]
…
生成动态路由的源码位于 src/store/modules/permission.js
中的 generateRoutes
方法,源码如下:
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes(‘admin’)) {
accessedRoutes = asyncRoutes || []
} else {
// 不是 admin 去遍历生成对应的权限路由表
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
// vuex 中保存异步路由和常规路由
commit(‘SET_ROUTES’, accessedRoutes)
resolve(accessedRoutes)
})
}
从 route.js
读取 asyncRoutes
和 constantRoutes
之后首先判断当前角色是否是 admin
,是的话默认超级管理员能够访问所有的路由,当然这里也可以自定义,否则去过滤出路由权限路由表,然后保存到 Vuex
中。最后将过滤之后的 asyncRoutes
和 constantRoutes
进行合并。过滤权限路由的源码如下:
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
// 浅拷贝
const tmp = { …route }
// 过滤出权限路由
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
首先定义一个空数组,对传入 asyncRoutes
进行遍历,判断每个路由是否具有权限,未命中的权限路由直接舍弃 判断权限方法如下:
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
// roles 有对应路由元定义的 role 就返回 true
return roles.some(role => route.meta.roles.includes(role))
} else {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后的最后
面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
需要完整面试题的朋友可以点击蓝色字体免费获取
n(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
首先定义一个空数组,对传入 asyncRoutes
进行遍历,判断每个路由是否具有权限,未命中的权限路由直接舍弃 判断权限方法如下:
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
// roles 有对应路由元定义的 role 就返回 true
return roles.some(role => route.meta.roles.includes(role))
} else {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-NFpqkK7D-1712218138117)]
[外链图片转存中…(img-EMh8BI5R-1712218138118)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-RjlywXI9-1712218138118)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后的最后
面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
需要完整面试题的朋友可以点击蓝色字体免费获取
[外链图片转存中…(img-KDWl42Io-1712218138118)]
[外链图片转存中…(img-pcJy6oA8-1712218138119)]
[外链图片转存中…(img-ZpGCeQWE-1712218138119)]
[外链图片转存中…(img-0G8w5yaD-1712218138119)]