likes
comments
collection
share

vue2视图切换:vue-router

作者站长头像
站长
· 阅读数 35

vue路由是单页面中视图切换的方案。

文件main.js:

import Vue from "vue";
import VueRouter from "vue-router";
import App from "./App";

Vue.use(VueRouter);

// 1. 定义组件。
const Foo = { template: "<div>foo</div>" };
const Bar = { template: "<div>bar</div>" };

// 2. 定义路由
const routes = [
  { path: "/foo", component: Foo },
  { path: "/bar", component: Bar }
];

// 3. 实例化`VueRouter`
const router = new VueRouter({
  routes
});

// 4. 将`router`作为参数实例化`Vue`。
const app = new Vue({
  el: "#app",
  render(h) {
    return h(App);
  },
  router
});

文件App.vue:

<template>
  <div>
    <div>
      <!-- 使用 router-link 组件来导航. -->
      <router-link to="/foo">Go to Foo</router-link>
      <router-link to="/bar">Go to Bar</router-link>
    </div>
    <!-- 路由出口 -->
    <router-view></router-view>
  </div>
</template>

我们让其跑在本地http://localhost:8080上。

一、安装VueRouter

VueRouterClass类:

export default class VueRouter {
    // ...
}
VueRouter.install = install

install定义在同级目录下的install.js文件中:

import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

Vue.use定义在initGlobalAPI(Vue)中的initUse(Vue)

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}
/**
 * Convert an Array-like object to a real Array.
 */
export function toArray(list: any, start?: number): Array<any> {
  start = start || 0;
  let i = list.length - start;
  const ret: Array<any> = new Array(i);
  while (i--) {
    ret[i] = list[i + start];
  }
  return ret;
}

当执行Vue.use(VueRouter)时,获取到this.installedPlugins,如果其中存在当前的plugin则直接返回,否则获取除去前1位(VueRouer)后的参数数组,然后将this(Vue)补充至首位,就是将第一个参数由VueRouer换成了VueVueRouer中的installfunction类型,所以,再以VueRouer为执行主体,Vue为参数,执行plugin.install.apply(plugin, args),相当于执行install (Vue)

install(Vue)中首次安装则会执行install.installed = true_Vue = Vue,这里的_Vue就可以在全局使用,如果进行第二次重复安装,则会执行if (install.installed && _Vue === Vue) return进行终止操作。接下来会在Vue中通过Vue.mixin在全局options中混入方法beforeCreatedestroyed,在Vue原型上挂载$router$route,通过Vue.component注册组件RouterViewRouterLink,最后将合并策略中与路由先关的beforeRouteEnterbeforeRouteLeavebeforeRouteUpdate都赋值为合并策略中的created函数。

二、实例化VueRouter

export default class VueRouter {
  // ...
  constructor (options: RouterOptions = {}) {
    if (process.env.NODE_ENV !== 'production') {
      warn(this instanceof VueRouter, `Router must be called with the new operator.`)
    }
    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、init、beforeEach、beforeResolve、afterEach和onReady等各种方法...
}

可以看出VueRouter实例化时将会生成appappsoptionsbeforeHooksresolveHooksafterHooksmatchermode等属性构成的实例。先分别看匹配器matcher、模式modehistory的获取。

1、matcher

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // 这里定义了match、addRoute、getRoutes、addRoutes等方法
  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

这里定义了matchaddRoutegetRoutesaddRoutes并返回,其中都用到了对于路由的匹配、添加或者获取都是基于pathList, pathMapnameMap,首先来看createRouteMap(routes)

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>,
  parentRoute?: RouteRecord
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
  })
  // ...
  return {
    pathList,
    pathMap,
    nameMap
  }
}
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  const { path, name } = route
  // ...
  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    instances: {},
    enteredCbs: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }

  if (route.children) {
    // ...
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  // ...
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}

先看主流程,通过normalizePath(path, parent, pathToRegexpOptions.strict)的方式处理当前路径,然后创建可以描述当前路由pathcomponentsnameparent等属性的record对象。如果有route.children那么,通过递归的方式执行addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs),其中的参数record将作为parent的参数传递给下一个节点,作为record中的parent属性,这样就建立起了父子关系。 再看pathMap中如果没有record.path则进行推入,并且通过pathMap[record.path] = recordpathrecord作为映射关系,而nameMapnamerecord的映射。

2、mode

实例化VueRouter的过程中还将定义了mode,有三种类型:hashhistoryabstract

  • hash,是默认mode
  • history,如果supportsPushState判断为false即不支持pushState,则降级为hash
export const supportsPushState =
  inBrowser &&
  (function () {
    const ua = window.navigator.userAgent

    if (
      (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
      ua.indexOf('Mobile Safari') !== -1 &&
      ua.indexOf('Chrome') === -1 &&
      ua.indexOf('Windows Phone') === -1
    ) {
      return false
    }

    return window.history && typeof window.history.pushState === 'function'
  })()
  • abstract,在非浏览器场景下为abstract mode也各自对应history不同的获取方式,最常用的为hash模式,先看HashHistory实例化。

3、history

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
  // 还有`setupListeners`、`push`和`replace`等方法
}

这里不管哪种mode都继承于History类:

export class History {
  router: Router
  base: string
  current: Route
  pending: ?Route
  cb: (r: Route) => void
  ready: boolean
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  listeners: Array<Function>
  cleanupListeners: Function
  // 声明了需要由子类实现的go、push、replace和ensureURL等方法
  constructor (router: Router, base: ?string) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
    this.listeners = []
  }
  // 还有`listen`、`onReady`、`onError`和`transitionTo`等方法
}

这里继承的过程类似于执行了History.call(this, router, base),最终创建了包含routerbasecurrent等属性的实例history

三、过程分析

1、初始化

通过脚手架启动项目,然后,在浏览器地址栏输入http://localhost:8080时,地址栏链接会变成http://localhost:8080/#/,app.vue页面中<router-view></router-view>部分渲染成了<!---->注释节点。

(1)url修改

在实例化HashHistory的过程中,会执行ensureSlash()

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}
export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  let href = window.location.href
  const index = href.indexOf('#')
  // empty path
  if (index < 0) return ''

  href = href.slice(index + 1)

  return href
}
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}
export function replaceState (url?: string) {
  pushState(url, true)
}
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      // preserve existing history state as it could be overriden by the user
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

执行完const path = getHash()path"",接着执行replaceHash('/'),即执行replaceState(getUrl(path)),其中的getUrl(path)http://localhost:8080/#/。最终执行pushState('http://localhost:8080/#/', true),相当于执行浏览器中window.history.replaceState(stateCopy, '', 'http://localhost:8080/#/'),实现了http://localhost:8080/直接替换为http://localhost:8080/#/而不在浏览器中留下任何记录。

(2)页面展示

页面中app.vue的render为:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    [
      _c(
        "div",
        [
          _c("router-link", { attrs: { to: "/foo" } }, [_vm._v("Go to Foo")]),
          _vm._v(" "),
          _c("router-link", { attrs: { to: "/bar" } }, [_vm._v("Go to Bar")])
        ],
        1
      ),
      _vm._v(" "),
      _c("router-view")
    ],
    1
  )
}

当执行到_c("router-link", { attrs: { to: "/bar" } }, [_vm._v("Go to Bar")])的时候,就执行到了组件router-view中的render函数:

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // used by devtools to display a router-view badge
    data.routerView = true

    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    // render previous view if the tree is inactive and kept-alive
    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        // #2301
        // pass props
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
        // render previous empty view
        return h()
      }
    }

    const matched = route.matched[depth]
    const component = matched && matched.components[name]

    // render empty node if no matched route or no config component
    if (!matched || !component) {
      cache[name] = null
      return h()
    }

    // cache component
    cache[name] = { component }

    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    data.registerRouteInstance = (vm, val) => {
      // val could be undefined for unregistration
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }

    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }

    // register instance in init hook
    // in case kept-alive component be actived when routes changed
    data.hook.init = (vnode) => {
      if (vnode.data.keepAlive &&
        vnode.componentInstance &&
        vnode.componentInstance !== matched.instances[name]
      ) {
        matched.instances[name] = vnode.componentInstance
      }

      // if the route transition has already been confirmed then we weren't
      // able to call the cbs during confirmation as the component was not
      // registered yet, so we call it here.
      handleRouteEntered(route)
    }

    const configProps = matched.props && matched.props[name]
    // save route and configProps in cache
    if (configProps) {
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }

    return h(component, data, children)
  }
}

function fillPropsinData (component, data, route, configProps) {
  // resolve props
  let propsToPass = data.props = resolveProps(route, configProps)
  if (propsToPass) {
    // clone to prevent mutation
    propsToPass = data.props = extend({}, propsToPass)
    // pass non-declared props as attrs
    const attrs = data.attrs = data.attrs || {}
    for (const key in propsToPass) {
      if (!component.props || !(key in component.props)) {
        attrs[key] = propsToPass[key]
        delete propsToPass[key]
      }
    }
  }
}

因为其中matched为空,所以返回了空的注释节点。 当执行到_c("router-link", { attrs: { to: "/foo" } }, [_vm._v("Go to Foo")]),的时候就执行到了组件router-link中的render函数中的以下代码:

// ...
const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location, noop)
        } else {
          router.push(location, noop)
        }
      }
    }

    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => {
        on[e] = handler
      })
    } else {
      on[this.event] = handler
    }
}
// ...
if (Array.isArray(this.event)) {
  this.event.forEach(e => {
    on[e] = handler
  })
} else {
  on[this.event] = handler
}
// ...

也就是当执行到点击事件的时候,会执行到handler函数,即router.push(location, noop)。 在当前例子中初始化时最终渲染结果为:

vue2视图切换:vue-router

2、切换

当点击Go to Foo或者Go to Bar的过程中,执行router.push(location, noop):

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
}

其中执行完pushState(cleanPath(this.base + route.fullPath))逻辑后,页面url地址栏的链接会变成http://localhost:8080/#/foo。 然后,在组件router-view变成vNode的时候,会执行到return h(component, data, children),当前例子中的路由/foo的路由对应的组件component{ template: '<div>foo</div>' },最终的渲染结果就是:

vue2视图切换:vue-router

总结

路由点击的过程会分两步:url的替换和真实组件的替换。 url替换会通过window.history.pushState方法进行页面url的替换,当前替换记录会存放在浏览器记录中。 然后,通过router-view组件获取到点击路由对应的真实组件,在patch阶段进行渲染。

转载自:https://juejin.cn/post/7141965586099077157
评论
请登录