Vue:vue-router的基础原理

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事件;

      注意:

      1. 仅仅调用pushState方法或replaceState方法是不能触发该事件的,只有用户点击浏览器前进或者后退按钮时,或者用history.back/forward/go方法时才会触发。

        1. 只针对同一个文档,如果浏览历史的切换会切换不同的文档,该事件也不会触发;

      用法:使用时,可以为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()注册过的回调函数。
    
  • 完整的导航解析流程:

    1. 导航触发;
    2. 在即将失活的组件调用beforeRouteLeave守卫;
    3. 调用全局beforeEach守卫;
    4. 在宠用的组件里调用beforeRouteUpdate守卫;
    5. 在路由配置里调用beforeEnter守卫;
    6. 解析异步路由组件;
    7. 在下一个激活的组件里调用beforeRouteEnter守卫;
    8. 调用全局beforeResolve守卫;
    9. 导航被确认;
    10. 调用全局的afterEach钩子;
    11. 触发DOM的更新;
    12. 调用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 // 浏览器地址栏显示新的地址

猜你喜欢

转载自blog.csdn.net/yivisir/article/details/109305952