vue的matcher_Vue番外篇 -- vue-router浅析原理

本文深入探讨Vue Router的原理,包括它在单页面应用中的角色,对比传统页面跳转的区别,以及Hash模式、History模式和abstract模式的实现细节。详细解释了vue-router如何管理WebApp的路径,通过创建Matcher和History实例来实现路径和组件的映射,同时也阐述了在不同模式下如何处理URL变化和页面更新,以及如何在服务端配置以支持History模式。文章还涵盖了vue-router的基本使用步骤和源码分析,帮助读者理解其工作流程。
摘要由CSDN通过智能技术生成

近期被问到一个问题,在你们项目中使用的是Vue的SPA(单页面)还是Vue的多页面设计?

这篇文章主要围绕Vue的SPA单页面设计展开。 关于如何展开Vue多页面设计请点击查看。

vue-router是什么?

首先我们需要知道vue-router是什么,它是干什么的?

这里指的路由并不是指我们平时所说的硬件路由器,这里的路由就是SPA(单页应用)的路径管理器。 换句话说,vue-router就是WebApp的链接路径管理系统。

vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。

那与传统的页面跳转有什么区别呢?

1.vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。

2.传统的页面应用,是用一些超链接来实现页面切换和跳转的。

在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质 就是建立起url和页面之间的映射关系。

至于为啥不能用a标签,这是因为用Vue做的都是单页应用,就相当于只有一个主的index.html页面,所以你写的标签是不起作用的,必须使用vue-router来进行管理。

vue-router实现原理

SPA(single page application):单一页面应用程序,有且只有一个完整的页面;当它在加载页面的时候,不会加载整个页面的内容,而只更新某个指定的容器中内容。

单页面应用(SPA)的核心之一是:

1.更新视图而不重新请求页面;

2.vue-router在实现单页面前端路由时,提供了三种方式:Hash模式、History模式、abstract模式,根据mode参数来决定采用哪一种方式。

路由模式

vue-router 提供了三种运行模式:

● hash: 使用 URL hash 值来作路由。默认模式。

● history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。

● abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。

Hash模式

vue-router 默认模式是 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会去重新加载。

hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分(/#/..),浏览器只会加载相应位置的内容,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

History模式

HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面;

由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

//main.js文件中

const router = new VueRouter({

mode: 'history',

routes: [...]

})

当使用 history 模式时,URL 就像正常的 url,例如 yoursite.com/user/id,比较好… 不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问

所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

export const routes = [

{path: "/", name: "homeLink", component:Home}

{path: "/register", name: "registerLink", component: Register},

{path: "/login", name: "loginLink", component: Login},

{path: "*", redirect: "/"}]

此处就设置如果URL输入错误或者是URL 匹配不到任何静态资源,就自动跳到到Home页面。

abstract模式

abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。

根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)

vue-router使用方式

1:下载 npm i vue-router -S

**2:在main.js中引入 ** import VueRouter from 'vue-router';

3:安装插件 Vue.use(VueRouter);

4:创建路由对象并配置路由规则

let router = new VueRouter({routes:[{path:'/home',component:Home}]});

5:将其路由对象传递给Vue的实例,options中加入 router:router

6:在app.vue中留坑

具体实现请看如下代码:

//main.js文件中引入

import Vue from 'vue';

import VueRouter from 'vue-router';

//主体

import App from './components/app.vue';

import index from './components/index.vue'

//安装插件

Vue.use(VueRouter); //挂载属性

//创建路由对象并配置路由规则

let router = new VueRouter({

routes: [

//一个个对象

{ path: '/index', component: index }

]

});

//new Vue 启动

new Vue({

el: '#app',

//让vue知道我们的路由规则

router: router, //可以简写router

render: c => c(App),

})

复制代码

最后记得在在app.vue中“留坑”

//app.vue中

export default {

data(){

return {}

}

}

复制代码

vue-router源码分析

我们先来看看vue的实现路径。

在入口文件中需要实例化一个 VueRouter 的实例对象 ,然后将其传入 Vue 实例的 options 中。

1 export defaultclass VueRouter {2 static install: () => void;3 static version: string;4

5 app: any;6 apps: Array;7 ready: boolean;8 readyCbs: Array;9 options: RouterOptions;10 mode: string;11 history: HashHistory | HTML5History |AbstractHistory;12 matcher: Matcher;13 fallback: boolean;14 beforeHooks: Array<?NavigationGuard>;15 resolveHooks: Array<?NavigationGuard>;16 afterHooks: Array<?AfterNavigationHook>;17

18 constructor (options: RouterOptions ={}) {19 this.app = null

20 this.apps =[]21 this.options =options22 this.beforeHooks =[]23 this.resolveHooks =[]24 this.afterHooks =[]25 //创建 matcher 匹配函数

26 this.matcher = createMatcher(options.routes || [], this)27 //根据 mode 实例化具体的 History,默认为'hash'模式

28 let mode = options.mode || 'hash'

29 //通过 supportsPushState 判断浏览器是否支持'history'模式

30 //如果设置的是'history'但是如果浏览器不支持的话,'history'模式会退回到'hash'模式

31 //fallback 是当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。

32 this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false

33 if (this.fallback) {34 mode = 'hash'

35 }36 //不在浏览器内部的话,就会变成'abstract'模式

37 if (!inBrowser) {38 mode = 'abstract'

39 }40 this.mode =mode41 //根据不同模式选择实例化对应的 History 类

42 switch(mode) {43 case 'history':44 this.history = new HTML5History(this, options.base)45 break

46 case 'hash':47 this.history = new HashHistory(this, options.base, this.fallback)48 break

49 case 'abstract':50 this.history = new AbstractHistory(this, options.base)51 break

52 default:53 if (process.env.NODE_ENV !== 'production') {54 assert(false, `invalid mode: ${mode}`)55 }56 }57 }58

59 match (60 raw: RawLocation,61 current?: Route,62 redirectedFrom?: Location63 ): Route {64 return this.matcher.match(raw, current, redirectedFrom)65 }66

67 get currentRoute (): ?Route {68 return this.history && this.history.current69 }70

71 init (app: any /*Vue component instance*/) {72 process.env.NODE_ENV !== 'production' &&assert(73 install.installed,74 `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +

75 `before creating root instance.`76 )77

78 this.apps.push(app)79

80 //main app already initialized.

81 if (this.app) {82 return

83 }84

85 this.app =app86

87 const history = this.history88 //根据history的类别执行相应的初始化操作和监听

89 if (history instanceofHTML5History) {90 history.transitionTo(history.getCurrentLocation())91 } else if (history instanceofHashHistory) {92 const setupHashListener = () =>{93 history.setupListeners()94 }95 history.transitionTo(96 history.getCurrentLocation(),97 setupHashListener,98 setupHashListener99 )100 }101

102 history.listen(route =>{103 this.apps.forEach((app) =>{104 app._route =route105 })106 })107 }108 //路由跳转之前

109 beforeEach (fn: Function): Function {110 return registerHook(this.beforeHooks, fn)111 }112 //路由导航被确认之间前

113 beforeResolve (fn: Function): Function {114 return registerHook(this.resolveHooks, fn)115 }116 //路由跳转之后

117 afterEach (fn: Function): Function {118 return registerHook(this.afterHooks, fn)119 }120 //第一次路由跳转完成时被调用的回调函数

121 onReady (cb: Function, errorCb?: Function) {122 this.history.onReady(cb, errorCb)123 }124 //路由报错

125 onError (errorCb: Function) {126 this.history.onError(errorCb)127 }128 //路由添加,这个方法会向history栈添加一个记录,点击后退会返回到上一个页面。

129 push (location: RawLocation, onComplete?: Function, onAbort?: Function) {130 this.history.push(location, onComplete, onAbort)131 }132 //这个方法不会向history里面添加新的记录,点击返回,会跳转到上上一个页面。上一个记录是不存在的。

133 replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {134 this.history.replace(location, onComplete, onAbort)135 }136 //相对于当前页面向前或向后跳转多少个页面,类似 window.history.go(n)。n可为正数可为负数。正数返回上一个页面

137 go (n: number) {138 this.history.go(n)139 }140 //后退到上一个页面

141 back () {142 this.go(-1)143 }144 //前进到下一个页面

145 forward () {146 this.go(1)147 }148

149 getMatchedComponents (to?: RawLocation | Route): Array{150 const route: any =to151 ?to.matched152 ?to153 : this.resolve(to).route154 : this.currentRoute155 if (!route) {156 return[]157 }158 return [].concat.apply([], route.matched.map(m =>{159 return Object.keys(m.components).map(key =>{160 returnm.components[key]161 })162 }))163 }164

165 resolve (166 to: RawLocation,167 current?: Route,168 append?: boolean

169 ): {170 location: Location,171 route: Route,172 href: string,173 //for backwards compat

174 normalizedTo: Location,175 resolved: Route176 } {177 const location =normalizeLocation(178 to,179 current || this.history.current,180 append,181 this

182 )183 const route = this.match(location, current)184 const fullPath = route.redirectedFrom ||route.fullPath185 const base = this.history.base186 const href = createHref(base, fullPath, this.mode)187 return{188 location,189 route,190 href,191 //for backwards compat

192 normalizedTo: location,193 resolved: route194 }195 }196

197 addRoutes (routes: Array) {198 this.matcher.addRoutes(routes)199 if (this.history.current !==START) {200 this.history.transitionTo(this.history.getCurrentLocation())201 }202 }203 }

HashHistory

• hash虽然出现在url中,但不会被包括在http请求中,它是用来指导浏览器动作的,对服务器端没影响,因此,改变hash不会重新加载页面。

• 可以为hash的改变添加监听事件:

window.addEventListener("hashchange",funcRef,false)

复制代码

• 每一次改变hash(window.location.hash),都会在浏览器访问历史中增加一个记录。

export class HashHistory extends History {

constructor (router: Router, base:?string, fallback: boolean) {

super(router, base)//check history fallback deeplinking

//如果是从history模式降级来的,需要做降级检查

if (fallback && checkFallback(this.base)) {//如果降级且做了降级处理,则返回

return}

ensureSlash()

}

.......

functioncheckFallback (base) {

const location=getLocation(base)//得到除去base的真正的 location 值

if (!/^\/#/.test(location)) {//如果此时地址不是以 /# 开头的

//需要做一次降级处理,降为 hash 模式下应有的 /# 开头

window.location.replace(

cleanPath(base+ '/#' +location)

)return true}

}function ensureSlash (): boolean{//得到 hash 值

const path =getHash()if (path.charAt(0) === '/') {//如果是以 / 开头的,直接返回即可

return true}//不是的话,需要手动保证一次 替换 hash 值

replaceHash('/' +path)return false}

exportfunctiongetHash (): string {//We can't use window.location.hash here because it's not

//consistent across browsers - Firefox will pre-decode it!

//因为兼容性的问题,这里没有直接使用 window.location.hash

//因为 Firefox decode hash 值

const href =window.location.href

const index= href.indexOf('#')return index === -1 ? '' : decodeURI(href.slice(index + 1))

}//得到hash之前的url地址

functiongetUrl (path) {

const href=window.location.href

const i= href.indexOf('#')

const base= i >= 0 ? href.slice(0, i) : hrefreturn`${base}#${path}`

}//添加一个hash

functionpushHash (path) {if(supportsPushState) {

pushState(getUrl(path))

}else{

window.location.hash=path

}

}//替代hash

functionreplaceHash (path) {if(supportsPushState) {

replaceState(getUrl(path))

}else{

window.location.replace(getUrl(path))

}

}

hash的改变会自动添加到浏览器的访问历史记录中。 那么视图的更新是怎么实现的呢,看下 transitionTo()方法:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {

const route= this.router.match(location, this.current) //找到匹配路由

this.confirmTransition(route, () => { //确认是否转化

this.updateRoute(route) //更新route

onComplete &&onComplete(route)this.ensureURL()//fire ready cbs once

if (!this.ready) {this.ready = true

this.readyCbs.forEach(cb =>{ cb(route) })

}

}, err=>{if(onAbort) {

onAbort(err)

}if (err && !this.ready) {this.ready = true

this.readyErrorCbs.forEach(cb =>{ cb(err) })

}

})

}//更新路由

updateRoute (route: Route) {

const prev= this.current //跳转前路由

this.current = route //装备跳转路由

this.cb && this.cb(route) //回调函数,这一步很重要,这个回调函数在index文件中注册,会更新被劫持的数据 _router

this.router.afterHooks.forEach(hook =>{

hook&&hook(route, prev)

})

}

}

pushState

export function pushState (url?: string, replace?: boolean) {

saveScrollPosition()//try...catch the pushState call to get around Safari

//DOM Exception 18 where it limits to 100 pushState calls

//加了 try...catch 是因为 Safari 有调用 pushState 100 次限制

//一旦达到就会抛出 DOM Exception 18 错误

const history =window.historytry{if(replace) {//replace 的话 key 还是当前的 key 没必要生成新的

history.replaceState({ key: _key }, '', url)

}else{//重新生成 key

_key =genKey()//带入新的 key 值

history.pushState({ key: _key }, '', url)

}

}catch(e) {//达到限制了 则重新指定新的地址

window.location[replace ? 'replace' : 'assign'](url)

}

}

replaceState

// 直接调用 pushState 传入 replace 为 true

export function replaceState (url?: string) {

pushState(url, true)

}

复制代码

pushState和replaceState两种方法的共同特点:当调用他们修改浏览器历史栈后,虽然当前url改变了,但浏览器不会立即发送请求该url,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础。

supportsPushState

export const supportsPushState = inBrowser && (function() {

const ua=window.navigator.userAgentif(

(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&ua.indexOf('Mobile Safari') !== -1 &&ua.indexOf('Chrome') === -1 &&ua.indexOf('Windows Phone') === -1) {return false}return window.history && 'pushState' inwindow.history

})()

其实所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。 $router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()

监听地址栏

在浏览器中,用户可以直接在浏览器地址栏中输入改变路由,因此还需要监听浏览器地址栏中路由的变化 ,并具有与通过代码调用相同的响应行为,在HashHistory中这一功能通过setupListeners监听hashchange实现:

setupListeners () {

window.addEventListener('hashchange', () =>{if (!ensureSlash()) {return}this.transitionTo(getHash(), route =>{

replaceHash(route.fullPath)

})

})

}

HTML5History

History interface是浏览器历史记录栈提供的接口,通过back(),forward(),go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。

export class HTML5History extends History {

constructor (router: Router, base:?string) {

super(router, base)

const expectScroll= router.options.scrollBehavior //指回滚方式

const supportsScroll = supportsPushState &&expectScrollif(supportsScroll) {

setupScroll()

}

const initLocation= getLocation(this.base)//监控popstate事件

window.addEventListener('popstate', e =>{

const current= this.current//Avoiding first `popstate` event dispatched in some browsers but first

//history route not updated since async guard at the same time.

//避免在某些浏览器中首次发出“popstate”事件

//由于同一时间异步监听,history路由没有同时更新。

const location = getLocation(this.base)if (this.current === START && location ===initLocation) {return}this.transitionTo(location, route =>{if(supportsScroll) {

handleScroll(router, route, current,true)

}

})

})

}

hash模式仅改变hash部分的内容,而hash部分是不会包含在http请求中的(hash带#):

oursite.com/#/user/id //如请求,只会发送http://oursite.com/

所以hash模式下遇到根据url请求页面不会有问题

而history模式则将url修改的就和正常请求后端的url一样(history不带#)

如果这种向后端发送请求的话,后端没有配置对应/user/id的get路由处理,会返回404错误。

官方推荐的解决办法是在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。同时这么做以后,服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。

两种模式比较

一般的需求场景中,hash模式与history模式是差不多的,根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有以下优势:

• pushState设置的新url可以是与当前url同源的任意url,而hash只可修改#后面的部分,故只可设置与当前同文档的url

• pushState设置的新url可以与当前url一模一样,这样也会把记录添加到栈中,而hash设置的新值必须与原来不一样才会触发记录添加到栈中

• pushState通过stateObject可以添加任意类型的数据记录中,而hash只可添加短字符串 pushState可额外设置title属性供后续使用

AbstractHistory

'abstract'模式,不涉及和浏览器地址的相关记录,流程跟'HashHistory'是一样的,其原理是通过数组模拟浏览器历史记录栈的功能

//abstract.js实现,这里通过栈的数据结构来模拟路由路径

export class AbstractHistory extends History {

index: number;

stack: Array;

constructor (router: Router, base:?string) {

super(router, base)this.stack =[]this.index = -1}//对于 go 的模拟

go (n: number) {//新的历史记录位置

const targetIndex = this.index +n//小于或大于超出则返回

if (targetIndex < 0 || targetIndex >= this.stack.length) {return}//取得新的 route 对象

//因为是和浏览器无关的 这里得到的一定是已经访问过的

const route = this.stack[targetIndex]//所以这里直接调用 confirmTransition 了

//而不是调用 transitionTo 还要走一遍 match 逻辑

this.confirmTransition(route, () =>{this.index =targetIndexthis.updateRoute(route)

})

}

//确认是否转化路由

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {

const current= this.current

const abort= err =>{if(isError(err)) {if (this.errorCbs.length) {this.errorCbs.forEach(cb =>{ cb(err) })

}else{

warn(false, 'uncaught error during route navigation:')

console.error(err)

}

}

onAbort&&onAbort(err)

}//判断如果前后是同一个路由,不进行操作

if(

isSameRoute(route, current)&&route.matched.length===current.matched.length

) {this.ensureURL()returnabort()

}//下面是各类钩子函数的处理

//*********************

})

}

看到这里你已经对vue-router的路由基本掌握的差不多了,要是喜欢看源码可以点击查看

要是喜欢可以给我一个star,github

感谢Aine_潔和CaiBoBo两位老师提供的思路。

作者:DIVI

链接:https://juejin.im/post/5bc6eb875188255c9c755df2

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值