vue-router 源代碼全流程分析「長文」

說明

  • 如下內容均是依託 vue-router 2.0.0 版本展開分析。
  • 此篇文章,是本身對 vue-router 實現分析的記錄性文章,若有任何錯誤,歡迎指正,相互交流。

基礎使用

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');
複製代碼

根據上述基礎使用,咱們大概能夠梳理一個基本流程出來:html

  • 註冊插件:
    • $router$route 注入全部啓用路由的子組件。
    • 安裝 <router-view><router-link>
  • 定義路由組件。
  • new VueRouter(options) 建立一個路由器, 傳入相關配置。
  • 建立並掛載根實例,確保注入路由器。路由組件將在 <router-view> 中呈現。

下面咱們就根據上述流程步驟,一步一步解析,vue-router 代碼實現。前端

註冊插件(vue-router)

首先 Vue.use(VueRouter); 這段代碼間接執行 VueRouter 暴露的 install 方法,下面來看看 install 具體實現:(因爲有完整的註釋,故略去文字敘述)vue

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 的不少方法。html5

代碼接着執行,接下來就是爲 vue-router 裝填配置並實例化。以後把實例化的結果傳入 Vuenode

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 構造函數。git

定義路由: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 方法註冊插件。github

其中對相關代碼作了詳細的註釋,無需再此重複論述,但其中有一些方法將在後面涉及時,作深刻分析。繼續分析前,這裏須要先對這段代碼進行解釋:正則表達式

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。這裏暫時先不對其內部實現作詳細介紹,以後再被調用處詳細論述。vue-router

初始化

咱們知道在 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())
        })
      }
    })
  })
}

複製代碼
  • 到此,路由核心基類已經所有梳理完畢

  • 上述代碼邏輯很清晰,有詳細的註釋,這裏直接略過文字描述,上述代碼所涉及的函數調用 👇:

    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;
      }
    }
  }
}
複製代碼

結語

最後的最後,HashAbstract 兩種模式都是依託於 History 基類去實現,這裏就不作深刻分析了。 若對此感興趣請參閱 vue-router

更多細節部分,建議直接把 vue-router 項目拉下來,本地跑起來,根據 vue-router 提供的用例分析,必定收穫滿滿。

參閱

相關文章
相關標籤/搜索