天天看点

vue-router源码解析(三) —— History系列文章目录前言一、index.js中的History初始化二、History目录总结

系列文章目录

1、vue-router源码解析(一)

2、vue-router源码解析(二) —— install

3、vue-router源码解析(三) —— History

文章目录

  • 系列文章目录
  • 前言
  • 一、index.js中的History初始化
  • 二、History目录
    • 1、base.js
    • 2、hash.js
    • 3、html5.js
    • 4、abstract.js
  • 总结

前言

上一篇简单介绍了下vue-router的挂载过程,本篇详细解析下VueRoute的三种路由模式~

一、index.js中的History初始化

VueRouter 对象是在 src/index.js 中暴露出来的,它在实例初始化时,初始化了History对象:

// index.js
// 引入history中的HashHistory,HTML5History,AbstractHistory模块
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'
// 定义VueRouter对象
export default class VueRouter {
	constructor (options: RouterOptions = {}) {
		...
		let mode = options.mode || 'hash'   // 默认是hash模式
    	this.fallback = 
    	mode === 'history' && !supportsPushState && options.fallback !== false   
   	 	if (this.fallback) {  // 降级处理,不支持History模式则使用hash模式
      		mode = 'hash'
    	}
    	if (!inBrowser) {
      		mode = 'abstract'
    	}
   		this.mode = mode
		switch (mode) {
	      case 'history':
	        this.history = new HTML5History(this, options.base)
	        break
	      case 'hash':  // 传入fallback
	        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}`)
	        }
	    }
	}
	...
}
           
  • 在VueRouter实例初始化中,mode得到用户传入的路由模式值,默认是

    hash

    。支持三种模式:

    hash、history、abstract

  • 接着判定当为history模式时,当前环境是否支持HTML5 history API,若不支持则fallback=true,降级处理,并且使用hash模式:

    if (this.fallback) { mode='hash' }

  • 判定当前环境是否是浏览器环境,若不是,则默认使用

    abstract

    抽象路由模式,这种抽象模式,通过数组来模拟浏览器操作栈。
  • 根据不同的mode,初始化不同的History实例,hash模式需传入

    this.fallback

    来判断降级处理情况。因为要针对这种降级情况做特殊的URL处理。后续history/hash.js会讲到。

二、History目录

├── history          // 路由模式相关
│   ├── abstract.js  // 非浏览器环境下的,抽象路由模式
│   ├── base.js      // 定义History基类
│   ├── hash.js      // hash模式,#
│   └── html5.js     // html5 history模式
           
vue-router源码解析(三) —— History系列文章目录前言一、index.js中的History初始化二、History目录总结

HashHistory、HTML5History、AbstractHistory

实例都 继承自src/history/base.js 中的

History

类的

1、base.js

export class History {
	router: Router   //vueRouter对象
	base: string    //基准路径
	current: Route  //当前的route对象
	pending: ?Route // 正在跳转的route对象,阻塞状态
	cb: (r: Route) => void  // 每一次路由跳转的回调,会触发routeview的渲染
	ready: boolean  // 就绪状态
	readyCbs: Array<Function>  // 就绪状态的回调数组
	readyErrorCbs: Array<Function> // 就绪时产生错误的回调数组。
	errorCbs: Array<Function> // 错误的回调数组 
	listeners: Array<Function>
	cleanupListeners: Function
	
	// 以下方法均在子类中实现(hashHistory,HTML5History,AbstractHistory)
    +go: (n: number) => void
    +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
    +replace: (
	    loc: RawLocation,
	    onComplete?: Function,
	    onAbort?: Function
     ) => void
    +ensureURL: (push?: boolean) => void
    +getCurrentLocation: () => string
    +setupListeners: Function
    
	constructor (router: Router, base: ?string) {
	    this.router = router
	    this.base = normalizeBase(base)   // 返回基准路径
	    // start with a route object that stands for "nowhere"
	    this.current = START   // 当前路由对象,import {START} from '../util/route'
	    ...
	  }
   	// 注册监听
	listen (cb: Function) {
	    this.cb = cb
	}
	// transitionTo方法,是对路由跳转的封装,onComplete是成功的回调,onAbort是失败的回调
	transitionTo (location: RawLocation,onComplete?,onAbort?){
	  	...
	}
	// confirmTransition方法,是确认跳转
	confirmTransition (location: RawLocation,onComplete?,onAbort?){
	  	...
	}
	  // 更新路由,并执行listen 的 cb 方法, 更改_route变量,触发视图更新
	updateRoute (route: Route) {
	    this.current = route  // 更新 current route
	    this.cb && this.cb(route)
	}
  ...
}
           

this.current = START

赋予current属性为一个route对象的初始状态:START在src/util/route.js中有定义,createRoute函数在route.js中也有定义,返回一个Route对象。

export const START = createRoute(null, {
  path: '/'
})
           

我们所用到的route对象,都是通过

createRoute

方法返回。可以看到我们用route时常用到的

name

,

meta

,

path

,

hash

,

params

等属性

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}

  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}
           

2、hash.js

下面看一下HashHistory对象

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    //  判定是否是从history模式降级而来,若是降级模式,更改URL(自动添加#号)
    if (fallback && checkFallback(this.base)) {
      return
    }
    // 保证 hash 是以 / 开头,所以访问127.0.0.1时,会自动替换为127.0.0.1/#/
    ensureSlash()  
  }

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  setupListeners () {...}

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {...}

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {...}

  go (n: number) {
    window.history.go(n)
  }
  
  ensureURL (push?: boolean) {...}
  
  // 获取当前hash值
  getCurrentLocation () {...}
}
           
  • HashHistory在初始化中继承于History父类,在初始化中,继承了父类的相关属性,判定了是否是从history模式降级而来,对URL做了相关处理。
  • 分别具体实现了父类的

    setupListeners

    push

    replace

    go

    ensureURL

    getCurrentLocation

    方法。

重点看一下我们经常用到的push()方法

我们使用vue-router跳转路由时使用:

this.$router.push()

。可见在VueRouter对象中会有一个push方法:(index.js)

export default class VueRouter {
	...
	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)
	    }
  	}
}
           

以上可以看出,

router.push()

最终会使用

this.history.push()

方法跳转路由。来看一下HashHistory中push()方法:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)  // 
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }
           
  • push()方法也主要是用到了

    transitionTo()

    方法跳转路由,transitionTo()是在base.js中History基类中有定义,HashHistory也继承了此方法。
  • 在调用transitionTo()方法,路由跳转完成之后,执行

    pushHash(route.fullPath)

    ,这里做了容错处理,判定是否存在html5 history API,若支持用history.pushState()操作浏览器历史记录,否则用

    window.location.hash = path

    替换文档。注意:调用history.pushState()方法不会触发 popstate 事件,popstate只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JS中调用 history.back()、history.forward()、history.go() 方法)。
function pushHash (path) {
  if (supportsPushState) {// 判定是否存在html5 history API
    pushState(getUrl(path))// 使用pushState或者window.location.hash替换文档
  } else {
    window.location.hash = path
  }
}
           
  • 查看 transitionTo 方法,主要是调用了

    confirmTransition()

    方法。
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    this.pending = route
    const abort = err => {   // 定义取消函数
      ...
      onAbort && onAbort(err)
    }
    
	// 如果目标路由与当前路由相同,取消跳转
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    if (
      isSameRoute(route, current) &&
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      return abort(createNavigationDuplicatedError(current, route))
    }
    
    // 根据当前路由对象和匹配的路由:返回更新的路由、激活的路由、停用的路由
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )
   // 定义钩子队列
    const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
      extractLeaveGuards(deactivated),
      // global before hooks
      this.router.beforeHooks,
      // in-component update hooks
      extractUpdateHooks(updated),
      // in-config enter guards
      activated.map(m => m.beforeEnter),
      // async components
      resolveAsyncComponents(activated)
    )
    // 定义迭代器
    const iterator = (hook: NavigationGuard, next) => {
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      try {
        hook(route, current, (to: any) => {
          if (to === false) {
            // next(false) -> abort navigation, ensure current URL
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) {
            this.ensureURL(true)
            abort(to)
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            // next('/') or next({ path: '/' }) -> redirect
            abort(createNavigationRedirectedError(current, route))
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // confirm transition and pass on the value
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }
    
    // 执行钩子队列
    runQueue(queue, iterator, () => {
      // wait until async components are resolved before
      // extracting in-component enter guards
      const enterGuards = extractEnterGuards(activated)
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            handleRouteEntered(route)
          })
        }
      })
    })
  }
           

大致是几个步骤:

  1. 如果目标路由与当前路由相同,取消跳转
  2. 定义钩子队列,依次为:

    组件导航守卫 beforeRouteLeave -> 全局导航守卫 beforeHooks -> 组件导航守卫 beforeRouteUpdate -> 目标路由的 beforeEnter -> 处理异步组件 resolveAsyncComponents

  3. 定义迭代器
  4. 执行钩子队列

3、html5.js

HTML5History类的实现方式与HashHistory的思路大致一样。不再详细赘述。

4、abstract.js

AbstractHistory类,也同样继承实现了History类中几个路由跳转方法。但由于此模式一般用于非浏览器环境,没有history 相关操作API,通过

this.stack

数组来模拟操作历史栈。

export class AbstractHistory extends History {
  index: number
  stack: Array<Route>

  constructor (router: Router, base: ?string) {
    super(router, base)
    this.stack = []  // 初始化模拟记录栈
    this.index = -1  // 当前活动的栈的位置
  }
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(
      location,
      route => {
      	// 更新历史栈信息
        this.stack = this.stack.slice(0, this.index + 1).concat(route) 
        this.index++ // 更新当前所处位置
        onComplete && onComplete(route)
      },
      onAbort
    )
  }
  ...
 }
           

总结

三种路由方式可以让前端不需要请求服务器,完成页面的局部刷新。

继续阅读