Vue:vue-router的基础原理
一、前言
- 大多数Vue应用都为单页面应用,而实现单页面应用最关键的工具就是router,router的底层封装了浏览器的History类,使得页面在切换时浏览器无需请求新页面;
二、 vue-router的基本知识
1. vue-router的三种模式
-
vue-router总共有三种模式:HTML5 History、HashHistory、AbstractHistory(暂时不拓展)
-
History模式:
-
例如:http://test.com/abc
-
popstate事件:
定义:当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件;
注意:
-
仅仅调用pushState方法或replaceState方法是不能触发该事件的,只有用户点击浏览器前进或者后退按钮时,或者用history.back/forward/go方法时才会触发。
- 只针对同一个文档,如果浏览历史的切换会切换不同的文档,该事件也不会触发;
用法:使用时,可以为popstate事件指定回调函数,这个回调函数的参数是一个event事件对象,他的state属性指向pushState和replaceState方法的第一个参数(即url的状态对象)。
-
-
-
Hash模式:
-
例如:http://test.com/#/abc
-
若点击跳转链接或者浏览器历史跳转,将会触发hashchange事件,通过解析url,匹配到对应的路由规则,从而跳转到abc页面;
-
hashchange事件的触发条件:
直接更改浏览器地址,在最后面增加或者改变#hash
通过改变location.href或location.hash的值
通过点击带描点的链接
浏览器的前进后退可能改变hash,前提是hash的值不同
-
-
若是手动刷新,浏览器不会向服务器发送请求,但是也不会触发hashchange事件,可以通过load事件,解析url,匹配对应的路由规则,跳转到abc页面;
-
hash模式采用dom替换的方式进行页面内容的更改;
-
-
Abstract模式:Pending
2. Hash路由的关键实现
-
首先建立一个index.html页面,内含有a标签,a标签里有hash值,可进行页面跳转;
-
阻止浏览器默认行为,即链接的跳转。
-
捕获a标签的内容,作为hash值;
-
进行浏览器hash跳转;
-
在Vue源码中的实现逻辑:
$router.push()=>
hashHistory.push()=>
History.transitionTo()=>
History.updateRoute()=>
{app._route = route}=>
vm.render()
-
关键代码实现:
// 捕获hash document.querySelectorAll('a').forEach(item=>{ item.addEventListener('click',e=>{ e.preventDefault(); let link = item.textContent; location.hash = link; },false); }) // 监听路由 window.addEventListener('hashchange',e=>{ console.log({ location : location.href, hash : location.hash }); // 根据hash,进行dom操作 })
3. History路由的关键实现
-
首先建立一个index.html页面,内含有a标签,a标签里有目标路径,可进行页面跳转;
-
阻止浏览器默认行为,即链接的跳转。
-
捕获a标签内容,作为目标路径;
-
利用history.pushState方法,进行页面状态改变;
-
关键代码:
// 捕获路径 document.querySelectorAll('a').forEach(item=>{ item.addEventListener('click',e=>{ e.preventDefault(); let link = item.textContent; if(!!window.history %% history.pushState){ window.history.pushState({name : 'history'},link,link); }else{ // 不支持,安装polyfill补丁 } },false); }) // 监听路由 window.addEventListener('popstate',e=>{ console.log({ location : location.href, state : e.state }); // 根据路径,进行dom操作 })
4. 导航守卫
-
功能:正如其名,
vue-router
提供的导航守卫主要用来通过跳转或取消的方式守卫导航,通俗地讲,就是检测路由跳转过程中的具体变化。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的; -
三种导航守卫:全局守卫、路由守卫、组件守卫
-
全局守卫在路由的实例对象注册时使用:beforeEach,beforeResolve,afterEach
-
路由守卫在路由的配置项中定义:beforeEnter
-
组件守卫在组件属性中定义:beforeRouteEnter,beforeRouteUpdate,beforeRouterLeave
-
每个守卫方法都接收三个参数:
to :Route,即将要进入的路由对象
from :Route,当前导航正要离开的路由
next : Function,最后一定要调用该方法来resolve这个钩子,执行效果依赖next方法的调用参数:
1. next() : 进行管道中的下一个钩子。如果全部执行完了,则导航的状态就是comfirmded。 2. next(false) : 终端当前的导航。若浏览器的url改变了,那么url地址会重置到from路由对应的地址; 3. next('/')或者next({path : '/'}) : 跳转到一个不同的地址,当前的导航被中断,然后执行一个新的导航。可以向next转递任意位置的对象,且允许设置`replace: true`、`name: 'home'` 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。 4. next(error) : (2.4.0+)若传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调函数。
-
完整的导航解析流程:
- 导航触发;
- 在即将失活的组件调用
beforeRouteLeave
守卫; - 调用全局
beforeEach
守卫; - 在宠用的组件里调用
beforeRouteUpdate
守卫; - 在路由配置里调用
beforeEnter
守卫; - 解析异步路由组件;
- 在下一个激活的组件里调用
beforeRouteEnter
守卫; - 调用全局
beforeResolve
守卫; - 导航被确认;
- 调用全局的
afterEach
钩子; - 触发DOM的更新;
- 调用
beforeRouteEnter
守卫中传给next 的回调函数,创建好组件实例会作为回调函数的参数传入;
三、 vue-router的源码分析
1. 源码解析
-
首先看一下vue-router的构造函数
constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this) let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.mode = mode 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}`) } } }
-
首先是获取构造函数传递的mode值,若mode为history而浏览器不支持此模式,就强制mode为hash;如果支持history,则根据mode来选择模式。
-
获取到mode后,就是对路由进行初始化init了。来看看init方法:
init (app: any /* Vue component instance */) { // .... const history = this.history if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) }else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } // .... // VueRouter类暴露的以下方法实际是调用具体history对象的方法 push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.push(location, onComplete, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.replace(location, onComplete, onAbort) } }
-
从上面的源码可以看出,两种模式都是用transitionTo函数。
-
Hash模式下的
HashHistory.push()
:push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.transitionTo(location, route => { pushHash(route.fullPath); onComplete && onComplete(route); }, onAbort) } function pushHash (path) { window.location.hash = path }
-
HashHistory.push
方法最主要的是对location的hash进行了赋值,hash的改变将会自动添加到浏览器的访问历史记录中。 -
视图更新,就要牵涉到
TransitionTo
函数了:transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { // 调用 match 得到匹配的 route 对象 const route = this.router.match(location, this.current) this.confirmTransition(route, () => { this.updateRoute(route) ... }) } updateRoute (route: Route) { this.cb && this.cb(route) } listen (cb: Function) { this.cb = cb }
-
当路由发生变化时,调用了History的this.cb方法,这个方法是通过
History.listen
方法设置的。让我们回到VueRouter类的定义中,找到init方法:init (app: any /* Vue component instance */) { this.apps.push(app) history.listen(route => { this.apps.forEach((app) => { app._route = route }) }); }
-
代码中的app指的是Vue实例,app._route是在
Vue.use(Router)
加载vue-router插件的时候,通过Vue.mixin
方法全局注册的一个混合,影响到注册后的每个Vue实例,此混合在beforeCreated
钩子中通过Vue.util.defineReactive
定义了响应式的 _route。当route改变时,会自动调用Vue.render
来更新视图。vm.render
是根据当前的 _route 的path,name等属性,来将路由对应的组件渲染的。
2. 路由改变到视图更新的流程
1. this.$router.push(path)
2. HashHistory,push
3. History.transitionTo()
4. const route = this.$router.match(location,this.current) // 进行地址匹配,得到当前地址的route对象
5. History.updateRoute(route)
6. app._route = route
7. vm.render() // 在<router-view></router-view>中渲染
8. window.location.hash = route.fullpath // 浏览器地址栏显示新的地址