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

系列文章目录

1、vue-router源码解析(一)
2、vue-router源码解析(二) —— install
3、vue-router源码解析(三) —— History



前言

上一篇简单介绍了下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模式

在这里插入图片描述
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()方法:

扫描二维码关注公众号,回复: 12466311 查看本文章
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
    )
  }
  ...
 }

总结

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

猜你喜欢

转载自blog.csdn.net/qq_36131788/article/details/112800220