Skip to main content

VueRouter原理分析

SewenJanuary 22, 2024About 19 min源码分析VueVueRouter

VueRouter原理分析

Vue-Router 3.x

路由基础

总体流程

路由对象

VueRouter 的实现是一个类,它的定义在 src/index.js中:

export default class VueRouter {
  static install: () => void;
  static version: string;

  app: any;
  apps: Array<any>;
  ready: boolean;
  readyCbs: Array<Function>;
  options: RouterOptions;
  mode: string;
  history: HashHistory | HTML5History | AbstractHistory;
  matcher: Matcher;
  fallback: boolean;
  beforeHooks: Array<?NavigationGuard>;
  resolveHooks: Array<?NavigationGuard>;
  afterHooks: Array<?AfterNavigationHook>;

  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}`)
        }
    }
  }

  match (
    raw: RawLocation,
    current?: Route,
    redirectedFrom?: Location
  ): Route {
    return this.matcher.match(raw, current, redirectedFrom)
  }

  get currentRoute (): ?Route {
    return this.history && this.history.current
  }

  init (app: any) {
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )

    this.apps.push(app)

    if (this.app) {
      return
    }

    this.app = app

    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
      })
    })
  }

  beforeEach (fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
  }

  beforeResolve (fn: Function): Function {
    return registerHook(this.resolveHooks, fn)
  }

  afterEach (fn: Function): Function {
    return registerHook(this.afterHooks, fn)
  }

  onReady (cb: Function, errorCb?: Function) {
    this.history.onReady(cb, errorCb)
  }

  onError (errorCb: Function) {
    this.history.onError(errorCb)
  }

  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)
  }

  go (n: number) {
    this.history.go(n)
  }

  back () {
    this.go(-1)
  }

  forward () {
    this.go(1)
  }

  getMatchedComponents (to?: RawLocation | Route): Array<any> {
    const route: any = to
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if (!route) {
      return []
    }
    return [].concat.apply([], route.matched.map(m => {
      return Object.keys(m.components).map(key => {
        return m.components[key]
      })
    }))
  }

  resolve (
    to: RawLocation,
    current?: Route,
    append?: boolean
  ): {
    location: Location,
    route: Route,
    href: string,
    normalizedTo: Location,
    resolved: Route
  } {
    const location = normalizeLocation(
      to,
      current || this.history.current,
      append,
      this
    )
    const route = this.match(location, current)
    const fullPath = route.redirectedFrom || route.fullPath
    const base = this.history.base
    const href = createHref(base, fullPath, this.mode)
    return {
      location,
      route,
      href,
      normalizedTo: location,
      resolved: route
    }
  }

  addRoutes (routes: Array<RouteConfig>) {
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}

路由模式

hash 模式

使用 url 的 hash 值来作为路由。支持所有浏览器。一般用于单页应用。

表现:

原理:

History 模式

使用 HTML5 History API 和并需要服务器配置。一般用于多页应用。

表现:

原理:

缺点:

Abstract 模式

三种模式路由切换的底层实现

路由安装

路由匹配

路由切换

内置组件

<router-view>

相关问题

路由匹配过程

路由匹配引擎?

/test/:name 与 /test/:id 两个路由将如何匹配? (假设 name 参数特点为非数字字符串,id 参数特点为数字)

路由之间是怎么跳转的?有哪些方式?

组件导航: router-link

编程导航: router.push、 router.replace、 router.go

active-class 是哪个组件的属性?

active-class 是 router-link 属性,设置链接激活时使用的 CSS 类名。默认值可以通过路由的构造选项 linkActiveClass 来全局配置。


导航守卫相关:

响应路由参数的变化

如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1 -> /users/2) 如何响应变化?

对同一个组件中参数的变化做出响应

参考资料:响应路由参数的变化

beforeRouteEnter 能否拿到组件实例 this? 为什么?

如何在路由守卫进行管道中的下一个钩子?

为什么路由路由守卫中必须调用 next(),才会前进到下一个导航守卫钩子函数中?

Q:vue 路由中实现了一个 iterator 遍历器,在路由切换时,解析路由出守卫队列 queue ,然后使用遍历器遍历 路由守卫队列;

路由应用:

切换路由时需要保存草稿的功能,怎么实现?

使用 <keep-alive> 缓存组件:在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

在router中设置router的元信息meta:

//...router.js 
export default new Router({
    routes: [ 
      { path: '/',
        name: 'Hello',
        component: Hello, 
        meta: {
          keepAlive: false    // 不需要缓存 
  } 
}, 
       { path: '/page1', 
         name: 'Page1', 
         component: Page1, 
         meta: {
           keepAlive: true    // 需要被缓存 
       } 
     } 
   ] 
})

组件中添加 meta 判断:

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
 <router-view v-if="!$route.meta.keepAlive"></router-view>

刷新无法触发 router.beforeEach 守卫问题

参考资料