Анализ исходного кода vue-router |1.4w слов |

Vue.js
Анализ исходного кода vue-router |1.4w слов |

Анализ исходного кода Vue-router | Слово 1.3w | Предупреждение о нескольких изображениях - [Часть 1]

  • Всем привет, я сияю 😎
  • vue-routerкаждыйvue开发者Плагин, который вступит в контакт с
  • В соответствии с концепцией отслеживания источника я открылvue-routerПуть к анализу исходного кода 😂
  • Я работаю сверхурочно каждый день, так что прошло несколько месяцев до и после
  • Время затянулось надолго, поэтому могут быть нестыковки в размышлениях, и надеюсь вы меня простите🤣
  • Из-за лимита персонажей Наггетс разделен на три части: верхнюю, среднюю и нижнюю.
  • Последняя статья — это статья, которая в основном знакомит с идеями дизайна и основными принципами реализации интерфейсной маршрутизации.vue-routerсвязанная терминология, структура каталогов, установка, создание экземпляра, реализация, связанная с инициализацией
  • В средней части представлен механизм сопоставления маршрутов и срабатывания навигационной защиты его основных функций.
  • Следующая часть — это заключение, представляющее некоторые неосновные функции, такие как прокрутка,view,linkРеализация компонента
  • Это первый раз, когда я делаю анализ исходного кода, должно быть много ошибок или недоразумений, пожалуйста, поправьте меня🤞
  • адрес проекта
    • https://github.com/BryanAdamss/vue-router-for-analysis
    • Если вы думаете, что это поможет вам, не забудьте дать мнеstar
  • исходный файл uml
    • https://github.com/BryanAdamss/vue-router-for-analysis/blob/dev/vue-router.EAP
  • Ссылки на связанные статьи

Идеи дизайна

  • разборvue-routerПеред этой библиотекой маршрутизации нам нужно понять, что такое маршрутизация и что такое маршрутизация переднего плана? Каковы обычные процедуры реализации внешней маршрутизации?

Что такое Frontend-маршрутизация

  • Что касается определения маршрутизации, вики определяет его следующим образом;路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。路由引导分组转送,经过一些中间的节点后,到它们最后的目的地。作成硬件的话,则称为路由器。路由通常根据路由表——一个存储到各个目的地的最佳路径的表——来引导分组转送
  • Приведенное выше определение может быть официальным, но мы можем выделить некоторые важные моменты.
    • Маршрутизация — это действие, отвечающее за передачу информации с адреса источника на адрес назначения;
    • Для выполнения такой деятельности требуется очень важная вещьтаблица маршрутизации- Таблица сопоставления исходного и конечного адресов
  • 在web后台开发中,“route”是指根据url分配到对应的处理程序。; Цитата изhttps://www.zhihu.com/question/46767015@Хе Шиджун
    • Пользователь вводит URL-адрес, браузер передает его на сервер, сервер сопоставляет таблицу сопоставления, находит соответствующий обработчик и возвращает соответствующий ресурс (страницу или другое);
  • Для фронтенда появление концепции маршрутизации сопровождаетсяspaпоявляющийся; вspaДо его появления переход по страницам (навигация) управлялся сервером, и при переходе был явный процесс перехода на белый экран;spaПосле того, как он появился, для лучшего опыта сервер больше не контролировал переход, поэтому появилась фронтенд-роутинг, и фронтенд мог свободно управлять отрисовкой компонентов для имитации переходов по страницам.
  • резюме
    • маршрутизация на стороне сервераurlНазначить соответствующий обработчик, возврат страницы, возврат интерфейса
    • Внешняя маршрутизация осуществляется черезjsсогласно сurlВернуться к соответствующему компоненту

Как реализовать интерфейсную маршрутизацию

  • Прочитав вышеприведенное определение, для реализации маршрутизации нужна очень важная вещь — таблица отображения маршрутизации;
    • Когда сервер выполняет переход на страницу маршрутизации, таблица сопоставления отражаетurlа также页面Отношение
    • Теперь внешний интерфейс в основном модульный, поэтому таблица отображения маршрутизации внешнего интерфейса отражаетurlа также组件Отношение
  • Как следующий псевдокод
// 使用es6 map
const routeMap=new Map([
    ['/',首页组件],
    ['/bar',Bar组件],
    ['/bar/foo',Foo组件]
])

// 你也可以用对象字面量
const routeMap={
    '/':首页组件,
    '/bar':Bar组件,
    '/bar/foo':Foo组件
}
  • С таблицей отображения мы знаемurlОтображение отношений с компонентами;
  • Однако таблица сопоставления поддерживает только отношения и не может помочь нам завершить доступ/bar,вернутьBar组件Такой процесс, поэтому нам также нужен сопоставитель, который поможет нам завершить процесс отurlприбыть组件совместная работа;
  • у вас есть路由映射表а также匹配器Можете ли вы добиться внешней маршрутизации?
  • нашspaОн работает в браузерной среде, и браузер имеет前进、返回Функционал, нам нужно записывать посещенныеurl;
    • Мы знаем, что для достижения чего-то вроде撤销、恢复функция, обязательно нужно использовать структуру данных -栈(stack); каждое посещениеurl,Будуurl,pushВ стеке при возврате выполнитьpopчтобы получить последний визитurl
    • К счастью, браузерная платформа предоставила нам такой стек, нам не нужно реализовывать его самим, нам достаточно вызвать его интерфейсwindow.historyПросто реализуйте функцию
  • Нарисуйте картинку, чтобы описать сотрудничество между тремя
    • sequence.png
      • sequence.png
  • Когда мы посещаем URL-адрес, например/foo, сопоставитель удержит/fooПерейдите к карте маршрутизации, чтобы найти соответствующий компонент, верните компонент для рендеринга и поместите запись доступа в стек истории.
  • когда мы идем вперед/назад, чтобы получить доступ к определенномуurl, он сначала найдет соответствующий из стека историиurl, то сопоставитель беретurlПерейдите к компоненту и вернитесь к рендерингу; однако доступ к этому осуществляется через перенаправление вперед/назад, поэтому нет необходимости снова проталкивать стек истории

Суммировать

  • Для реализации внешнего маршрута необходимы три части
    • Карта маршрута
      • можно выразитьurlа также组件Таблица сопоставления отношений, которую вы можете использоватьMap,对象字面量реализовать
    • сопоставитель
      • ответственный за посещениеurl, выполните сопоставление, чтобы найти соответствующий组件
    • стек истории
      • Платформа браузера, уже изначально поддерживаемая, нет необходимости реализовывать, напрямую вызывать интерфейс
  • Теперь вам не нужно беспокоиться об их конкретной реализации, вам нужно только знать, что есть эти три вещи и как они работают вместе;
  • Мы посмотрим на это позжеvue-routerКак использовать их для обеспечения внешней маршрутизации

период, термин

  • в анализеvue-routerПеред исходным кодом давайте разберемсяvue-routerЕсли вам трудно понять некоторые концептуальные термины, часто встречающиеся в книге, вы можете сначала пропустить ее, столкнуться с ней позже и вернуться к ней снова;

Правила маршрутизации, объекты конфигурации (RouteConfig)

  • Элемент конфигурации маршрута, используемый для описания маршрута.
  • Красные прямоугольники на рисунке ниже — это все объекты конфигурации маршрутизации.
  • route-config.png
    • route-config.png
  • потому чтоvue-routerда поддержка嵌套路由, поэтому объекты конфигурации также могут быть вложены друг в друга
  • Полная форма выглядит следующим образом
interface RouteConfig = {
  path: string,
  component?: Component,// 路由组件
  name?: string, // 命名路由
  components?: { [name: string]: Component }, // 命名视图组件
  redirect?: string | Location | Function,
  props?: boolean | Object | Function,
  alias?: string | Array<string>,
  children?: Array<RouteConfig>, // 嵌套路由
  beforeEnter?: (to: Route, from: Route, next: Function) => void,
  meta?: any,

  // 2.6.0+
  caseSensitive?: boolean, // 匹配规则是否大小写敏感?(默认值:false)
  pathToRegexpOptions?: Object // 编译正则的选项
}

маршрутная запись (RouteRecord)

  • Каждое правило маршрутизации создает запись маршрутизации; вложенная маршрутизация и маршрутизация псевдонимов также создают запись маршрутизации; она является частью таблицы сопоставления маршрутизации.

 const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由
    components: route.components || { default: route.component }, // 保存路由组件,支持命名视图https://router.vuejs.org/zh/guide/essentials/named-views.html#命名视图
    instances: {}, // 保存每个命名router-view需要渲染的路由组件
    name,
    parent,
    matchAs,
    redirect: route.redirect, // 重定向的路由配置对象
    beforeEnter: route.beforeEnter, // 路由独享的守卫
    meta: route.meta || {}, // 元信息
    props: // 动态路由传参;https://router.vuejs.org/zh/guide/essentials/passing-props.html#路由组件传参
      route.props == null
        ? {}
        : route.components // 命名视图的传参规则需要使用route.props指定的规则
          ? route.props
          : { default: route.props }
  }

объект маршрута (Route)

  • RouteОтображает информацию о статусе текущего активированного маршрута, включая текущуюURLанализируемая информация иURLсовпадающая запись маршрутаOни (route records).
    • https://router.vuejs.org/zh/api/#路由对象
  • Обратите внимание, что это указывает наRouteМожет быть связан с несколькимиRouteRecord
  • пройти черезthis.$routeДоступный этоRouteобъект
  • route.png
    • route.png
  • Объекты маршрута неизменяемы (immutable) новый объект генерируется после каждой успешной навигации

Место нахождения(Location)

  • это неwindow.locationцитаты,vue-routerвнутренне определяетLocation, это объект, используемый для описания целевого местоположения;
  • $router.push/replace,router-link的toполученоLocation对象
    • https://router.vuejs.org/zh/api/#to
  • vue-routerВнутриurl stringПеревести вLocation对象, так точно$router.push/replace,router-link的toполучилRawLocation对象
  • RawLocation对象даStringа такжеLocationтип союза
export type RawLocation = string | Location

export interface Location {
  name?: string
  path?: string
  hash?: string
  query?: Dictionary<string | (string | null)[] | null | undefined>
  params?: Dictionary<string>
  append?: boolean
  replace?: boolean
}

компонент маршрутизации (RouteComponent)

  • Когда маршрут успешно сопоставлен, его необходимоrouter-viewВизуализируйте компонент, компонент, который необходимо визуализировать,路由组件
  • RouteConfigсерединаcomponent、componentsопределено вvue组件это компонент маршрутизации
  • Особенности компонентов маршрутизации
    • имеютДействует только в компонентах маршрутизацииохранник (beforeRouteEnter 、beforeRouteUpdate、beforeRouteLeave)
    • Вы, как и я, когда-нибудь звонили в компонентbeforeRouteEnterОбнаружение не вступает в силу, потому что эту защиту можно использовать только в路由组件Он вызывается во всех компонентах, не являющихся маршрутизирующими, включая компоненты-потомки компонентов маршрутизации; если вы хотите реализовать его в компонентах маршрутизацииbeforeRouteEnterПодобно эффекту наблюдения за охраной, вы можете пройтиwatch $routeсудить вручную
  • Красное поле отмечено路由组件
  • route-component.png
    • route-component.png
  • Прочитав приведенную выше терминологию, вы все еще можете быть в тумане. Это нормально. Это будет подробно объяснено позже. Сейчас вам нужно только иметь общее представление;

окружающая обстановка

  • vue-routerВерсия:v3.1.6
  • nodeВерсия:v8.17.0
  • Адрес склада анализов:https://github.com/BryanAdamss/vue-router-for-analysis
    • фокус
      • Обрати внимание наcommit记录,commitЗадокументировал весь мой процесс анализа
      • Если вы думаете, что все в порядке, не забывайтеstar,fork🤞

Структура каталогов

  • Сначала мы будемvue-routerскладcloneДавайте посмотрим, как выглядит структура каталогов.
  • git clone git@github.com:BryanAdamss/vue-router-for-analysis.git

содержание

  • Вы можете видеть, что будут следующие каталоги
  • directory.png
    • directory.png
  • Не смотрите сколько каталогов, на самом деле я уже отметил для вас ключевые моменты
  • На самом деле нам нужно только обратить внимание на следующие каталоги или файлы
  • examples
    • Вот кейсы, тщательно подготовленные чиновником
    • не только сказать вамvue-routerКаковы основные функции, а также рассказать вам, как бороться с некоторыми сложными сценариями, такими как контроль разрешений, динамическое добавление маршрутов и т. д., короче говоря, это стоит изучить;
    • examples.png
      • examples.png
    • Кроме того, когда мы анализируем исходный код, мы также можем использовать эти примеры для отладки.
      • Добавьте исходный код, где вы хотите отладитьdebugger断点标识Затем начните примерnpm run dev,Открытьlocalhost:8080Только что
  • srcсодержание
    • Этоvue-routerКаталог, в котором хранится исходный код, — это то, на чем нам нужно сосредоточиться.
    • src.png
      • src.png
    • Возьмите несколько важных каталогов и поговорите об этом в первую очередь.
      • componentsКаталог для хранения встроенных компонентовrouter-link,router-viewиз
      • historyявляется ядром хранилищаhistory类Место
      • utilХранится в некоторых вспомогательных функциях
      • index.jsдаvue-routerвходной файл, а такжеvue-routerгде класс определен
      • install.jsэто файл, в котором находится логика установки
  • flow/declarations.js
    • этоvue-routerФайл объявления типа потока, через который мы можем узнатьvue-routerКак выглядят несколько основных классов (объектов)
    • В нем это выглядит так
    • declarations.png
      • declarations.png

Базовый пример

  • Начнем с самого простого примера
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1. Use plugin.
// 安装插件
Vue.use(VueRouter)

// 2. Define route components
// 定义路由组件
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 3. Create the router
// 实例化vue-router
const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar },
  ]
})

// 4. Create and mount root instance.
new Vue({
  router, // 注入router实例
  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>
       </ul>
       
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')
  • Как видите, первое использованиеvueСинтаксис плагина установленvue-router; затем мы создаем экземплярVueRouter; наконец, мыVueRouterЭкземпляр вводится вVue
  • На самом деле это включает в себя три основных процесса: установка, создание экземпляра, инициализация.
  • Начнем с этих трех процессов

Процесс установки

  • Мы знаем, что покаvue plugin, должен быть одинinstallметод;
  • также упоминалось вышеvue-routerВходной файл находится вsrc/index.jsСередина, потом пойдемindex.jsнайти вinstallметод
// src/index.js

/* @flow */
import { install } from './install' // 导入安装方法
...

// VueRouter类
export default class VueRouter {
  ... 
}

...

VueRouter.install = install // 挂载安装方法,Vue.use时,自动调用install方法
VueRouter.version = '__VERSION__'

// 浏览器环境,自动安装VueRouter
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
  • Как видите, он был импортирован в началеinstallметод и смонтировать его непосредственно как статический метод дляVueRouterна, таким образом, наVue.use(VueRouter)час,installбудет вызван метод;
  • Вы можете видеть, что если вы находитесь в среде браузера и передаетеscript标签вводится в видеVueкогда (будетwindowсадиться наVueглобальная переменная), попытается использовать ее автоматическиVueRouter
  • посмотрим дальшеinstall.jsчто в

install.js

  • installспособ не долгий

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

export let _Vue // 用来避免将Vue做为依赖打包进来

// install方法
export function install (Vue) {
  if (install.installed && _Vue === Vue) return // 避免重复安装
  install.installed = true
  
  _Vue = Vue // 保留Vue引用
  
  const isDef = v => v !== undefined
  
  // 为router-view组件关联路由组件
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    // 调用vm.$options._parentVnode.data.registerRouteInstance方法
    // 而这个方法只在router-view组件中存在,router-view组件定义在(../components/view.js @71行)
    // 所以,如果vm的父节点为router-view,则为router-view关联当前vm,即将当前vm做为router-view的路由组件
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  
  // 注册全局混入
  Vue.mixin({
    beforeCreate () {
      // this === new Vue({router:router}) === Vue根实例
      
      // 判断是否使用了vue-router插件
      if (isDef(this.$options.router)) {
        // 在Vue根实例上保存一些信息
        this._routerRoot = this // 保存挂载VueRouter的Vue实例,此处为根实例
        this._router = this.$options.router // 保存VueRouter实例,this.$options.router仅存在于Vue根实例上,其它Vue组件不包含此属性,所以下面的初始化,只会执行一次
        // beforeCreate hook被触发时,调用
        this._router.init(this) // 初始化VueRouter实例,并传入Vue根实例
        // 响应式定义_route属性,保证_route发生变化时,组件(router-view)会重新渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 回溯查找_routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      
      // 为router-view组件关联路由组件
      registerInstance(this, this)
    },
    destroyed () {
      // destroyed hook触发时,取消router-view和路由组件的关联
      registerInstance(this)
    }
  })
  
  // 在原型上注入$router、$route属性,方便快捷访问
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    // 每个组件访问到的$route,其实最后访问的都是Vue根实例的_route
    get () { return this._routerRoot._route }
  })
  
  // 注册router-view、router-link全局组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
  
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
  • можно увидеть,installМетод делает следующие вещи

Избегайте повторных установок

  • добавлениемinstalledИдентификация, чтобы определить, следует ли повторить установку

резервVueцитаты, не ставьтеVueупакован как зависимость

  • installКогда метод вызывается, онVueПередайте в качестве параметра,Vueбудет назначено заранее определенному_VueПеременная
  • В другие модули вы можете импортировать это_Vue, чтобы оба имели доступ кVue, что позволяет избежатьVueупакован как зависимость
  • Это практический совет по разработке плагинов

зарегистрировал глобальный миксин

  • Этот миксин будет влиять на каждое творение после регистрацииVueэкземпляр, то есть каждый из следующихVueЭкземпляры будут выполнять код в миксине
  • Давайте посмотрим на код в миксине

  // 注册全局混入
  Vue.mixin({
    beforeCreate () {
      // this === new Vue({router:router}) === Vue根实例
      
      // 判断是否使用了vue-router插件
      if (isDef(this.$options.router)) {
        // 在Vue根实例上保存一些信息
        this._routerRoot = this // 保存挂载VueRouter的Vue实例,此处为根实例
        this._router = this.$options.router // 保存VueRouter实例,this.$options.router仅存在于Vue根实例上,其它Vue组件不包含此属性,所以下面的初始化,只会执行一次
        // beforeCreate hook被触发时,调用
        this._router.init(this) // 初始化VueRouter实例,并传入Vue根实例
        // 响应式定义_route属性,保证_route发生变化时,组件(router-view)会重新渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 回溯查找_routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      
      // 为router-view组件关联路由组件
      registerInstance(this, this)
    },
    destroyed () {
      // destroyed hook触发时,取消router-view和路由组件的关联
      registerInstance(this)
    }
  })
  • Он регистрирует два хука жизненного циклаbeforeCreateа такжеdestroyed;
  • Уведомление
    • В этих двух крючкахthisэто указать на крючок, который вызывался в то времяvue实例;
    • Логика этих двух хуков не будет выполняться в процессе установки и будет вызываться только тогда, когда хук выполняется при создании экземпляра компонента.
  • давайте сначала посмотримbeforeCreateкрюк
  • он судит первымthis.$options.routerСуществует ли он, мыnew Vue({router})час,routerбыл сохранен вVue根实例из$optionsна, в то время как другиеVue实例из$optionsнет лиrouterиз
    • такifЗаявление вthis === new Vue({router})будет выполнен, когдаVue根实例Есть только один, поэтому эта логика будет выполняться только один раз
    • мы можемifпечатать вthisИ в сочетании с инструментами отладки, чтобы увидеть
    • root-instance.png
      • root-instance.png
    • конечно,ifЛогика in выполняется только один раз, иthisпросто укажите наVue根实例
  • Давайте посмотримifчто именно сделал
    • сохраняется в корневом экземпляре_routerRoot, идентифицироватьrouterсмонтированныйVue实例
    • сохраняется в корневом экземпляреVueRouterпример (router)
    • правильноrouterинициализирован (init)
      • routerДавайте подробнее рассмотрим логику инициализации позже.
    • В корневом экземпляре реактивный определяет_routeАтрибуты
      • гарантия_routeпри смене,router-viewбудет перерендерен, что мы позжеrouter-viewкомпоненты будут подробно рассмотрены
  • давайте посмотрим еще разelseчто именно сделал
    • В основном определяется для каждого компонента_routerRoot, используя метод послойного поиска с возвратом
  • мы видим другойregisterInstanceметод, он вbeforeCreate,destroyedОба вызываются, но количество параметров разное
    • существуетbeforeCreateПередаются два параметра, и обаthisто есть текущийvue实例, пока вdestroyedпрошел только одинvue实例
    • Разговаривалиrouter-viewбудет вдаваться в подробности, вам просто нужно знать, что он используется дляrouter-viewАссоциация компонентов или отвязка路由组件может быть использован
    • Передача двух параметров означает ассоциацию, передача одного параметра означает отвязку

Добавить свойства экземпляра, методы

  • существуетVueВнедрить в прототип$router、$routeсвойства, удобный вvue实例прошедшийthis.$router,this.$routeбыстрый доступ

Зарегистрировать глобальные компоненты router-view, router-link

  • пройти черезVue.componentГрамматически зарегистрированныйrouter-viewа такжеrouter-linkДва глобальных компонента

Установите стратегию слияния охранников компонентов маршрута.

  • Настройте компонент маршрутизацииbeforeRouteEnter,beforeRouteLeave,beforeRouteUpdateСтратегия защищенного слияния

Суммировать

  • Давайте воспользуемся картинкой, чтобы подытожить процесс установки.
  • install.png
    • install.png

созданный процесс

  • Прочитав процесс установки, давайте посмотримVueRouterпроцесс создания экземпляра
  • В этом разделе мы сосредоточимся на процессе создания экземпляров, поэтому рассмотрим толькоconstructorосновная логика в

Конструктор VueRouter

  • мы открытыsrc/index.jsпосмотриVueRouterКонструктор
// src/index.js

export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    this.app = null // 保存挂载实例
    this.apps = [] // VueRouter支持多实例
    this.options = options
    this.beforeHooks = [] // 接收 beforeEach hook
    this.resolveHooks = [] // 接收 beforeResolve hook
    this.afterHooks = [] // 接收 afterEach hook
    this.matcher = createMatcher(options.routes || [], this) // 创建路由matcher对象,传入routes路由配置列表及VueRouter实例

    let mode = options.mode || 'hash'
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false // 是否回退
    if (this.fallback) {
      mode = 'hash'
    }

    // 非浏览器环境,强制使用abstract模式
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    // 根据不同mode,实例化不同history实例
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
}
  • Конструктор в основном делает следующие вещи

Получить RouterOptions

  • Как видите, конструктор получаетoptions选项对象, который имеет типRouterOptions, Давайте взглянемRouterOptions
  • Открытьflow/declarations.js
// flow/declarations.js

declare type RouterOptions = {
  routes?: Array<RouteConfig>;// 路由配置规则列表
  mode?: string; // 路由模式
  fallback?: boolean; // 是否启用回退
  base?: string;// base地址
  linkActiveClass?: string; // router-link激活时类名
  linkExactActiveClass?: string;// router-link精准激活时类名
  parseQuery?: (query: string) => Object; // 自定义解析qs的方法
  stringifyQuery?: (query: Object) => string; // 自定义序列化qs的方法
  scrollBehavior?: (// 控制滚动行为
    to: Route,
    from: Route,
    savedPosition: ?Position
  ) => PositionResult | Promise<PositionResult>;
}

  • RouterOptionsОпределенныйVueRouterвсе опции, которые можно получить;
  • Остановимся на следующих вариантах
    • routesЭто список правил конфигурации маршрутизации, который в основном используется для последующего создания таблиц сопоставления маршрутизации;
      • Это массив, каждый элемент которого представляет собой правило конфигурации маршрутизации (RouteConfig),оRouteConfig, вы можете вернуться к разделу терминологии;
    • mode,fallbackотносится к режиму маршрутизации
      • будет подробно позжеVueRouterрежим маршрутизации

начальное значение атрибута

  • Некоторым свойствам присваиваются начальные значения, например, для получения全局导航守卫(beforeEach、beforeResolve、afterEach)Массив инициализирован

Создать сопоставление

  • пройти черезcreateMatcherгенерируетсяmatcher
  • этоmatcher对象Это сопоставитель, который изначально был в чате, отвечает за сопоставление URL, он получаетroutesа такжеrouter实例;createMatcherне только созданmatcher, также созданный路由映射表RouteMap, мы рассмотрим подробнее позже

Определить режим маршрутизации

  • Мы подробно обсудим три режима маршрутизации позже.
  • Просто нужно знать сейчасVueRouterкак определяется режим маршрутизации
  • VueRouterбудет основываться наoptions.mode,options.fallback,supportsPushState,inBrowserчтобы определить окончательный шаблон маршрутизации
  • определите сначалаfallback,fallbackУстанавливается только в пользователеmode:historyи текущая среда не поддерживаетpushStateА пользователь активно заявляет, что нужно откатиться, в это времяfallbackтолько дляtrue
  • когдаfallback为trueбудет использоваться, когдаhashмодель;
  • Если он, наконец, будет найден в среде, отличной от браузера, он будет вынужден использоватьabstractмодель
  • route-mode.png
    • route-mode.png
  • inBrowserа такжеsupportsPushStateРеализация очень проста
// src/util/dom.js
export const inBrowser = typeof window !== 'undefined' // 直接判断window是否存在,来确定是否在浏览器环境中

// src/util/push-state.js
export const supportsPushState =
  inBrowser && // 浏览器环境
  (function () {
    const ua = window.navigator.userAgent
    if (
      (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
      ua.indexOf('Mobile Safari') !== -1 &&
      ua.indexOf('Chrome') === -1 &&
      ua.indexOf('Windows Phone') === -1
    ) {
      return false // 特殊浏览器直接不支持
    }
    return window.history && typeof window.history.pushState === 'function' // 判断是否支持pushState
  })()

Создание различных экземпляров истории на основе шаблонов маршрутизации

  • Создание различных маршрутов на основе шаблона маршрутизации на предыдущем шаге.History实例, о режиме маршрутизации,History实例я расскажу об этом позже

резюме

  • VueRouterКонструктор в основном делает следующие вещи
    • получитьRouterOptions
    • Затем присвойте начальные значения некоторым свойствам
    • генерируетсяmatcherсопоставитель
    • Определить режим маршрутизации
    • Создание различных маршрутов в соответствии с различными режимами маршрутизацииHistory实例

Создать сопоставление

  • Давайте взглянемcreateMatcherреализация внутри
  • createMatcherреализуется вsrc/create-matcher.jsсередина
 // src/create-matcher.js
 
... 
 
// Matcher类型
export type Matcher = {
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; // 匹配方法
  addRoutes: (routes: Array<RouteConfig>) => void; // 添加Route方法
};

// Matcher工厂函数
export function createMatcher (
  routes: Array<RouteConfig>, // 路由配置列表
  router: VueRouter // VueRouter实例
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes) // 创建路由映射表
  // 添加路由
  function addRoutes (routes) {
    // 由于传入pathList, pathMap, nameMap了,所以createRouteMap方法会执行添加逻辑
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // 传入location,返回匹配的Route对象
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    ...
  }
  
  // 返回Matcher对象,暴露match、addRoutes方法
  return {
    match,
    addRoutes
  }
} 
  • можно увидеть,createMatcherметод получает список правил конфигурации маршрутизации иrouter实例, который возвращаетMatcherобъект
  • Matcherобъект содержитmatchметод и динамически добавляемый маршрутaddRoutesметод;
    • И оба эти метода объявлены вcreateMatcherВнутри, благодаря функции закрытия, он может получить доступcreateMatcherвсе переменные в области видимости

резюме

  • мы резюмируемcreateMatcherлогика
  • create-matcher.png
    • create-matcher.png
  • Мы видим, чтоcreateMatcherа такжеaddRoutesметод называетсяcreateRouteMapдва метода передают только разные параметры.Судя по имени метода, этот метод должен быть таким же, как路由表RouteMapСвязанный
  • посмотрим дальшеcreateRouteMapреализация

createRouteMap

  • createRouteMapметод находится вsrc/create-route-map.jsсередина

// 创建路由映射map、添加路由记录
export function createRouteMap (
  routes: Array<RouteConfig>, // 路由配置列表
  oldPathList?: Array<string>, // 旧pathList
  oldPathMap?: Dictionary<RouteRecord>, // 旧pathMap
  oldNameMap?: Dictionary<RouteRecord>// 旧nameMap
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {

  // 若旧的路由相关映射列表及map存在,则使用旧的初始化(借此实现添加路由功能)
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  
  // 遍历路由配置对象,生成/添加路由记录
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  
  // ensure wildcard routes are always at the end
  // 确保path:*永远在在最后
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  
  // 开发环境,提示非嵌套路由的path必须以/或者*开头
  if (process.env.NODE_ENV === 'development') {
    // warn if routes do not include leading slashes
    const found = pathList
    // check for missing leading slash
      .filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')
    if (found.length > 0) {
      const pathNames = found.map(path => `- ${path}`).join('\n')
      warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)
    }
  }
  
  return {
    pathList,
    pathMap,
    nameMap
  }
}
  • можно увидетьcreateRouteMapВозвращает объект, содержащийpathList,pathMapа такжеnameMap
  • pathListХранится вroutesвсе вpath
  • pathMapподдерживаетсяpathа также路由记录RouteRecordотображение
  • nameMapподдерживаетсяnameа также路由记录RouteRecordотображение
    • потому чтоVueRouterПоддержка именованных маршрутов
  • Последние два предназначены для быстрого нахождения соответствующего路由记录
  • Вы можете использовать следующиеroutesпередачаcreateRouteMapчто вернется
[
    { path: '/', component: Home },
    { path: '/foo', component: Foo ,
      children:[
      {
        path:'child',
        component:FooChild
      }
    ]},
    { path: '/bar/:dynamic', component: Bar },
  ]
  • route-map-obj.png
    • route-map-obj.png
  • Так как именованного маршрута нет, тоnameMapПусто
  • pathListсохранено всеpath, один пустой, что на самом деле/,существуетnormalizePathбыл удален, когда
  • pathMapзаписал каждыйpathа также对应RouteRecordотношения отображения

резюме

  • VueRouterТаблица карты маршрутов состоит из трех частей:pathList,pathMap,nameMap; последние два используются для быстрого поиска
  • createRouteMapлогика
    • Сначала определите, существует ли уже таблица отображения, связанная с маршрутизацией, если да, используйте ее, в противном случае создайте новую;
      • Это позволяетcreateRouteMapСоздать/добавить двойные функции
    • затем пройтиroutes, по очереди для каждогоrouteпередачаaddRouteRecordгенерироватьRouteRecordи обновитьpathList,pathMapа такжеnameMap
    • из-заpathListВ последующей логике он будет использоваться для обхода совпадения, для производительности необходимоpath:*помещен вpathListпоследний
    • Наконец проверьте невложенные маршрутыpathЭто/или*начало
  • Это можно резюмировать следующим образом
  • create-route-map-sequence.png
    • create-route-map-sequence.png
  • Далее давайте посмотрим, как генерируются записи маршрутизации.

addRouteRecord

  • Этот метод в основном предназначен для создания записей маршрутизации и обновления таблицы сопоставления маршрутизации.
  • родыsrc/create-route-map.js
// src/create-route-map.js

// 添加路由记录,更新pathList、pathMap、nameMap
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord, // 父路由时记录
  matchAs?: string // 处理别名路由时使用
) {
  const { path, name } = route
  
  if (process.env.NODE_ENV !== 'production') {
  
    // route.path不能为空
    assert(path != null, `"path" is required in a route configuration.`)
    
    // route.component不能为string
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(
        path || name
      )} cannot be a ` + `string id. Use an actual component instead.`
    )
  }
  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
    
  // 生成格式化后的path(子路由会拼接上父路由的path)
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
 
 // 匹配规则是否大小写敏感?(默认值:false)
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  
  // 生成一条路由记录
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由
    components: route.components || { default: route.component }, // 保存路由组件,支持命名视图https://router.vuejs.org/zh/guide/essentials/named-views.html#命名视图
    instances: {}, // 保存router-view实例
    name,
    parent,
    matchAs,
    redirect: route.redirect, // 重定向的路由配置对象
    beforeEnter: route.beforeEnter, // 路由独享的守卫
    meta: route.meta || {}, // 元信息
    props: // 动态路由传参;https://router.vuejs.org/zh/guide/essentials/passing-props.html#路由组件传参
      route.props == null
        ? {}
        : route.components // 命名视图的传参规则需要使用route.props指定的规则
          ? route.props
          : { default: route.props }
  }
  
  // 处理有子路由情况
  if (route.children) {
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    // https://github.com/vuejs/vue-router/issues/629
    // 命名路由 && 未使用重定向 && 子路由配置对象path为''或/时,使用父路由的name跳转时,子路由将不会被渲染
    if (process.env.NODE_ENV !== 'production') {
      if (
        route.name &&
        !route.redirect &&
        route.children.some(child => /^\/?$/.test(child.path))
      ) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
            `When navigating to this named route (:to="{name: '${
              route.name
            }'"), ` +
            `the default child route will not be rendered. Remove the name from ` +
            `this route and use the name of the default child route for named ` +
            `links instead.`
        )
      }
    }
    
    // 遍历生成子路由记录
    route.children.forEach(child => {
      const childMatchAs = matchAs // matchAs若有值,代表当前路由是别名路由,则需要单独生成别名路由的子路由,路径前缀需使用matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  
  // 若pathMap中不存在当前路径,则更新pathList和pathMap
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  
  // 处理别名;https://router.vuejs.org/zh/guide/essentials/redirect-and-alias.html#%E5%88%AB%E5%90%8D
  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] // alias支持string,和Array<String>
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if (process.env.NODE_ENV !== 'production' && alias === path) { // alias的值和path重复,需要给提示
        warn(
          false,
          `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }
      
      // 生成别名路由配置对象
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      
      // 添加别名路由记录
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute, // 别名路由
        parent, // 当前路由的父路由,因为是给当前路由取了个别名,所以二者其实是有同个父路由的
        record.path || '/' // matchAs,用来生成别名路由的子路由;
      )
      // ! 总结:当前路由设置了alias后,会单独为当前路由及其所有子路由生成路由记录,且子路由的path前缀为matchAs(即别名路由的path)
    }
  }
  
  // 处理命名路由
  if (name) {
    // 更新nameMap
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      // 路由重名警告
      warn(
        false,
        `Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}
  • посмотри на логику
    • Проверил правила маршрутизации дляpathа такжеcomponent
    • генерироватьpath-to-regexpОпцииpathToRegexpOptions
    • форматpath, если это вложенный маршрут, он добавит родительский маршрутpath
    • Создание записей маршрутизации
    • Обработка вложенных маршрутов и рекурсивное создание записей подмаршрутов
    • возобновитьpathList,pathMap
    • Обработка псевдонимов маршрутизации и создание записей псевдонимов маршрутизации
    • обрабатывать именованные маршруты, обновлятьnameMap
  • Давайте посмотрим на некоторую основную логику

Создание записей маршрутизации

  • Записи маршрутизации записывают основную информацию о маршрутизации.
  const record: RouteRecord = {
    path: normalizedPath,// 规范化后的路径
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由
    components: route.components || { default: route.component }, // 保存路由组件,支持命名视图https://router.vuejs.org/zh/guide/essentials/named-views.html#命名视图
    instances: {}, // 保存router-view实例
    name,
    parent,// 父路由记录
    matchAs, // 别名路由需要使用
    redirect: route.redirect, // 重定向的路由配置对象
    beforeEnter: route.beforeEnter, // 路由独享的守卫
    meta: route.meta || {}, // 元信息
    props: // 动态路由传参;https://router.vuejs.org/zh/guide/essentials/passing-props.html#路由组件传参
      route.props == null
        ? {}
        : route.components // 命名视图的传参规则需要使用route.props指定的规则
          ? route.props
          : { default: route.props }
  }
  • Запись маршрутизации имеетregexполе, которое представляет собой расширенное регулярное выражение, являющееся ключом к достижению динамического сопоставления маршрутов.
  • regexчерезcompileRouteRegexВозвращает метод, который вызываетpath-to-regexp
import Regexp from 'path-to-regexp'

...

// 使用path-to-regexp包,生成route对应的正则,可以用来生成动态路由需要的正则表达式
function compileRouteRegex (
  path: string,
  pathToRegexpOptions: PathToRegexpOptions
): RouteRegExp {

  const regex = Regexp(path, [], pathToRegexpOptions)
  if (process.env.NODE_ENV !== 'production') {
    const keys: any = Object.create(null)
    regex.keys.forEach(key => {
      // 重复key浸膏
      warn(
        !keys[key.name],
        `Duplicate param keys in route with path: "${path}"`
      )
      keys[key.name] = true
    })
  }
  return regex
}
  • Давайте посмотримpath-to-regexpкак это используется
  • Официальный сайт:Уууу, эта лошадь plus.com/package/pat…
  • Regexp получает три параметраpath,keys,options;pathДля пути, который необходимо преобразовать в обычный,keys, используется для полученияpathнайти вkey, который можно передать или напрямую использовать в возвращаемом значенииkeysАтрибуты,optionsвариант
  const pathToRegexp= require('path-to-regexp')
   
  const regexp = pathToRegexp("/foo/:bar");
  // regexp = /^\/foo\/([^\/]+?)\/?$/i
  // :bar被处理成正则的一个组了,当正则被执行时,就可以通过组取到bar对应的值
  console.log(regexp.keys)  // keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }] 
  • В следующем примере вы можете узнать, как динамическая маршрутизация получает значения параметров.
// test.js

const pathToRegexp= require('path-to-regexp')
   
const regexp = pathToRegexp("/foo/:bar");
 
console.log(regexp.keys)// 记录了key信息

const m = '/foo/test'.match(regexp) // 正则的组记录了value信息

console.log('key:',regexp.keys[0].name,',value:',m[1])

  • path-to-regex-demo.png
    • path-to-regex-demo.png

Создание вложенных записей маршрутизации

  • мы знаемvue-routerОн поддерживает вложенную маршрутизацию, давайте посмотрим, как создавать записи вложенной маршрутизации.
// src/create-route-map

 // 处理有子路由情况
  if (route.children) {
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    // https://github.com/vuejs/vue-router/issues/629
    // 命名路由 && 未使用重定向 && 子路由配置对象path为''或/时,使用父路由的name跳转时,子路由将不会被渲染
    if (process.env.NODE_ENV !== 'production') {
      if (
        route.name &&
        !route.redirect &&
        route.children.some(child => /^\/?$/.test(child.path))
      ) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
            `When navigating to this named route (:to="{name: '${
              route.name
            }'"), ` +
            `the default child route will not be rendered. Remove the name from ` +
            `this route and use the name of the default child route for named ` +
            `links instead.`
        )
      }
    }
    // 遍历生成子路由记录
    route.children.forEach(child => {
      const childMatchAs = matchAs // matchAs若有值,代表当前路由是别名路由,则需要单独生成别名路由的子路由,路径前缀需使用matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  • Первый для#629Проблема делает предупреждение
    • #629Проблема в основном, когда маршрут命名路由 && 未使用重定向 && 子路由配置对象path为''或/При использовании родительского маршрутаnameПри прыжках дочерние маршруты не будут отображаться
  • Затем просмотрите список правил подмаршрутизации, чтобы сгенерировать записи подмаршрутизации.
    • Это также обрабатывает подмаршрутизацию альтернативной маршрутизации.
      • Если во время обхода обнаруживается, что родительский маршрут помечен как маршрут-псевдоним, дочерний маршрутpathРодительский маршрут должен быть добавлен впередиpath, а затем сгенерировать запись
  • Мы можем посмотреть на следующие вложенные маршруты, как выглядит сгенерированная таблица сопоставления маршрутов.
[
    {
      path: '/parent',
      component: Parent,
      children: [
        { path: 'foo', component: Foo }, 
      ]
    }
 ]
  • nested-route.png
    • nested-route.png
  • Видны подмаршрутыpathДобавить родительский маршрут перед встречейpath

Создание псевдонимов маршрутных записей

  • VueRouterПоддерживает установку псевдонимов для маршрутов;/aпсевдоним/b, что означает, что когда пользователь получает доступ/b час,URLостанется как/b, но совпадение маршрута/a, когда пользователь получает доступ/a Такой же
  • Давайте посмотрим, как перец создает записи псевдонимов маршрутизации.
// src/create-route-map.js

  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] // alias支持string,和Array<String>
    
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if (process.env.NODE_ENV !== 'production' && alias === path) { // alias的值和path重复,需要给提示
        warn(
          false,
          `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }
      
      // 生成别名路由配置对象
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      
      // 添加别名路由记录
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute, // 别名路由
        parent, // 当前路由的父路由,因为是给当前路由取了个别名,所以二者其实是有同个父路由的
        record.path || '/' // matchAs,用来生成别名路由的子路由;
      )
      // ! 总结:当前路由设置了alias后,会单独为当前路由及其所有子路由生成路由记录,且子路由的path前缀为matchAs(即别名路由的path)
    }
  }
  • Псевдонимы поддерживают одиночные и множественные псевдонимы, т.е.route.aliasПоддержка входящего/fooили['/foo','/bar'], поэтому два случая сначала нормализуются и обрабатываются в массив
  • Затем выполните итерацию по этому массиву, сначала проверив псевдонимы иpathТо ли повторить, то ли сгенерировать запись конфигурации для псевдонима маршрута отдельно, и, наконец, вызватьaddRouteRecordСоздание псевдонимов маршрутных записей
    • Обратите внимание, что здесь также передаетсяmatchAsОбрабатывает сценарий, в котором маршруты-псевдонимы генерируют подмаршруты, главным образом путем установкиmatchAsдляrecord.path || '/', Затем в соответствии с созданной записью подмаршрутизацииmatchAsСоздание подзаписей псевдонимов записей маршрутизации, подробности см. выше.嵌套路由章节
  • Давайте посмотрим, как выглядит таблица сопоставления маршрутизации, сгенерированная псевдонимом маршрутизации.
[
    { path: '/root', component: Root, alias: '/root-alias' },
    { path: '/home', component: Home,
      children: [
        { path: 'bar', component: Bar, alias: 'bar-alias' },
        { path: 'baz', component: Baz, alias: ['/baz', 'baz-alias'] }        
      ]
    }
 ]
  • alias-route.png
    • alias-route.png
  • Вы можете видеть, что отдельная запись маршрутизации создается как для маршрута-псевдонима, так и для подмаршрутов маршрута-псевдонима.

резюме

  • Записи маршрутизации路由映射表важные части
    • в маршрутной записиregexЭто ключевое поле для обработки динамической маршрутизации, в основном с помощьюpath-to-regexpосуществленный
  • Генерация записей маршрутизации в основном учитывает создание следующих записей маршрутизации
    • Вложенные маршруты
      • Подмаршруты генерируют отдельную запись маршрутизации
    • Псевдонимная маршрутизация и подмаршрутизация псевдонимной маршрутизации
      • Псевдоним маршрута и его подмаршруты будут генерировать запись маршрута соответственно.
    • именованный маршрут
  • Весь процесс создания маршрутных записей показан на следующем рисунке.
  • add-route-record.png
    • add-route-record.png
  • На данный момент мы закончили анализVueRouterСоздайте логику, связанную с сопоставлением (создайте таблицу сопоставления маршрутов) при создании экземпляра.
  • Ниже мы рассмотрим генерацию из паттернов маршрутизацииHistory实例родственная логика

режим маршрутизации

  • Очень важной особенностью внешней маршрутизации является переключение страниц без обновления.
    • То есть URL-адрес меняется, и страница не обновляется для реализации перехода на страницу.
  • Для этого есть два варианта
  • своего родаhash+hashChange, другое использованиеHistory APIизpushState+popState
  • Первый в основном используетhashСтраница не обновляется при изменении и срабатываетhashChangeЭта функция для реализации внешней маршрутизации
  • Последний полностью используетHTML5 History API изpushStateМетоды иpopStateСобытия для реализации внешней маршрутизации
  • Сравните два
  • бывший
    • Хорошая совместимость,hashChangeПоддержка до IE8
    • urlбудет нести/#/, не красиво
    • Модификация сервера не требуется
  • последний
    • Совместимость с IE10
    • urlследовать нормальномуurlТакой же
    • из-за егоurlследовать нормальномуurlТо же самое, поэтому при обновлении он будет использовать этоurlДля ссылки запрашивается страница сервера, а на сервере этой страницы нет, и будет 404. Поэтому серверу необходимо сотрудничать, чтобы перенаправить все запросы на домашнюю страницу, и передать управление всем маршрутом на фронтальный маршрут.
  • VueRouterПоддерживаются три режима маршрутизации, а именноhash,history,abstract
  • hashВыкройка является реализацией первой схемы
  • historyВыкройка является реализацией второй схемы
  • abstractРежим используется в небраузерных средах, в основном дляSSR

основной класс

  • VueRouterТри режима маршрутизации в основном реализуются следующими тремя основными классами.
  • History
    • базовый класс
    • родыsrc/history/base.js
  • HTML5History
    • для поддержкиpushStateбраузер
    • src/history/html5.js
  • HashHistory
    • для не поддерживаемыхpushStateбраузер
    • src/history/hash.js
  • AbstractHistory
    • Для небраузерных сред (рендеринг на стороне сервера)
    • src/history/abstract.js
  • Через следующую картинку вы можете понять отношения между ними
  • route-mode-class.png
    • route-mode-class.png
  • HTML5History,HashHistory,AbstractHistoryВсе три унаследованы от базового классаHistory;
  • Мало того, что трое могут получить доступHistory类Все свойства и методы, они также реализуют 5 интерфейсов, объявленных в базовом классе, которые должны быть реализованы подклассами (go、push、replace、ensureURL、getCurrentLocation)
  • из-заHashHistoryмониторhashChangeОсобенность, так что будет еще одинsetupListenersметод
  • AbstractHistoryПоскольку его необходимо использовать в небраузерной среде, стека истории нет, поэтому его можно использовать только черезindex、stackимитировать
  • Ранее мы анализировалиVueRouterПри создании экземпляра процедуры знайтеVueRouterПосле определения режима маршрутизации различныеHistory实例
  • Итак, давайте посмотрим разницуHistoryпроцесс создания экземпляра

Класс истории

  • Это родительский класс (базовый класс), и другие классы наследуют его.
  • Код находится наsrc/history/base.js
// src/history/base.js
// 父类
export class History {
  router: Router
  base: string
  current: Route
  pending: ?Route
  cb: (r: Route) => void
  ready: boolean
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  
  // implemented by sub-classes
  // 需要子类(HTML5History、HashHistory)实现的方法
  +go: (n: number) => void
  +push: (loc: RawLocation) => void
  +replace: (loc: RawLocation) => void
  +ensureURL: (push?: boolean) => void
  +getCurrentLocation: () => string
  
  constructor (router: Router, base: ?string) {
    this.router = router
    // 格式化base,保证base是以/开头
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START // 当前指向的route对象,默认为START;即from
    this.pending = null // 记录将要跳转的route;即to
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }
  
  ...
  
}
  • Как видите, конструктор в основном делает следующие вещи
    • сохраненrouterПример
    • нормализованныйbase,убедисьbaseда/начало
    • Инициализирован текущий маршрут для указания, только по умолчаниюSTARTНачальный маршрут; когда маршрут прыгает,this.currentпредставляет собойfrom
    • Следующий маршрут при инициализации перехода по маршруту по умолчаниюnull; При переходе маршрутаthis.pendingпредставляет собойto
    • Некоторые свойства, связанные с обратным вызовом, инициализируются
  • STARTопределено вsrc/utils/route.jsсередина
// src/utils/route.js
// 初始路由
export const START = createRoute(null, {
  path: '/'
})
  • History类Процесс создания экземпляра выглядит следующим образом
  • history.png
    • history.png

Класс истории HTML5

  • Давайте посмотримHTML5History类, который унаследован отHistory类
  • родыsrc/history/html5.js

export class HTML5History extends History {
  constructor (router: Router, base: ?string) {
    // 初始化父类History
    super(router, base)
    
    // 检测是否需要支持scroll
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    
    // 若支持scroll,初始化scroll相关逻辑
    if (supportsScroll) {
      setupScroll()
    }
    
    // 获取初始location
    const initLocation = getLocation(this.base)
    
    // 监听popstate事件
    window.addEventListener('popstate', e => {
      const current = this.current
      
      // Avoiding first `popstate` event dispatched in some browsers but first
      // history route not updated since async guard at the same time.
      // 某些浏览器,会在打开页面时触发一次popstate
      // 此时如果初始路由是异步路由,就会出现`popstate`先触发,初始路由后解析完成,进而导致route未更新
      // 所以需要避免
      const location = getLocation(this.base)
      if (this.current === START && location === initLocation) {
        return
      }
      
      // 路由地址发生变化,则跳转,并在跳转后处理滚动
      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    })
  }
   
}
  • Видно, что он унаследован отHistory类, поэтому конструктор родительского класса вызывается в конструкторе (super(router,base))
  • Проверяет, нужно ли поддерживать поведение прокрутки, и если да, инициализирует связанную с прокруткой логику.
  • слушалpopstate事件, И вpopstateПри срабатывании звонитеtransitionToспособ прыгать
  • Обратите внимание, что здесь обрабатывается сценарий исключения
    • В некоторых браузерах открытие страницы срабатывает один разpopstate, в это время, если компонент маршрутизации является асинхронным, он появитсяpopstateСобытие инициировано, но асинхронный компонент не был проанализирован, что в конечном итоге приводит кrouteнет обновления
    • Так это заблокировано
  • Позже будет специальная глава о прокрутке и переходах маршрутизации.
  • HTML5History类Процесс создания экземпляра выглядит следующим образом
  • h5history.png
    • h5history.png

Класс HashHistory

  • родыsrc/history/hash.js
// src/history/hash.js

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    // 实例化父类
    super(router, base)
    // check history fallback deeplinking
    // fallback只有在指明了mode为history,但是浏览器又不支持popstate,用户手动指明了fallback为true时,才为true,其它情况为false
    // 如果需要回退,则将url换为hash模式(/#开头)
    // this.base来自父类
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
}
  • он наследует отHistory, так еще называютsuper(router,base)
  • проверилfallback, чтобы увидеть, нужно ли вам откатить, как упоминалось ранее, входящийfallbackТолько если пользователь установилhistoryи не поддерживаетpushStateи только если включен резервный вариантtrue
  • Поэтому в это время необходимоhistoryмодальныйurlзаменитьhashрежим, то есть добавить#, эта логика задаетсяcheckFallbackосуществленный
  • если неfallback, затем напрямую вызовитеensureSlash,убедисьurlда/начало
  • Давайте посмотримcheckFallback,ensureSlashвыполнить
// src/history/hash.js

/**
 * 检查回退,将url转换为hash模式(添加/#)
 */
function checkFallback (base) {
  const location = getLocation(base)
  // 地址不以/#开头,则添加之
  if (!/^\/#/.test(location)) {
    window.location.replace(cleanPath(base + '/#' + location)) // 这一步实现了url替换
    return true
  }
}

/**
 * 确保url是以/开头
 */
function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

// 替换hash记录
function replaceHash (path) {
  // 支持pushState,则优先使用replaceState
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
  • вы нашлиHashHistoryМеньше поддержки прокрутки и прослушиванияhashChangeсвязанная логика, это потому, чтоhashChangeЕсть некоторые специальные сценарии, вам нужно подождать, покаmountsконтролировать
    • Этот кусок был помещен на логикуsetupListenersметод,setupListenersБудет вVueRouterпередачаinitназывается, когда
  • HashHistory类Процесс создания экземпляра выглядит следующим образом
  • hash-history.png
    • hash-history.png

Класс абстрактной истории

  • AbstractHistoryдля небраузерных сред
  • родыsrc/history/abstract.js
// src/history/abstract.js

/**
 * 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式
 *
 * @export
 * @class AbstractHistory
 * @extends {History}
 */
export class AbstractHistory extends History {
 
  constructor (router: Router, base: ?string) {
    // 初始化父类
    super(router, base)
    this.stack = []
    this.index = -1
  }
}
  • Как видите, его инстанцирование самое простое, инициализируется только родительский класс, аindex,stackинициализирован
  • Как упоминалось ранее, в небраузерной среде нет стека истории, поэтому используйтеindex,stackдля имитации стека истории
  • AbstractHistory类Процесс создания экземпляра выглядит следующим образом
  • abstract-history.png
    • abstract-history.png

резюме

  • В этом разделе мы сделали краткий анализ трех режимов маршрутизации, а также рассмотрели требования для реализации этих трех режимов маршрутизации.HistoryКак создается экземпляр класса
  • Слишком далеко,VueRouterВесь процесс создания экземпляра в основном завершен
  • Ниже мы кратко суммируем диаграммуVueRouterпроцесс создания экземпляра
  • vue-router-instance.png
    • vue-router-instance.png

процесс инициализации

  • Проанализировав процесс создания экземпляра, давайте посмотрим, как осуществляется инициализация и что было сделано

Когда вызывать инициализацию

  • Анализируя процесс установки, мы знаем, чтоVueRouterЗарегистрирован глобальный миксин, миксинbeforeCreateкрюк
  • код показывает, как показано ниже
// src/install.js

// 注册全局混入
Vue.mixin({
  beforeCreate () {
    
    // 判断是否使用了vue-router插件
    if (isDef(this.$options.router)) {
     ...
      this._router = this.$options.router // 保存VueRouter实例,this.$options.router仅存在于Vue根实例上,其它Vue组件不包含此属性,所以下面的初始化,只会执行一次
      // beforeCreate hook被触发时,调用
      this._router.init(this) // 初始化VueRouter实例,并传入Vue根实例
    } else {
      ...
    }
    ...
  },
  destroyed () {
   ...
  }
})
  • Мы знаем, что глобальные примеси повлияют на все последующие творения.Vue实例,такbeforeCreateПервый триггер находится вVue根实例实例化的时候которыйnew Vue({router})час,

вызов после триггераrouterпримерinitметод и пройти вVue根实例, завершите процесс инициализации;

  • из-заrouterсуществует только вVue根实例из$options, поэтому вся инициализация будет вызвана только один раз
  • посмотрим дальшеinitреализация метода

метод инициализации

  • VueRouterизinitметод находится вsrc/index.js
// src/install.js
export default class VueRouter {
  
  // 初始化,app为Vue根实例
  init (app: any /* Vue component instance */) {
    
    // 开发环境,确保已经安装VueRouter
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )
    
    this.apps.push(app) // 保存实例
    
    // set up app destroyed handler
    // https://github.com/vuejs/vue-router/issues/2639
    // 绑定destroyed hook,避免内存泄露
    app.$once('hook:destroyed', () => {
      // clean out app from this.apps array once destroyed
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      // ensure we still have a main app or null if no apps
      // we do not release the router so it can be reused
      // 需要确保始终有个主应用
      if (this.app === app) this.app = this.apps[0] || null
    })
    
    // main app previously initialized
    // return as we don't need to set up new history listener
    // main app已经存在,则不需要重复初始化history 的事件监听
    if (this.app) {
      return
    }
    
    this.app = app
    
    const history = this.history
    
    if (history instanceof HTML5History) {
      // 若是HTML5History类,则直接调用父类的transitionTo方法,跳转到当前location
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
    
      // 若是HashHistory,在调用父类的transitionTo方法后,并传入onComplete、onAbort回调
      const setupHashListener = () => {
        // 调用HashHistory.setupListeners方法,设置hashchange监听
        // 在 route 切换完成之后再设置 hashchange 的监听,
        // 修复https://github.com/vuejs/vue-router/issues/725
        // 因为如果钩子函数 beforeEnter 是异步的话, beforeEnter 钩子就会被触发两次. 因为在初始化时, 如果此时的 hash 值不是以 / 开头的话就会补上 #/, 这个过程会触发 hashchange 事件, 就会再走一次生命周期钩子, 也就意味着会再次调用 beforeEnter 钩子函数.
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener, // transitionTo的onComplete回调
        setupHashListener // transitionTo的onAbort回调
      )
    }
    
    // 调用父类的listen方法,添加回调;
    // 回调会在父类的updateRoute方法被调用时触发,重新为app._route赋值
    // 由于app._route被定义为响应式,所以app._route发生变化,依赖app._route的组件(route-view组件)都会被重新渲染
    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
    
  }
} 
  • Видно, что в основном делаются следующие вещи
  • проверилVueRouterОн уже установлен
  • Сохранил креплениеrouter实例изvue实例
    • VueRouterПоддерживает вложенность нескольких экземпляров, поэтомуthis.appsдержатьrouter实例изvue实例
  • зарегистрировал одноразовый хукdestroyed,существуетdestroyed, удалитьthis.app, чтобы избежать утечек памяти
  • проверилthis.app, чтобы избежать повторных прослушивателей событий
  • согласно сhistoryтипа, звонитеtransitionToПерейти на другую начальную страницу
  • регистрupdateRouteобратный звонок, вrouterобновить, обновитьapp._routeполная перерисовка страницы
    • это мыview组件Главы будут подробно обсуждать
  • Давайте сосредоточимся наtransitionToродственная логика

setupListeners

  • Как упоминалось выше, при инициализации будетhistory类型,передачаtransitionToПерейти на другую начальную страницу
  • Зачем переходить на начальную страницу?
    • Поскольку URL-адрес может указывать на другие страницы во время инициализации, вам необходимо вызватьgetCurrentLocationметод, проанализировать маршрут из текущего URL-адреса, а затем перейти к нему
  • можно увидетьHTML5History类а такжеHashHistory类передачаtransitionToПараметры метода не совпадают
    • Первый проходит только по одному параметру
    • Последний проходит по трем параметрам
  • Давайте посмотримtransitionToсигнатура метода

// src/history/base.js
...
// 父类
export class History {
  ...
  
  // 路由跳转
  transitionTo (
    location: RawLocation, // 原始location,一个url或者是一个Location interface(自定义形状,在types/router.d.ts中定义)
    onComplete?: Function, // 跳转成功回调
    onAbort?: Function// 跳转失败回调
  ) {
    ...   
  }
}
  • Первый параметр — это адрес для анализа, второй — обратный вызов успешного перехода, а третий — обратный вызов неудачного перехода.
  • Давайте взглянемHashHistoryПочему классы должны передаваться в обратных вызовах
  • Вы можете видеть, что входящие успешные и неудачные обратные вызовыsetupHashListenerфункция,setupHashListenerВызывается внутри функцииhistory.setupListenersметод, и этот методHashHistory类уникальный
  • Открытьsrc/history/hash.js
// src/history/hash.js

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  // 修复#725;https://github.com/vuejs/vue-router/issues/725
  // 因为如果钩子函数 beforeEnter 是异步的话, beforeEnter 钩子就会被触发两次. 因为在初始化时, 如果此时的 hash 值不是以 / 开头的话就会补上 #/, 这个过程会触发 hashchange 事件, 就会再走一次生命周期钩子, 也就意味着会再次调用 beforeEnter 钩子函数.
  setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    // 若支持scroll,初始化scroll相关逻辑
    if (supportsScroll) {
      setupScroll()
    }
    // 添加事件监听
    window.addEventListener(
      supportsPushState ? 'popstate' : 'hashchange', // 优先使用popstate
      () => {
        const current = this.current
        if (!ensureSlash()) {
          return
        }
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, /* to*/route, /* from*/current, true)
          }
          // 不支持pushState,直接替换记录
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      }
    )
  }
  • Основная логика следующая
  • setupListenersВ основном он определяет, нужно ли поддерживать поведение прокрутки, и если да, то инициализирует соответствующую логику.
  • затем добавьтеurl变化Мониторинг событий, как было сказано ранее, есть два варианта реализации маршрутизацииpushState+popState,hash+hashChange
  • здесь можно увидеть дажеHashHistoryтакже будет использоваться предпочтительноpopstateсобытие для мониторингаurlИзменение
  • когдаurlКогда произойдет изменение, оно будет вызваноtransitionToПерейти на новый маршрут
  • Вы можете увидеть логическую сумму этого блокаHTML5History类Логика, обрабатываемая при создании экземпляра, очень похожа
// src/history/html5.js

export class HTML5History extends History {
  constructor (router: Router, base: ?string) {
    ...

    // 检测是否需要支持scroll
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    // 若支持scroll,初始化scroll相关逻辑
    if (supportsScroll) {
      setupScroll()
    }

    // 获取初始location
    const initLocation = getLocation(this.base)
    // 监听popstate事件
    window.addEventListener('popstate', e => {
     ...

      // 路由地址发生变化,则跳转,并在跳转后处理滚动
      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    })
  }
}
  • Так почему же два разных тайминга?
    • HTML5HistroyПрослушивание событий при создании экземпляра
    • HashHistoryПрослушивание событий после начального перехода по маршруту
  • это исправить#725 Вопрос
    • еслиbeforeEnterасинхронный,beforeEnterсработает дважды, потому что во время инициализацииhashзначение не/Он будет добавлен в начале#/, этот процесс запуститсяhashchangeсобытие, поэтому хук жизненного цикла будет взят снова, что приведет к другому вызовуbeforeEnterКрючковая функция. Так что толькоhashChangeМониторинг события задерживается до завершения начального перехода по маршруту;

резюме

  • Для процесса инициализации обобщается следующая диаграмма действий.
  • init.png
    • init.png

Суммировать

  • Статья в основном начинается с общих идей дизайна внешней маршрутизации и постепенно анализирует основные принципы проектирования внешней маршрутизации, чтобы прояснить идеи дизайна.
  • Затем введите глобальнуюvue-routerнесколько терминов
  • После общего впечатления от терминологии введемvue-routerструктура каталогов, как она иерархична
  • Разобравшись со структурой каталогов, мы начинаем анализировать с установки, чтобы понятьvue-routerЧто вы делали во время установки
  • После окончания установки вводим еще несколькоHistory类Каковы отношения наследования между ними и как они создаются?
  • Создание экземпляра завершено, и мы, наконец, вводим процесс инициализации

PS

Ссылаться на

общаться с

  • Если у вас есть какие-либо вопросы, вы можете добавить WeChat, чтобы общаться, расти вместе и вместе добиваться прогресса~