vue-router 源代码全流程分析「长文」

栏目: JavaScript · 发布时间: 6年前

内容简介:根据上述基础使用,我们大概可以梳理一个基本流程出来:下面我们就根据上述流程步骤,一步一步解析,首先
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const Home = { template: '<div>home</div>' };
const Foo = { template: '<div>foo</div>' };
const Bar = { template: '<div>bar</div>' };
const Child = { template: '<div>Child</div>' };

const router = new VueRouter({
  mode: 'history',
  // base: __dirname,
  base: '/', // 默认 ‘/’
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    {
      path: '/bar',
      component: Bar,
      children: [{ path: 'child', component: Child }]
    }
  ]
});

const template = `
<div id="app">
  <h1>Basic</h1>
  <ul>
    <li><router-link to="/">/</router-link></li>
    <li><router-link to="/foo">/foo</router-link></li>
    <li><router-link to="/bar">/bar</router-link></li>
    <li><router-link to="/bar/child">/bar</router-link></li>
  </ul>
  <router-view class="view"></router-view>
</div>
`;

new Vue({
  router,
  template
}).$mount('#app');
复制代码

根据上述基础使用,我们大概可以梳理一个基本流程出来:

  • 注册插件:
    • $router$route 注入所有启用路由的子组件。
    • 安装 <router-view><router-link>
  • 定义路由组件。
  • new VueRouter(options) 创建一个路由器, 传入相关配置。
  • 创建并挂载根实例,确保注入路由器。路由组件将在 <router-view> 中呈现。

下面我们就根据上述流程步骤,一步一步解析, vue-router 代码实现。

注册插件(vue-router)

首先 Vue.use(VueRouter); 这段代码间接执行 VueRouter 暴露的 install 方法,下面来看看 install 具体实现:(由于有完整的注释,故略去文字叙述)

install

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

/**
 * 安装 Vue.js 插件 install 方法调用时,会将 Vue 作为参数传入。
 *
 * @export
 * @param {*} Vue
 * @returns
 *
 */
export function install(Vue) {
  // 防止插件被多次安装 - 当 install 方法被同一个插件多次调用,插件将只会被安装一次。
  if (install.installed) return;
  install.installed = true;

  // 在 Vue 原型上添加 $router 属性( VueRouter )并代理到 this.$root._router
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this.$root._router;
    }
  });

  // 在 Vue 原型上添加 $route 属性( 当前路由对象 )并代理到 this.$root._route
  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this.$root._route;
    }
  });

  // 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
  Vue.mixin({
    /**
     * 混入 Vue 创建前钩子
     * 1.取传入 Vue 构造函数的路由配置参数并调用 init 方法。
     * 2.在 Vue 根实例添加 _router 属性( VueRouter 实例)
     * 3.执行路由实例的 init 方法并传入 Vue 实例
     * 4.把 ($route <=> _route) 处理为响应式的。
     */
    beforeCreate() {
      if (this.$options.router) {
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      }
    }
  });

  // 注册全局组件
  Vue.component('router-view', View);
  Vue.component('router-link', Link);
}
复制代码

上述 就是 vue-router 暴露给 Vue 的注册方法。这里特别说明一下: defineReactive Vue 构建响应式的核心方法。在研究注册的两个全局组件: <router-view><router-link> 之前,我们先讨论 VueRouter 构造函数,因为它们其中涉及到 VueRouter 的很多方法。

代码接着执行,接下来就是为 vue-router 装填配置并实例化。之后把实例化的结果传入 Vue

const router = new VueRouter({
  // 选择路由模式
  mode: 'history',
  // 应用的基路径。默认值: "/" 例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 "/app/".
  base: '/', // 默认 ‘/’
  // 路由配置表
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    {
      path: '/bar',
      component: Bar,
      children: [{ path: 'child', component: Child }]
    }
  ]
});

new Vue({
  router
}).$mount('#app');
复制代码

接下来我们就来看看定义路由的 VueRouter 构造函数。

定义路由:VueRouter 构造函数

/* @flow */

import { install } from './install';
import { createMatcher } from './create-matcher';
import { HashHistory } from './history/hash';
import { HTML5History } from './history/html5';
import { AbstractHistory } from './history/abstract';
import { inBrowser, supportsHistory } from './util/dom';
import { assert } from './util/warn';

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

  app: any; // Vue 实例
  options: RouterOptions; // 路由配置
  mode: string; // 路由模式,默认 hash
  history: HashHistory | HTML5History | AbstractHistory;
  match: Matcher; // 一个数组,包含当前路由的所有嵌套路径片段的路由记录。?
  fallback: boolean; // 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。
  beforeHooks: Array<?NavigationGuard>; // 前置钩子集合
  afterHooks: Array<?(to: Route, from: Route) => any>; // 后置钩子集合

  constructor(options: RouterOptions = {}) {
    this.app = null;
    this.options = options;
    this.beforeHooks = [];
    this.afterHooks = [];
    this.match = createMatcher(options.routes || []); // 匹配器

    /******* 确定路由模式 - 默认为 hash *******/
    let mode = options.mode || 'hash';
    // 如果传入的模式为 ·history· 在浏览器环境下不支持 history 模式,则强制回退到 hash 模式
    this.fallback = mode === 'history' && !supportsHistory;
    if (this.fallback) {
      mode = 'hash';
    }
    // 在非浏览器环境下,采用 abstract 模式
    if (!inBrowser) {
      mode = 'abstract';
    }
    this.mode = mode;
  }

  /**
   * 当前路由
   *
   * @readonly
   * @type {?Route}
   * @memberof VueRouter
   */
  get currentRoute(): ?Route {
    return this.history && this.history.current;
  }

  /**
   * 初始化
   * @param {Any} app Vue component instance
   */
  init(app: any) {
    // 断言有没有安装插件,如果没有抛出错误提示
    assert(
      install.installed,
      `没有安装。在创建根实例之前,请确保调用 Vue.use(VueRouter)。`
    );

    this.app = app;
    const { mode, options, fallback } = this;
    // 根据不同模式实例化不同基类,无效模式下抛出错误
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base);
        break;
      case 'hash':
        this.history = new HashHistory(this, options.base, fallback);
        break;
      case 'abstract':
        this.history = new AbstractHistory(this);
        break;
      default:
        assert(false, `invalid mode: ${mode}`);
    }

    // 调用 history 属性下 listen 方法?
    this.history.listen(route => {
      this.app._route = route;
    });
  }

  /**
   * Router 实例方法 beforeEach 全局前置的导航守卫。
   * 当一个导航触发时,全局前置守卫按照创建顺序调用。
   * 守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
   *
   * @param {Function} fn (to, from, next) => {}
   * @memberof VueRouter
   *
   */
  beforeEach(fn: Function) {
    this.beforeHooks.push(fn);
  }

  /**
   * Router 实例方法 afterEach 全局后置钩子
   *
   * @param {Function} fn (to, from) => {}
   * @memberof VueRouter
   */
  afterEach(fn: Function) {
    this.afterHooks.push(fn);
  }

  /**
   * 编程式导航 push 导航到对应的 location
   * 这个方法会向 history 栈添加一个新的记录,
   * 所以,当用户点击浏览器后退按钮时,则回到之前的 location。
   *
   * @param {RawLocation} location
   * @memberof VueRouter
   */
  push(location: RawLocation) {
    this.history.push(location);
  }

  /**
   * 编程式导航 replace 导航到对应的 location
   * 它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
   *
   * @param {RawLocation} location
   * @memberof VueRouter
   */
  replace(location: RawLocation) {
    this.history.replace(location);
  }

  /**
   * 在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
   *
   * @param {number} n
   * @memberof VueRouter
   */
  go(n: number) {
    this.history.go(n);
  }

  /**
   * 后退
   *
   * @memberof VueRouter
   */
  back() {
    this.go(-1);
  }

  /**
   * 前进
   *
   * @memberof VueRouter
   */
  forward() {
    this.go(1);
  }

  /**
   * 获取匹配到的组件列表
   *
   * @returns {Array<any>}
   * @memberof VueRouter
   */
  getMatchedComponents(): Array<any> {
    if (!this.currentRoute) {
      return [];
    }
    return [].concat.apply(
      [],
      this.currentRoute.matched.map(m => {
        return Object.keys(m.components).map(key => {
          return m.components[key];
        });
      })
    );
  }
}

// 添加 install 方法
VueRouter.install = install;

// 在浏览器环境下且 Vue 构造函数存在的情况下调用 use 方法注册插件(插件预装)
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter);
}
复制代码

由上述代码实现分析,首先声明了一些属性、方法、和常用的 API ;接着添加 install 方法和执行 Vue.use 方法注册插件。

其中对相关代码做了详细的注释,无需再此重复论述,但其中有一些方法将在后面涉及时,做深入分析。继续分析前,这里需要先对这段代码进行解释:

this.match = createMatcher(options.routes || []); // 创建匹配器
复制代码
/**
 * 创建匹配器
 *
 * @export
 * @param {Array<RouteConfig>} routes
 * @returns {Matcher}
 */
export function createMatcher(routes: Array<RouteConfig>): Matcher {
  const { pathMap, nameMap } = createRouteMap(routes);

  function match(
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    ...
    return _createRoute(null, location);
  }

  function redirect(record: RouteRecord, location: Location): Route {
    ...
  }

  function alias(
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {
    ...
  }

  function _createRoute(
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    ...
  }

  return match;
}

复制代码

上述代码首先根据传入的路由配置表,创建新的映射表并从中解构出路径映射表、名称映射表,之后返回内建函数 match 。这里暂时先不对其内部实现做详细介绍,之后再被调用处详细论述。

初始化

我们知道在 VueRouter - install 注册插件这个方法时,混入了一个全局的生命周期函数 beforeCreate , 代码如下:

export function install(Vue) {
  ...

  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      }
    }
  });

  ...

}
复制代码

我们发现其中执行了 vue-router 提供的 init 方法并传入 Vue 组件实例。那么接下来我们就来看看 init 做了哪些事情。

init 方法简析

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

  app: any; // vue 实例
  options: RouterOptions; // 路由配置
  mode: string; // 路由模式,默认 hash
  history: HashHistory | HTML5History | AbstractHistory;
  match: Matcher; // 一个数组,包含当前路由的所有嵌套路径片段的路由记录 。
  fallback: boolean; // 回退 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。
  beforeHooks: Array<?NavigationGuard>; // 前置钩子集合
  afterHooks: Array<?(to: Route, from: Route) => any>; // 后置钩子集合

  constructor(options: RouterOptions = {}) {
    ...
  }

  ...

  /**
   * 初始化
   * @param {Any} app Vue component instance
   */
  init(app: any) {
    // 断言有没有安装插件,如果没有抛出错误提示
    assert(
      install.installed,
      `没有安装。在创建根实例之前,请确保调用 Vue.use(VueRouter)。`
    );

    this.app = app;
    const { mode, options, fallback } = this;
    // 根据不同模式实例化不同基类,无效模式下抛出错误
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base);
        break;
      case 'hash':
        this.history = new HashHistory(this, options.base, fallback);
        break;
      case 'abstract':
        this.history = new AbstractHistory(this);
        break;
      default:
        assert(false, `invalid mode: ${mode}`);
    }

    // 执行父类监听函数,注册回调
    this.history.listen(route => {
      this.app._route = route;
    });
  }

  ...

}

复制代码

上述代码实现:

  • 首先断言有没有安装插件,如果没有抛出错误提示。
  • VueRouter 上添加 Vue 实例.
  • 根据不同模式实例化不同基类,无效模式下抛出错误
  • 执行父类监听函数,注册回调:
    • 在路由改变时替换 vue 实例上 _route 当前匹配的路由对象属性
    • 响应式值得改变,从而触发视图的重新渲染
    • <router-view> 中拿到匹配的路由对象,渲染匹配到的路由组件。完成跳转。

根据上述基础示例,选用的 history 模式。我们先看一下该模式的实现。

HTML5History

/**
 * h5 - history 模式
 *
 * @export
 * @class HTML5History
 * @extends {History}
 */
export class HTML5History extends History {
  constructor(router: VueRouter, base: ?string) {
    // 调用父类,并传入VueRouter路由实例和基础路径
    super(router, base);

    // 跳转核心方法 跳转到跟=基础路径
    this.transitionTo(getLocation(this.base));

    const expectScroll = router.options.scrollBehavior;
    // 添加 popstate 监听函数
    window.addEventListener('popstate', e => {
      _key = e.state && e.state.key;
      const current = this.current;
      this.transitionTo(getLocation(this.base), next => {
        if (expectScroll) {
          this.handleScroll(next, current, true);
        }
      });
    });

    // 若存在滚动行为配置,则添加 scroll 监听函数
    if (expectScroll) {
      window.addEventListener('scroll', () => {
        saveScrollPosition(_key);
      });
    }
  }

  /**
   * 前进对应步数
   *
   * @param {number} n
   * @memberof HTML5History
   */
  go(n: number) {
    // 通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面
    window.history.go(n);
  }

  /**
   * 导航到不同的 location 向 history 栈添加一个新的记录
   *
   * @param {RawLocation} location
   * @memberof HTML5History
   */
  push(location: RawLocation) {
    // 拿到当前路由对象
    const current = this.current;
    // 调用跳转核心方法
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath));
      this.handleScroll(route, current, false);
    });
  }

  /**
   * 导航到不同的 location 替换掉当前的 history 记录。
   *
   * @param {RawLocation} location
   * @memberof HTML5History
   */
  replace(location: RawLocation) {
    const current = this.current;
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath));
      this.handleScroll(route, current, false);
    });
  }

  /**
   * 更新 URL
   *
   * @memberof HTML5History
   */
  ensureURL() {
    if (getLocation(this.base) !== this.current.fullPath) {
      replaceState(cleanPath(this.base + this.current.fullPath));
    }
  }

  /**
   * 处理页面切换时,滚动位置
   *
   * @param {Route} to 将要去的路由对象
   * @param {Route} from 当前路由对象
   * @param {boolean} isPop 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用
   * @memberof HTML5History
   */
  handleScroll(to: Route, from: Route, isPop: boolean) {
    const router = this.router;
    // 若当前 Vue 组件实例不存在,直接return
    if (!router.app) {
      return;
    }

    // 取路由 滚动行为 配置参数。 若不存在直接 return
    // 使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样
    // 只在html5历史模式下可用; 默认没有滚动行为; 返回false以防止滚动.
    // { x: number, y: number }
    // { selector: string, offset? : { x: number, y: number }}
    const behavior = router.options.scrollBehavior;
    if (!behavior) {
      return;
    }
    // 断言 其必须是函数,否则抛出异常
    assert(typeof behavior === 'function', `scrollBehavior must be a function`);

    // 等到重新渲染完成后再滚动
    router.app.$nextTick(() => {
      // 获取滚动位置
      let position = getScrollPosition(_key);
      // 获取回调返回的滚动位置的对象信息
      const shouldScroll = behavior(to, from, isPop ? position : null);
      // 若不存在直接 return
      if (!shouldScroll) {
        return;
      }
      const isObject = typeof shouldScroll === 'object';
      // 处理模拟“滚动到锚点”的行为
      if (isObject && typeof shouldScroll.selector === 'string') {
        const el = document.querySelector(shouldScroll.selector);
        if (el) {
          position = getElementPosition(el);
        } else if (isValidPosition(shouldScroll)) {
          position = normalizePosition(shouldScroll);
        }
      } else if (isObject && isValidPosition(shouldScroll)) {
        position = normalizePosition(shouldScroll);
      }

      if (position) {
        // 把内容滚动到指定的坐标
        window.scrollTo(position.x, position.y);
      }
    });
  }
}
复制代码

HTML5History 实现简析:

  • 根据 init 方法 model: history 实例化 new HTML5History(this, options.base) 调用构造函数并传入 VueRouter 和 应用的基路径。
  • 构造函数
    • 调用父类 super(router, base) ,并传入 VueRouter 路由实例和基础路径;
    • 调用核心过渡跳转方法 跳转到应用的基路径;
    • 定义 popstate 监听函数, 并在回调里做相应跳转处理;
    • 若在实例化 VueRouter 传入滚动行为配置 scrollBehavior 则添加滚动监听事件。回调为:滚动到上次标记位置点。
  • 定义相应 Router 实例方法;和一些父类里调用子类实现的方法。

对于其提供的方法有很详细的注释信息,故接下来我们直接来看看父类的实现,其它两种模式也是继承了这个基类 History

History

/**
 * History 基类
 *
 * @export
 * @class History
 */
export class History {
  router: VueRouter;
  base: string;
  current: Route;
  pending: ?Route;
  cb: (r: Route) => void;

  // 以下这些方法由子类去实现
  go: (n: number) => void;
  push: (loc: RawLocation) => void;
  replace: (loc: RawLocation) => void;
  ensureURL: () => void; // 更新URL

  constructor(router: VueRouter, base: ?string) {
    // VueRouter 实例
    this.router = router
    // 应用的基路径
    this.base = normalizeBase(base)
    // 从一个表示 “nowhere” 的 route 对象开始
    this.current = START
    // 等待状态标志
    this.pending = null
  }

  /**
   * 注册回调
   *
   * @param {Function} cb
   * @memberof History
   */
  listen(cb: Function) {
    this.cb = cb;
  }

  /**
   * 核心跳转方法
   *
   * @param {RawLocation} location
   * @param {Function} [cb]
   * @memberof History
   */
  transitionTo(location: RawLocation, cb?: Function) { ... }

  // 最终过渡
  confirmTransition(route: Route, cb: Function) { ... }

  // 路由更新
  updateRoute(route: Route) { ... }
}

/**
 * 规范化应用的基路径
 *
 * @param {?string} base
 * @returns {string}
 */
function normalizeBase(base: ?string): string {
  if (!base) {
    if (inBrowser) {
      // respect <base> tag
      // HTML <base> 元素 指定用于一个文档中包含的所有相对 URL 的根 URL。一份中只能有一个 <base> 元素。
      const baseEl = document.querySelector('base')
      base = baseEl ? baseEl.getAttribute('href') : '/'
    } else {
      base = '/'
    }
  }
  // 确保有开始斜杠
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  // 去除末尾斜杠
  return base.replace(/\/$/, '')
}

复制代码
  • 上述是 History 基类中所有代码实现。同样是添加几个属性,和一些跳转所需的核心方法。这里只需要大概了解其内部大概实现,之后会详细论述。
  • 回到 HTML5History 构造函数的执行代码 this.transitionTo(getLocation(this.base)) ,调用核心过渡跳转方法 跳转到应用的基路径。

核心跳转方法 transitionTo

根据上述例子初始化时调用( HTML5History - this.transitionTo(getLocation(this.base)); ),这里的入参是: location: /, cb: undefined

/**
  * 核心跳转方法
  *
  * @param {RawLocation} location 地址
  * @param {Function} [cb] 回调
  * @memberof History
  */
transitionTo(location: RawLocation, cb?: Function) {
  // 获取路由匹配信息,传入 location 和 current属性
  const route = this.router.match(location, this.current)
  // 调用最终跳转方法,并传入路由对象信息,和回调
  // 回调:更新路由,执行传入回调, 更新 URL
  this.confirmTransition(route, () => {
    this.updateRoute(route)
    cb && cb(route)
    this.ensureURL()
  })
}

复制代码

根据上述代码实现简析:

  • 传入地址和 current 属性, current 属性在调用 super 在父类里初始时被赋值为 START

    // vue-router/src/history/base.js
    // 从一个表示 “nowhere” 的 route 对象开始
    this.current = START;
    复制代码
    START 代码实现及结果展示

    START

    // 表示初始状态的起始路径
    export const START = createRoute(null, {
      path: '/'
    });
    
    /**
     * 创建一个路由对象
     *
     * @export
     * @param {?RouteRecord} record
     * @param {Location} location
     * @param {Location} [redirectedFrom]
     * @returns {Route}
     */
    export function createRoute(
      record: ?RouteRecord,
      location: Location,
      redirectedFrom?: Location
    ): Route {
      const route: Route = {
        name: location.name || (record && record.name),
        meta: (record && record.meta) || {},
        path: location.path || '/',
        hash: location.hash || '',
        query: location.query || {},
        params: location.params || {},
        fullPath: getFullPath(location),
        matched: record ? formatMatch(record) : []
      };
      // 这里暂时不说
      if (redirectedFrom) {
        route.redirectedFrom = getFullPath(redirectedFrom);
      }
      return Object.freeze(route);
    }
    
    /**
     * 格式化匹配
     *
     * @param {?RouteRecord} record
     * @returns {Array<RouteRecord>}
     */
    function formatMatch(record: ?RouteRecord): Array<RouteRecord> {
      const res = [];
      while (record) {
        res.unshift(record);
        record = record.parent;
      }
      return res;
    }
    
    /**
     * 获取完整路径
     *
     * @param {*} { path, query = {}, hash = '' }
     * @returns
     */
    function getFullPath({ path, query = {}, hash = '' }) {
      return (path || '/') + stringifyQuery(query) + hash;
    }
    复制代码

    START 结果如下:

    START = {
        fullPath: "/",
        hash: "",
        matched: [],
        meta: {},
        name: null,
        params: {},
        path: "/",
        query: {},
        __proto__: Object,
        ,
      }
    复制代码
  • 首先调用 VueRouter 类的 match 属性, 该属性在初始化 VueRouter 被赋值为一个方法。

    // vue-router/src/index.js
    this.match = createMatcher(options.routes || []);
    复制代码
    createMatcher 生成 match 的代码实现

    createMatcher 实现

    • 首先调用 createRouteMap 传入路由映射表,解构出路径、名称映射表
    • 定义内建方法 match redirect _createRoute 最后 return match
    /**
      * 创建匹配器
      *
      * @export
      * @param {Array<RouteConfig>} routes
      * @returns {Matcher}
      */
      export function createMatcher(routes: Array<RouteConfig>): Matcher {
    
        const { pathMap, nameMap } = createRouteMap(routes);
    
        // 匹配函数
        function match(
          raw: RawLocation,
          currentRoute?: Route,
          redirectedFrom?: Location
        ): Route {
          ...
          return _createRoute(null, location);
        }
    
        // 重定向处理函数
        function redirect(record: RouteRecord, location: Location): Route {
          ...
        }
    
        // 别名处理函数
        function alias(
          record: RouteRecord,
          location: Location,
          matchAs: string
        ): Route {
          ...
        }
    
        // 路由信息生成函数
        function _createRoute(
          record: ?RouteRecord,
          location: Location,
          redirectedFrom?: Location
        ): Route {
          ...
        }
    
        return match;
      }
    
    复制代码

    createRouteMap

    • 首先创建 name、path 映射对象
    • 对路由表内部每一项进行处理
    • 最终返回包含 路径/名称的映射表
    /**
     * 创建路由映射表
     *
     * @export
     * @param {Array<RouteConfig>} routes
     * @returns {{
     *   pathMap: Dictionary<RouteRecord>,
     *   nameMap: Dictionary<RouteRecord>
     * }}
     */
    export function createRouteMap(
      routes: Array<RouteConfig>
    ): {
      pathMap: Dictionary<RouteRecord>,
      nameMap: Dictionary<RouteRecord>
    } {
      const pathMap: Dictionary<RouteRecord> = Object.create(null);
      const nameMap: Dictionary<RouteRecord> = Object.create(null);
    
      // 对路由表内部每一项进行处理
      routes.forEach(route => {
        addRouteRecord(pathMap, nameMap, route);
      });
    
      return {
        pathMap,
        nameMap
      };
    }
    复制代码

    addRouteRecord

    /**
     * 添加路由记录
     *
     * @param {Dictionary<RouteRecord>} pathMap 路径映射表
     * @param {Dictionary<RouteRecord>} nameMap 名称映射表
     * @param {RouteConfig} route 路由项
     * @param {RouteRecord} [parent] 父路由项
     * @param {string} [matchAs]
     */
    function addRouteRecord(
      pathMap: Dictionary<RouteRecord>,
      nameMap: Dictionary<RouteRecord>,
      route: RouteConfig,
      parent?: RouteRecord,
      matchAs?: string
    ) {
      // 解构路径和名称,若路径不存在,则抛出异常
      const { path, name } = route;
      assert(path != null, `在路由配置中需要“path”。`);
    
      // 定义路由记录构建选项
      const record: RouteRecord = {
        path: normalizePath(path, parent), // 规范化之后的路径
        components: route.components || { default: route.component }, // 路由组件
        instances: {},
        name, // 路由的名称
        parent, // 父路由
        matchAs,
        redirect: route.redirect, // 重定向
        beforeEnter: route.beforeEnter, // 进入前钩子函数,形如:(to: Route, from: Route, next: Function) => void;
        meta: route.meta || {} // 路由元信息
      };
    
      // 是否存在嵌套路由
      if (route.children) {
        // 如果路由已命名并具有默认子路由,则发出警告。
        // 如果用户按名称导航到此路由,则不会呈现默认的子节点(GH问题#629)。
        if (process.env.NODE_ENV !== 'production') {
          if (
            route.name &&
            route.children.some(child => /^\/?$/.test(child.path)) // 匹配空字符串
          ) {
            warn(
              false,
              `命名路由'${route.name}'有一个默认的子路由。
                当导航到这个命名路由(:to="{name: '${
                  route.name
                }'")时,将不会呈现默认的子路由。
                从此路由中删除该名称,并使用已命名链接的默认子路由的名称。`
            );
          }
        }
        // 若存在子路由,递归处理子路由表
        route.children.forEach(child => {
          addRouteRecord(pathMap, nameMap, child, record);
        });
      }
    
      // 是否存在别名配置 string | Array<string>
      if (route.alias) {
        // 处理数组情况
        if (Array.isArray(route.alias)) {
          // 递归处理别名配置项
          route.alias.forEach(alias => {
            addRouteRecord(
              pathMap,
              nameMap,
              { path: alias },
              parent,
              record.path
            );
          });
        } else {
          addRouteRecord(
            pathMap,
            nameMap,
            { path: route.alias },
            parent,
            record.path
          );
        }
      }
    
      // 分别项路径,名称映射表里新增记录
      pathMap[record.path] = record;
      if (name) nameMap[name] = record;
    }
    
    /**
     * 规范化路径
     *
     * @param {string} path
     * @param {RouteRecord} [parent]
     * @returns {string}
     */
    function normalizePath(path: string, parent?: RouteRecord): string {
      path = path.replace(/\/$/, ''); // 替换字符结尾为'/' => '' 如:'/foo/' => '/foo'
      if (path[0] === '/') return path;
      if (parent == null) return path;
      return cleanPath(`${parent.path}/${path}`); // 替换 '//' => '/' 如:'router//foo//' => 'router/foo/'
    }
    复制代码
    • 上述代码主要对路由配置的每一项进行处理,最终写入相应的路径,名称映射表。

    • 上述基础示例代码路由配置项处理之后为:

      pathMap:{
          '': {
            beforeEnter: undefined,
            components: {
              default: { template: "<div>home</div>" },
              __proto__: Object
            },
            instances: {},
            matchAs: undefined,
            meta: {},
            name: undefined,
            parent: undefined,
            path: "",
            redirect: undefined,
            __proto__: Object,
          },
          '/bar': {
            beforeEnter: undefined,
            components: {
              default: {template: "<div>bar</div>"},
              __proto__: Object
            },
            instances: {},
            matchAs: undefined,
            meta: {},
            name: undefined,
            parent: undefined,
            path: "/bar",
            redirect: undefined,
            __proto__: Object
          },
          '/bar/child': {
            beforeEnter: undefined,
            components: {
              default: {template: "<div>Child</div>"},
              __proto__: Object
            },
            instances: {},
            matchAs: undefined,
            meta: {},
            name: undefined,
            parent: {path: "/bar", ... },
            path: "/bar/child",
            redirect: undefined,
            __proto__: Object
          },
          '/foo': {
            beforeEnter: undefined,
            components: {
              default: {template: "<div>foo</div>"},
              __proto__: Object
            },
            instances: {},
            matchAs: undefined,
            meta: {},
            name: undefined,
            parent: undefined,
            path: "/foo",
            redirect: undefined,
            __proto__: Object
          }
        }
      
        nameMap:{}
      
      复制代码

    知道了 const { pathMap, nameMap } = createRouteMap(routes); 解构的实现及结果,咱们继续看 match 的代码实现

    function match(
      raw: RawLocation,
      currentRoute?: Route,
      redirectedFrom?: Location
    ): Route {
      const location = normalizeLocation(raw, currentRoute);
      const { name } = location;
    
      if (name) {
        // 若存在名称,从名称映射表中取对应记录
        const record = nameMap[name];
        if (record) {
          // 处理路径
          location.path = fillParams(
            record.path,
            location.params,
            `named route "${name}"`
          );
          return _createRoute(record, location, redirectedFrom);
        }
      } else if (location.path) {
        location.params = {};
        for (const path in pathMap) {
          if (matchRoute(path, location.params, location.path)) {
            return _createRoute(pathMap[path], location, redirectedFrom);
          }
        }
      }
      // 没有匹配直接传入 null。
      return _createRoute(null, location);
    }
    复制代码
    • 规范化目标路由的链接

      normalizeLocation 代码实现

      normalizeLocation

      /**
       * 规范化目标路由的链接
       *
       * @export
       * @param {RawLocation} raw 目标路由的链接
       * @param {Route} [current] 当前路由
       * @param {boolean} [append] 是否在当前 (相对) 路径前添加基路径
       * @returns {Location}
       */
      export function normalizeLocation(
        raw: RawLocation,
        current?: Route,
        append?: boolean
      ): Location {
        // 处理目标路由的链接(to),我们知道其支持多种写法
        // 'home'
        // { path: 'home' }
        // { path: `/user/${userId}` }
        // { name: 'user', params: { userId: 123 }}
        // { path: 'register', query: { plan: 'private' }}
        const next: Location = typeof raw === 'string' ? { path: raw } : raw;
        // 若已经被规范化或存在name属性直接返回 next
        if (next.name || next._normalized) {
          return next;
        }
        // 解析路径 返回 { path, query, hash }
        const parsedPath = parsePath(next.path || '');
        // current.path - 字符串,对应当前路由的路径,总是解析为绝对路径
        const basePath = (current && current.path) || '/';
        // 获取最终路径地址
        const path = parsedPath.path
          ? resolvePath(parsedPath.path, basePath, append)
          : (current && current.path) || '/';
        // 获取查询参数
        const query = resolveQuery(parsedPath.query, next.query);
        // 当前路由的 hash 值 (带 #) ,如果没有 hash 值,则为空字符串
        let hash = next.hash || parsedPath.hash;
        if (hash && hash.charAt(0) !== '#') {
          hash = `#${hash}`;
        }
      
        return {
          _normalized: true,
          path,
          query,
          hash
        };
      }
      复制代码
      • normalizeLocation 所涉及的函数调用的代码实现

        parsePath 代码实现

        解析路径

        /**
         * 解析路径
         *
         * @export
         * @param {string} path
         * @returns {{
         *   path: string;
         *   query: string;
         *   hash: string;
         * }}
         */
        export function parsePath(
          path: string
        ): {
          path: string,
          query: string,
          hash: string
        } {
          let hash = '';
          let query = '';
        
          // 是否存在 #
          const hashIndex = path.indexOf('#');
          if (hashIndex >= 0) {
            hash = path.slice(hashIndex); // 截取 hash 值
            path = path.slice(0, hashIndex); // 截取路径
          }
        
          // 是否存在查询参数
          const queryIndex = path.indexOf('?');
          if (queryIndex >= 0) {
            query = path.slice(queryIndex + 1); // 截取参数
            path = path.slice(0, queryIndex); // 截取路径
          }
        
          return {
            path,
            query,
            hash
          };
        }
        复制代码
        resolvePath 代码实现

        导出处理之后的路径地址

        /**
         * 导出处理之后的路径地址
         *
         * @export
         * @param {string} relative 相对路径
         * @param {string} base 基础路径
         * @param {boolean} [append] 是否在当前 (相对) 路径前添加基路径
         * @returns {string}
         */
        export function resolvePath(
          relative: string,
          base: string,
          append?: boolean
        ): string {
          if (relative.charAt(0) === '/') {
            return relative;
          }
        
          if (relative.charAt(0) === '?' || relative.charAt(0) === '#') {
            return base + relative;
          }
        
          // '/vue-router/releases' => ["", "vue-router", "releases"]
          const stack = base.split('/');
        
          // 删除后段
          // - 没有附加
          // - 附加到尾随斜杠(最后一段为空)
          if (!append || !stack[stack.length - 1]) {
            stack.pop();
          }
        
          // resolve 相对路径
          // '/vue-router/releases'.replace(/^\//, '') => "vue-router/releases"
          // 'vue-router/releases'.split('/') => ["vue-router", "releases"]
          const segments = relative.replace(/^\//, '').split('/');
          for (let i = 0; i < segments.length; i++) {
            const segment = segments[i];
            if (segment === '.') {
              continue;
            } else if (segment === '..') {
              stack.pop();
            } else {
              stack.push(segment);
            }
          }
        
          // 确保领先的削减 ensure leading slash
          if (stack[0] !== '') {
            stack.unshift('');
          }
        
          return stack.join('/');
        }
        复制代码
        resolveQuery 代码实现

        导出处理之后的路径地址

        /**
         * 导出查询参数
         *
         * @export
         * @param {?string} query
         * @param {Dictionary<string>} [extraQuery={}]
         * @returns {Dictionary<string>}
         */
        export function resolveQuery(
          query: ?string,
          extraQuery: Dictionary<string> = {}
        ): Dictionary<string> {
          if (query) {
            let parsedQuery;
            try {
              parsedQuery = parseQuery(query);
            } catch (e) {
              warn(false, e.message);
              parsedQuery = {};
            }
            for (const key in extraQuery) {
              parsedQuery[key] = extraQuery[key];
            }
            return parsedQuery;
          } else {
            return extraQuery;
          }
        }
        
        /**
         * 解析查询参数
         *
         * @param {string} query
         * @returns {Dictionary<string>}
         */
        function parseQuery(query: string): Dictionary<string> {
          const res = Object.create(null);
        
          // 匹配 ?、#、& 开头的字符串 如:'?id=1'.match(/^(\?|#|&)/) => ["?", "?", index: 0, input: "?id=1", groups: undefined]
          // '?id=1&name=cllemon'.replace(/^(\?|#|&)/, '') => id=1&name=cllemon
          query = query.trim().replace(/^(\?|#|&)/, '');
        
          if (!query) {
            return res;
          }
        
          // 如上例: => ["id=1", "name=cllemon"]
          query.split('&').forEach(param => {
            // 匹配 ”+“
            // 如上例:"id=1" => ["id", "1"]
            const parts = param.replace(/\+/g, ' ').split('=');
            // 如上例:["id", "1"] => 'id'
            // 解码由 decode 等于 decodeURIComponent() 方法用于 encodeURIComponent 方法或者其它类似方法编码的部分统一资源标识符(URI)。
            const key = decode(parts.shift());
            // 如上例:["1"]
            const val = parts.length > 0 ? decode(parts.join('=')) : null;
        
            if (res[key] === undefined) {
              res[key] = val;
            } else if (Array.isArray(res[key])) {
              res[key].push(val);
            } else {
              res[key] = [res[key], val];
            }
          });
        
          return res;
        }
        复制代码
    • fillParams 填充参数

      const regexpCompileCache: {
        [key: string]: Function
      } = Object.create(null);
      
      /**
       * 填充参数
       *
       * @param {string} path
       * @param {?Object} params
       * @param {string} routeMsg
       * @returns {string}
       */
      function fillParams(
        path: string,
        params: ?Object,
        routeMsg: string
      ): string {
        try {
          // 第三方库 path-to-regexp: 将路径字符串(例如`/user/:name`)转换为正则表达式
          // compile : 用于将字符串转换为有效路径。
          // 如: const toPath = Regexp.compile('/user/:id')
          //     toPath({ id: 123 }) //=> "/user/123"
          const filler =
            regexpCompileCache[path] ||
            (regexpCompileCache[path] = Regexp.compile(path));
          return filler(params || {}, { pretty: true });
        } catch (e) {
          assert(false, `missing param for ${routeMsg}: ${e.message}`);
          return '';
        }
      }
      复制代码
    • _createRoute 根据不同的配置项信息调用不同的路由创建处理函数

      function _createRoute(
        record: ?RouteRecord,
        location: Location,
        redirectedFrom?: Location
      ): Route {
        // 重定向处理函数
        if (record && record.redirect) {
          return redirect(record, redirectedFrom || location);
        }
        // 别名处理处理函数
        if (record && record.matchAs) {
          return alias(record, location, record.matchAs);
        }
        // createRoute 在 “START 代码实现及结果展示” 中以有论述
        return createRoute(record, location, redirectedFrom);
      }
      复制代码

最终 route (通过 match 函数返回的值) 值为:

route = {
  fullPath: '/',
  hash: '',
  matched: [
    {
      beforeEnter: undefined,
      components: {
        default: {
          template: '<div>home</div>'
        }
      },
      instances: {},
      matchAs: undefined,
      meta: {},
      name: undefined,
      parent: undefined,
      path: '',
      redirect: undefined
    }
  ],
  meta: {},
  name: undefined,
  params: {},
  path: '/',
  query: {},
  __proto__: Object
};
复制代码

最终跳转方法 confirmTransition

/**
 * 确认跳转
 *
 * @param {Route} route 目录路由信息
 * @param {Function} cb
 * @memberof History
 */
confirmTransition(route: Route, cb: Function) {
  const current = this.current
  // 是不是相同路由
  if (isSameRoute(route, current)) {
    this.ensureURL()
    return
  }

  const {
    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,
    // enter guards beforeEnter: (to, from, next) => {}
    activated.map(m => m.beforeEnter),
    // 异步组件
    resolveAsyncComponents(activated)
  )

  this.pending = route
  // 迭代方法
  const iterator = (hook: NavigationGuard, next) => {
    if (this.pending !== route) return
    // route: 即将要进入的目标 路由对象 current: 当前导航正要离开的路由 next: 调用该方法来 resolve 这个钩子
    hook(route, current, (to: any) => {
      // to === false: 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
      if (to === false) {
        // next(false) -> abort navigation, ensure current URL
        this.ensureURL()
      } else if (typeof to === 'string' || typeof to === 'object') {
        // next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
        // next('/') or next({ path: '/' }) -> redirect
        this.push(to)
      } else {
        // 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
        // 确认转换并传递值
        next(to)
      }
    })
  }
  // 队列执行函数
  runQueue(queue, iterator, () => {
    const postEnterCbs = []
    // 在提取组件内的 enter 守卫之前,请等待异步组件被解析
    runQueue(extractEnterGuards(activated, postEnterCbs), iterator, () => {
      if (this.pending === route) {
        this.pending = null
        cb(route)
        this.router.app.$nextTick(() => {
          postEnterCbs.forEach(cb => cb())
        })
      }
    })
  })
}

复制代码
  • 到此,路由核心基类已经全部梳理完毕

  • 上述代码逻辑很清晰,有详细的注释,这里直接略过文字描述,上述代码所涉及的函数调用 :point_down::

    resolveQueue 的代码实现
    /**
     * 比对当前路由和目标路由,导出失活和激活路由信息
     *
     * @param {Array<RouteRecord>} current
     * @param {Array<RouteRecord>} next
     * @returns {{
     *   activated: Array<RouteRecord>,
     *   deactivated: Array<RouteRecord>
     * }}
     */
    function resolveQueue(
      current: Array<RouteRecord>,
      next: Array<RouteRecord>
    ): {
      activated: Array<RouteRecord>,
      deactivated: Array<RouteRecord>
    } {
      let i;
      const max = Math.max(current.length, next.length);
      for (i = 0; i < max; i++) {
        if (current[i] !== next[i]) {
          break;
        }
      }
      return {
        activated: next.slice(i),
        deactivated: current.slice(i)
      };
    }
    复制代码
    extractLeaveGuards 的代码实现
    /**
     * 提取离开的路由对象
     *
     * @param {Array<RouteRecord>} matched
     * @returns {Array<?Function>}
     */
    function extractLeaveGuards(matched: Array<RouteRecord>): Array<?Function> {
      // 返回反转之后的数组元素
      return flatMapComponents(matched, (def, instance) => {
        // 提取匹配路由的组件路由守卫钩子函数
        const guard = def && def.beforeRouteLeave;
        if (guard) {
          return function routeLeaveGuard() {
            return guard.apply(instance, arguments);
          };
        }
      }).reverse();
    }
    复制代码
    resolveAsyncComponents 的代码实现
    /**
     * 加载异步组件
     *
     * @param {Array<RouteRecord>} matched
     * @returns {Array<?Function>}
     */
    function resolveAsyncComponents(
      matched: Array<RouteRecord>
    ): Array<?Function> {
      return flatMapComponents(matched, (def, _, match, key) => {
        // 如果它是一个函数并且没有附加 Vue 选项,
        // 那么假设它是一个异步组件解析函数
        // 我们没有使用 Vue 的默认异步解析机制
        // 因为我们希望在解析传入组件之前停止导航。
        if (typeof def === 'function' && !def.options) {
          return (to, from, next) => {
            const resolve = resolvedDef => {
              match.components[key] = resolvedDef;
              next();
            };
    
            const reject = reason => {
              warn(false, `Failed to resolve async component ${key}: ${reason}`);
              next(false);
            };
    
            const res = def(resolve, reject);
            if (res && typeof res.then === 'function') {
              res.then(resolve, reject);
            }
          };
        }
      });
    }
    复制代码
    runQueue 的代码实现
    /**
     * 执行对列
     *
     * @export
     * @param {Array<?NavigationGuard>} queue
     * @param {Function} fn
     * @param {Function} cb
     */
    export function runQueue(
      queue: Array<?NavigationGuard>,
      fn: Function,
      cb: Function
    ) {
      const step = index => {
        if (index >= queue.length) {
          cb();
        } else {
          if (queue[index]) {
            fn(queue[index], () => {
              step(index + 1);
            });
          } else {
            step(index + 1);
          }
        }
      };
      step(0);
    }
    复制代码
    extractEnterGuards 的代码实现
    /**
     * 提取进入的路由对象
     *
     * @param {Array<RouteRecord>} matched
     * @param {Array<Function>} cbs
     * @returns {Array<?Function>}
     */
    function extractEnterGuards(
      matched: Array<RouteRecord>,
      cbs: Array<Function>
    ): Array<?Function> {
      return flatMapComponents(matched, (def, _, match, key) => {
        // 提取组件内的守卫
        // 如:beforeRouteEnter (to, from, next) {}
        // 在渲染该组件的对应路由被 confirm 前调用,  不!能!获取组件实例 `this`, 因为当守卫执行前,组件实例还没被创建.
        const guard = def && def.beforeRouteEnter;
        if (guard) {
          return function routeEnterGuard(to, from, next) {
            // 不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
            // 如:next(vm => { // 通过 `vm` 访问组件实例 })
            return guard(to, from, cb => {
              next(cb);
              if (typeof cb === 'function') {
                cbs.push(() => {
                  cb(match.instances[key]);
                });
              }
            });
          };
        }
      });
    }
    复制代码
/**
  * 导航到不同的 location 向 history 栈添加一个新的记录
  *
  * @param {RawLocation} location
  * @memberof HTML5History
  */
  push(location: RawLocation) {
    // 拿到当前路由对象
    const current = this.current
    // 调用跳转核心方法
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      this.handleScroll(route, current, false)
    })
  }
复制代码
  • 最后我们以路由实例的跳转方法( push )来梳理一下路由跳转的过程:

    • 首先拿到当前路由信息。

    • 调用 History 提供的 transitionTo 过渡跳转的方法,传入要跳转的路径信息 location (由上文分析,此处是一个经过规范化之后的路径信息),以及一个参数为路由信息的回调函数(主要用来更新浏览器 URL 和处理用户滚动行为的方法「前文已分析」)。

    • transitionTo 方法上拿到目标路径匹配的路由信息,调用 confirmTransition 并传入匹配路由信息及一个回调函数,该回调函数主要做:调用 updateRoute 更新路由,执行 transitionTo 内传入的回调,以及更新 URL。

    • confirmTransition 内首先过滤掉相同路由;然后调用 resolveQueue 方法传入当前路由匹配信息及目标路由匹配信息解构出 deactivated, activated ; 然后构造执行队列(处理全局钩子函数,异步组件解析等);然后定义执行方法;最后执行队列执行函数(具体实现看上述代码解析)。最终在队列内执行 transitionTo 传入的回调,更新路由,执行监听函数:更新 URL ;更新 vue 根实例的 _route 属性,由值的改变,进而触发视图的重新渲染 在 中拿到匹配的路由对象,渲染匹配到的路由组件。完成跳转。

注册的两个全局组件: <router-view><router-link>

<router-view>

  • <router-view> 组件是一个 functional 组件,渲染路径匹配到的视图组件。 <router-view> 渲染的组件还可以内嵌自己的 <router-view> ,根据嵌套路径,渲染嵌套组件。
  • Props: 默认值: "default"; 如果 <router-view> 设置了名称,则会渲染对应的路由配置中 components 下的相应组件。
export default {
  name: 'router-view',

  functional: true, // functional 组件

  props: {
    // 用于匹配渲染对应的路由配置中 components 下的相应组件
    name: {
      type: String,
      default: 'default'
    }
  },

  render(h, { props, children, parent, data }) {
    data.routerView = true;

    const route = parent.$route;
    const cache = parent._routerViewCache || (parent._routerViewCache = {});
    let depth = 0;
    let inactive = false; // 是否失活

    // 循环寻找父节点,找到当前组件嵌套深度
    while (parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++;
      }
      // 处理 keep-alive _inactive vue 内部属性
      if (parent._inactive) {
        inactive = true;
      }
      parent = parent.$parent;
    }

    // 设置当前 router-view 所属层级
    data.routerViewDepth = depth;
    // 获取对应层级的匹配项
    const matched = route.matched[depth];
    // 若不存在,直接渲染为空
    if (!matched) {
      return h();
    }

    // 若失活,直接从缓存中取
    const component = inactive
      ? cache[props.name]
      : (cache[props.name] = matched.components[props.name]);

    // keep-alive 非失活组件,
    if (!inactive) {
      // 添加钩子函数,更新匹配的组件实例
      (data.hook || (data.hook = {})).init = vnode => {
        debugger;
        matched.instances[props.name] = vnode.child;
      };
    }

    return h(component, data, children);
  }
};
复制代码

<router-link>

  • <router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 <a> 标签,可以通过配置 tag 属性生成别的标签。
  • <router-link> 比起写死的 <a href="..."> 会好一些。
  • 更多参阅 vue-router 文档。
/* @flow */

import { cleanPath } from '../util/path';
import { createRoute, isSameRoute, isIncludedRoute } from '../util/route';
import { normalizeLocation } from '../util/location';

// 解决奇怪的 flow bug
const toTypes: Array<Function> = [String, Object];

export default {
  name: 'router-link',
  props: {
    // 目标路由的链接
    to: {
      type: toTypes,
      required: true
    },
    // 标签名称
    tag: {
      type: String,
      default: 'a'
    },
    // 是否激活
    exact: Boolean,
    // 是否在当前 (相对) 路径前添加基路径 如:/a => /b (true:/a/b; false: /b)
    append: Boolean,
    // true: 当点击时,会调用 router.replace() 而不是 router.push() 不会留下 history 记录
    replace: Boolean,
    // 链接激活时使用的 CSS 类名
    activeClass: String
  },

  render(h: Function) {
    const router = this.$router;
    const current = this.$route;
    // 规范化 目标路由的链接
    const to = normalizeLocation(this.to, current, this.append);
    const resolved = router.match(to);
    const fullPath = resolved.redirectedFrom || resolved.fullPath;
    const base = router.history.base;
    const href = base ? cleanPath(base + fullPath) : fullPath;
    const classes = {};
    const activeClass =
      this.activeClass ||
      router.options.linkActiveClass ||
      'router-link-active';
    const compareTarget = to.path ? createRoute(null, to) : resolved;
    classes[activeClass] = this.exact
      ? isSameRoute(current, compareTarget)
      : isIncludedRoute(current, compareTarget);

    const on = {
      click: e => {
        // 阻止浏览器默认行为 防止a跳转
        e.preventDefault();
        if (this.replace) {
          router.replace(to);
        } else {
          router.push(to);
        }
      }
    };

    const data: any = {
      class: classes
    };

    if (this.tag === 'a') {
      data.on = on;
      data.attrs = { href };
    } else {
      // find the first <a> child and apply listener and href
      const a = findAnchor(this.$slots.default);
      if (a) {
        const aData = a.data || (a.data = {});
        aData.on = on;
        const aAttrs = aData.attrs || (aData.attrs = {});
        aAttrs.href = href;
      }
    }

    return h(this.tag, data, this.$slots.default);
  }
};

function findAnchor(children) {
  if (children) {
    let child;
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      if (child.tag === 'a') {
        return child;
      }
      if (child.children && (child = findAnchor(child.children))) {
        return child;
      }
    }
  }
}
复制代码

结语

最后的最后, Hash Abstract 两种模式都是依托于 History 基类去实现,这里就不做深入分析了。 若对此感兴趣请参阅 vue-router

更多细节部分,建议直接把 vue-router 项目拉下来,本地跑起来,根据 vue-router 提供的用例分析,一定收获满满。


以上所述就是小编给大家介绍的《vue-router 源代码全流程分析「长文」》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Algorithmic Beauty of Plants

The Algorithmic Beauty of Plants

Przemyslaw Prusinkiewicz、Aristid Lindenmayer / Springer / 1996-4-18 / USD 99.00

Now available in an affordable softcover edition, this classic in Springer's acclaimed Virtual Laboratory series is the first comprehensive account of the computer simulation of plant development. 150......一起来看看 《The Algorithmic Beauty of Plants》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具