Анализ исходного кода 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
- потому что
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
- Объекты маршрута неизменяемы (
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
- Прочитав приведенную выше терминологию, вы все еще можете быть в тумане. Это нормально. Это будет подробно объяснено позже. Сейчас вам нужно только иметь общее представление;
окружающая обстановка
-
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
- Не смотрите сколько каталогов, на самом деле я уже отметил для вас ключевые моменты
- На самом деле нам нужно только обратить внимание на следующие каталоги или файлы
-
examples- Вот кейсы, тщательно подготовленные чиновником
- не только сказать вам
vue-routerКаковы основные функции, а также рассказать вам, как бороться с некоторыми сложными сценариями, такими как контроль разрешений, динамическое добавление маршрутов и т. д., короче говоря, это стоит изучить; - examples.png
- Кроме того, когда мы анализируем исходный код, мы также можем использовать эти примеры для отладки.
- Добавьте исходный код, где вы хотите отладить
debugger断点标识Затем начните примерnpm run dev,Открытьlocalhost:8080Только что
- Добавьте исходный код, где вы хотите отладить
-
srcсодержание- Это
vue-routerКаталог, в котором хранится исходный код, — это то, на чем нам нужно сосредоточиться. - 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
- это
Базовый пример
- Начнем с самого простого примера
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
- конечно,
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
созданный процесс
- Прочитав процесс установки, давайте посмотрим
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
-
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
- Мы видим, что
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
- Так как именованного маршрута нет, то
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
- Далее давайте посмотрим, как генерируются записи маршрутизации.
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
Создание вложенных записей маршрутизации
- мы знаем
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
- Видны подмаршруты
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
- Вы можете видеть, что отдельная запись маршрутизации создается как для маршрута-псевдонима, так и для подмаршрутов маршрута-псевдонима.
резюме
- Записи маршрутизации
路由映射表важные части- в маршрутной записи
regexЭто ключевое поле для обработки динамической маршрутизации, в основном с помощьюpath-to-regexpосуществленный
- в маршрутной записи
- Генерация записей маршрутизации в основном учитывает создание следующих записей маршрутизации
- Вложенные маршруты
- Подмаршруты генерируют отдельную запись маршрутизации
- Псевдонимная маршрутизация и подмаршрутизация псевдонимной маршрутизации
- Псевдоним маршрута и его подмаршруты будут генерировать запись маршрута соответственно.
- именованный маршрут
- Вложенные маршруты
- Весь процесс создания маршрутных записей показан на следующем рисунке.
- 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
-
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
Класс истории 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
Класс 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
Класс абстрактной истории
-
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
резюме
- В этом разделе мы сделали краткий анализ трех режимов маршрутизации, а также рассмотрели требования для реализации этих трех режимов маршрутизации.
HistoryКак создается экземпляр класса - Слишком далеко,
VueRouterВесь процесс создания экземпляра в основном завершен - Ниже мы кратко суммируем диаграмму
VueRouterпроцесс создания экземпляра - 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-адреса, а затем перейти к нему
- Поскольку 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
Суммировать
- Статья в основном начинается с общих идей дизайна внешней маршрутизации и постепенно анализирует основные принципы проектирования внешней маршрутизации, чтобы прояснить идеи дизайна.
- Затем введите глобальную
vue-routerнесколько терминов - После общего впечатления от терминологии введем
vue-routerструктура каталогов, как она иерархична - Разобравшись со структурой каталогов, мы начинаем анализировать с установки, чтобы понять
vue-routerЧто вы делали во время установки - После окончания установки вводим еще несколько
History类Каковы отношения наследования между ними и как они создаются? - Создание экземпляра завершено, и мы, наконец, вводим процесс инициализации
PS
- Остальное будет представлено позже, если вы считаете, что это нормально, вы можете поставить лайк ✨
- личный
githubgithub.com/BryanAdamss, также резюмирует некоторые вещи, добро пожаловать звездочка- доска для рисования на холстеdrawing-board
- Вводная демонстрация внешнего интерфейса, сборник лучших практикfe-awesome-demos
- автоматически сгенерированный псевдоним
vue-cli-pluginУууу, эта лошадь plus.com/package/v ue…
- 🏆 Техническое эссе Nuggets | Специальная двойная секция
Ссылаться на
- GitHub.com/но я делаю/блог/я…
- GitHub.com/но я делаю/блог/я…
- GitHub.com/но я делаю/блог/я…
- GitHub.com/v UE JS/v UE-день…
- nuggets.capable/post/684490…
- nuggets.capable/post/684490…
- nuggets.capable/post/684490…
- nuggets.capable/post/684490…
- у-у-у. Краткое описание.com/afraid/29 oh 8214's 0…
- США ТБ Huang Yi.GitHub.IO/v UE-Anarias…
- blog.Liuyunzhuge.com/2020/04/08/…
общаться с
- Если у вас есть какие-либо вопросы, вы можете добавить WeChat, чтобы общаться, расти вместе и вместе добиваться прогресса~