Следуйте инструкциям, вы также можете написать VueRouter от руки

внешний интерфейс JavaScript Vue.js
Следуйте инструкциям, вы также можете написать VueRouter от руки

Заявление: Эта статья является первой подписанной статьей сообщества Nuggets, и ее перепечатка без разрешения запрещена.

написать впереди

VueRouter, несомненно, постоянно используется каждым разработчиком Vue, но что вы знаете о его исходном коде?

Я полагаю, что когда большинство фронтендов говорят о маршрутизации, они могут сказать, что их ядроhashа такжеhistoryдва режима,hashрежим прослушиванияhashchangeвыполнение мероприятия,historyрежим прослушиванияpopstateповторное использование событияpushstateИзмените URL-адрес для достижения, как вы думаете, вы понимаете? Или вы действительно думаете, что знание этого — суть VueRouter? Нет, далеко не так! ! !

На самом деле, мне нравятся большинство людей, не поставили перед умом VuerOter, что это очень простое. Но когда я начал читать источник Vuerooter, это не так просто, как я думал. Источник Vuerooter об общей структуре простой, но детали все еще хотят читать трудные, загадочные различные функции для достижения разделения, а также некоторые детали заставляют меня хотеть молчать, поэтому я прочитал исходную кошку Подумайте о глубине глубины этого способа, не желая направлять GAN две ночи перед большими целями.

В центре внимания этой статьи

Без лишних слов, давайте посмотрим, что вы можете узнать, прочитав эту статью?

Привнес немного здравого смысла в Router и написал упрощенную версию VueRouter (большинство основных функций).В отличие от большинства рукописных статей, код здесь полностью реализован шаг за шагом в соответствии со стандартом исходного кода, включая общую архитектуру. , API и т. д. все одинаковы. Следите за этой статьей еще раз. В дополнение к глубокому пониманию основного исходного кода вы можете легко получить доступ к деталям исходного кода позже. Кажется, что настоящий исходный код можно сказать без преувеличения: вертикальный Наслаждайтесь шелковистая гладкость!

Советы перед чтением

Эта статья основана на последней и наиболее стабильной версии VueRouter V3.5.2, 4.0+ все еще впереди, поэтому она выходит за рамки этой статьи.

Статья с исходным кодом очень скучная и мало кто ее читает, потому что она сложна для понимания и не несет практического удовольствия, поэтому рекомендуется вынуть редактор и следовать за ним руками.

Что касается написанной вручную реализации VueRouter в этой статье, то она в основном включает:

  • маршрутизация в режиме хэш/история
  • Вложенные маршруты
  • компоненты router-view/router-link
  • router/router/route
  • такие методы, как push/replace/go/back
  • addRoute/addRoutes/getRouters
  • router hook

Нереализованная часть также даст общее введение, и я аннотирую исходный код, который я только что клонировал, и поместил его в каталог проекта рукописного исходного кода (ссылка в конце статьи).Перейдите непосредственно к исходному коду, набор комбинаций ударов, неплохо, давай~

Перед началом можно просто посмотреть три блок-схемы, соответствующие всему Vuerouter, не понять, есть общее впечатление, и в конце статьи будет эта цифра.

Принцип реализации маршрутизации передней части

Внешняя маршрутизация означает, что внешний интерфейс отслеживает изменения URL-адресов, чтобы управлять отображением компонентов на странице, чтобы обеспечить переходы без обновления страницы.Хотя пользователи чувствуют, что они представляют собой группу разных страниц, на самом деле все они находятся на одной странице. Чтобы реализовать интерфейсную маршрутизацию, нам необходимо учитывать два момента:

  • URL меняется, но страница не обновляется?
  • Отслеживать изменения URL?

Далее давайте посмотрим, как решаются два режима Hash и History.

Простая реализация хеш-маршрутизации

Режим хеширования фактически переключает маршрут, изменяя хеш-значение после # в URL-адресе, потому что изменение хэш-значения в URL-адресе не приведет к обновлению страницы, а затем используйте событие hashchange для отслеживания изменения хеш-значения на управлять отрисовкой компонентов страницы. Небольшой пример:

<!DOCTYPE html>
<html lang="en">
<body>
  <a href="#/home">home</a>
  <a href="#/about">about</a>
  <!-- 渲染路由模块 -->
  <div id="view"></div>
</body>
<script>
  let view = document.querySelector("#view")

  let cb = () => {
    let hash = location.hash || "#/home";

  }
  window.addEventListener("hashchange", cb)
  window.addEventListener("load", cb)
</script>
</html>

Как и выше, изменение хеш-значения маршрутизации с помощью двух тегов a эквивалентноrouter-linkкомпонент, страницаid=viewdiv мы можем понять это какrouter-viewComponent, после загрузки страницы сначала выполните функцию cb для инициализации модулей хеширования и маршрутизации.После нажатия тега a изменение маршрутизации будет отслеживаться с помощью hashchange, чтобы вызвать обновление модуля маршрутизации.

Простая реализация маршрутизации истории

Существует также способ без #, то есть история, которая предоставляет два метода, pushState и replaceState. Используя эти два метода, вы можете изменить путь URL-адреса, не вызывая обновления страницы. В то же время, он также предоставляет событие popstate для мониторинга изменений маршрута, но события popstate не срабатывают при их изменении, как это делает hashchange.

  • Изменение URL-адреса при переходе вперед или назад в браузере вызовет событие popstate.
  • js может вызвать это событие, вызвав методы back, go, forward и другие методы historyAPI.

Давайте посмотрим, как он реализует мониторинг маршрутизации:

<!DOCTYPE html>
<html lang="en">
<body>
  <a href='/home'>home</a>
  <a href='/about'>about</a>
  <!-- 渲染路由模块 -->
  <div id="view"></div>
</body>
<script>
  let view = document.querySelector("#view")

  // 路由跳转
  function push(path = "/home"){
    window.history.pushState(null, '', path)
    update()
  }
  // 更新路由模块视图
  function update(){
    view.innerHTML = location.pathname
  }

  window.addEventListener('popstate', ()=>{
    update()
  })
  window.addEventListener('load', ()=>{
    let links = document.querySelectorAll('a[href]')
    links.forEach(el => el.addEventListener('click', (e) => {
      // 阻止a标签默认行为
      e.preventDefault()
      push(el.getAttribute('href'))
    }))
    push()
  })
</script>
</html>

Как и выше, тег arouter-linkкомпонент, divrouter-viewкомпоненты.

Поскольку событие popstate может отслеживать браузер только вперед и назад и использовать API истории вперед и назад, в дополнение к операции обновления в мониторинге событий также необходимо вручную обновлять модуль маршрутизации при переходе.

Таким образом можно добиться того же эффекта, что и при использовании хэша.В то же время, поскольку тег a имеет поведение перехода по щелчку по умолчанию, мы предотвращаем это поведение. В то же время мы можем изменить обновление URL-адреса прямо в браузере, но в этом примере это не поддерживается, поскольку для этого требуется взаимодействие с серверной частью.

Вышеупомянутый упрощенный принцип режима хеширования и режима истории Зная эти основы, мы можем начать писать VueRouter

Анализ использования VueRouter

Прежде чем писать VueRouter, нам нужно проанализировать уровень его использования, чтобы увидеть, что у него есть.Давайте сначала рассмотрим его использование:

  • Внесите VueRouter в файл конфигурации маршрутизации и используйте его как плагин.
  • Настройте объект маршрутизации в файле конфигурации маршрутизации, чтобы создать экземпляр маршрутизации и экспортировать его.
  • Смонтируйте экземпляр маршрутизатора, экспортированный файлом конфигурации, в корневой экземпляр Vue.

Все шаги следующие:

// router/index.js
import Vue from "vue";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
    component,
  },
  {
    path: "/about",
    name: "About",
    component,
  }
];

const router = new VueRouter({
  mode: "hash",
  base: process.env.BASE_URL,
  routes,
});

export default router;

В файле проекта main.js:

// main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

Как видите, VueRouter можно создать как класс, а также загрузить как плагин Vue.

Создание экземпляра легко понять, но зачем загружать плагин?

Когда мы используем VueRouter, мы часто используемrouter-linkа такжеrouter-viewДва компонента, мы не нашли куда внедрить эти два компонента, вы когда-нибудь задумывались, почему их можно использовать глобально? На самом деле он регистрируется глобально, когда VueRouter инициализируется как плагин.

Во время использования мы можем использоватьthis.$routerПолучите экземпляр маршрутизации, и там будут такие объекты, какpush/go/backи другие методы, вы также можетеthis.$routeчтобы получить объект маршрута только для чтения, который включает в себя наш текущий маршрут и некоторые параметры и т. д.

Подготовка перед рукописным вводом

Строительство проекта

Создайте проект Vue и используйте терминал, чтобы ввести следующую команду для создания проекта Vue:

vue create hello-vue-router

Обратите внимание, что при сборке выбран VueRouter!

Сборка завершается напрямуюyarn serveЗапустите, как показано ниже, очень знакомый интерфейс:

Тогда мыsrc/создать новую папкуhello-vue-router/, код VueRouter, который мы написали сами, находится в этой папке.

Создать новыйindex.jsЗадокументируйте, экспортируйте пустой класс Vuerouter:

/*
 * @path: src/hello-vue-router/index.js
 * @Description: 入口文件 VueRouter类
 */

export default class VueRouter(){
  constructor(options){}
}

Затем перейдите к файлу конфигурации маршрутизацииsrc/router/index.js, замените импортированный VueRouter нашим собственным и измените режим маршрутизации на хэш, потому что сначала мы должны реализовать хэш-режим, как показано ниже:

import Vue from 'vue'
import VueRouter from '@/hello-vue-router/index'
// import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [...]

const router = new VueRouter({
  mode: 'hash',
  base: process.env.BASE_URL,
  routes
})

export default router

Теперь страница становится пустой, а консоль сообщает об ошибке:

Cannot call a class as a function

Ошибка консоли говорит, что класс нельзя вызывать как функцию! ! !

Эй, а где ты сказал, что класс вызывается как функция?

ФактическиVue.use(VueRouter)Это, говоря об этом, мы должны представить API этого плагина установки Vue.

Анализ исходного кода Vue.use()

Как следует, по сути, этот метод принимает параметр типа function или object. Если аргумент является объектом, он должен иметь метод свойства install. Независимо от того, является ли параметр функцией или объектом, конструктор Vue будет передан в качестве первого параметра при выполнении метода установки или самой функции.

Таким образом, когда мы пишем плагин, если мы пишем функцию или объект с атрибутом функции установки, мы можем получить конструктор Vue, и мы можем использовать его для некоторых вещей.Это очень просто!

Vue.use = function (plugin: Function | Object) {
  // installedPlugins为已安装插件列表,若 Vue 构造函数不存在_installedPlugins属性,初始化
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  // 判断当前插件是否在已安装插件列表,存在直接返回,避免重复安装
  if (installedPlugins.indexOf(plugin) > -1) {
    return this
  }

	// toArray方法将Use方法的参数转为数组并删除了第一个参数(第一个参数就是我们的插件)
  const args = toArray(arguments, 1)
  // use是构造函数Vue的静态方法,那这里的this就是构造函数Vue本身
  // 把this即构造函数Vue放到参数数组args的第一项
  args.unshift(this)
  if (typeof plugin.install === 'function') {
    // 传入参数存在install属性且为函数
    // 将构造函数Vue和剩余参数组成的args数组作为参数传入install方法,将其this指向插件对象并执行install方法
    plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
    // 传入参数是个函数
    // 将构造函数Vue和剩余参数组成的args数组作为参数传入插件函数并执行
    plugin.apply(null, args)
  }
  // 像已安装插件列表中push当前插件
  installedPlugins.push(plugin)
  return this
}

Метод установки предварительной сборки

Теперь давайте начнем писать код! Теперь, когда вы знаете, как Vue загружает плагины, это легко, потому что мы экспортируем класс VueRouter, который также является объектом, поэтому добавьте к нему метод установки.

немного изменитьindex.js, добавьте статический метод install в класс VueRouter:

/*
 * @path: src/hello-vue-router/index.js
 * @Description: 入口文件 VueRouter类
 */
import { install } from "./install";

export default class VueRouter(){
  constructor(options){}
}
VueRouter.install = install;

затем вsrc/hello-vue-router/создать каталогinstal.js, экспортировать метод установки, мы виделиVue.use()В исходном коде метода должно быть известно, что первым параметром этого метода является конструктор Vue, а именно:

/*
 * @path: src/hello-vue-router/install.js
 * @Description: 插件安装方法install
 */
export function install(Vue){}

Это также было проанализировано выше: когда плагин установлен, метод установки будет глобально монтировать два компонента в Vue.router-viewа такжеrouter-link.

Вы знаете, мы делаем только 2 вещи в файле конфигурации маршрутизатора, чтобы инициализировать плагин VueRouter и создать экземпляр VueRouter, затем мы обычно используем его непосредственно в проекте.this.$router & this.$routeОткуда это?

первый$routerявляется экземпляром объекта VueRouter,$routeтекущий объект маршрутизации,$routeНа самом деле это$routerСвойство , эти два объекта доступны во всех компонентах Vue.

Некоторые друзья могут еще помнить вступительный файл проектаmain.js, мы подключаем экспортированный экземпляр маршрутизатора к корневому экземпляру Vue следующим образом:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: function (h) { return h(App) }
}).$mount('#app')

Но проблема возникает снова, мы просто монтируем его в корневой экземпляр, не каждый компонент прикрепляется, и Vue помещает объекты, непосредственно смонтированные в экземпляре Vue, в текущий экземпляр для нас.$optionsС точки зрения свойств, в сочетании с тем фактом, что мы подключены только к корневому экземпляру, мы можем получить доступ к объекту экземпляра маршрутизатора, только взявthis.$root.$options.routerчтобы получить это, здесьthis.$rootПолученный корневой экземпляр является корневым экземпляром.

Очевидно, внешнее так не называется.

так,$router & $routeЭти два свойства можно смонтировать только внутри компонента VueRouter, а также их необходимо использовать всеми компонентами во время разработки проекта Vue.

Подробности, как получить его объект-экземпляр в компоненте VueRouter (как получить новый объект VueRouter в этом классе)?

Некоторые друзья могли подумать, что этот экземпляр маршрутизатора смонтирован в корневом экземпляре Vue, да, это маршрутизатор, который передается при создании нового Vue. Вы можете получить это, как вы можете это получить?

Как упоминалось выше, мы можем сначала получить корневой экземпляр Vue, а затем использовать$options.routerЧтобы получить атрибут маршрутизатора, смонтированный на экземпляре, то есть в настоящее время рассматривается вопрос о том, как получить экземпляр компонента Vue в VueRouter (с экземпляром компонента вы можете получить экземпляр корневого компонента для доступа к нему.$optionsАтрибуты)

Эй, кажется, я снова подумал об этом. Метод установки VueRouter будет передан в конструктор Vue. Может ли он что-то делать?

Конструктор есть конструктор, это конечно не экземпляр, но конструктор у Vue естьmixinпуть, да混入

маленький подсказки: v УЭ.Суеверие

Подсчитано, что этот метод известен многим, но его необходимо внедрить.

Миксин делится на глобальный миксин и миксин компонента.Мы напрямую используем конструктор Vue.mixin.Это глобальный миксин.Он получает объектный параметр.В этом объектном параметре мы можем написать что угодно в компоненте Vue,а потом пишем это Куча будет смешана (также понимаемая как объединенная) с каждым компонентом Vue.

Например, если написать жизненный цикл и прописать в нем логику, то во всех компонентах Vue смешанная нами логика будет выполняться до начала жизненного цикла. Все еще не знаю? Для другого примера мы написалиmethods, который пишет функцию, то эта функция будет смешана со всеми компонентами Vuemethods, все компоненты можно вызывать напрямую.

Vue.mixin может напрямую записывать набор компонентов, что очень просто, можно записать глобальный миксин жизненного цикла в компонент.

Тогда снова возникает вопрос, в каком жизненном цикле это записано? На самом деле тоже все просто, достаточно посмотреть, какой жизненный цикл$optionsЕго можно построить,beforeCreateэтот цикл$optionsОн построен, то есть его можно использовать после этого жизненного цикла$options, нужно ли спрашивать? Конечно, чем раньше, тем лучше, т.beforeCreateэтот жизненный цикл.

Опять же, метод установки может передать параметр конструктору Vue и использовать миксин статического метода конструктора Vue для всех наших компонентов.beforeCreateЖизненный цикл перемешан с кусочком логики, который на нем смонтирован$router & $routeАтрибуты

Согласно нашей логике выше, мы начнем с полного кода, а затем объясним его шаг за шагом:

/*
 * @path: src/hello-vue-router/install.js
 * @Description: 插件安装方法install
 */

export let _Vue;

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

  _Vue = Vue;

  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._route = {};
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
    },
  });

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

  Vue.component('RouterView', {});
  Vue.component('RouterLink', {});  
}

Чтобы объяснить это блоком:

export _Vue;

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

  _Vue = Vue;
}

А? Файл установки не только экспортирует метод установки, но также экспортирует переменную _Vue.Что это такое?

При инициализации плагина выполняется метод install.В этом методе строковый параметр, то есть конструктор Vue, присваивается переменной _Vue и экспортируется.На самом деле у этого _Vue две функции:

Во-первых, чтобы плагин не регистрировался и не устанавливался несколько раз, потому что в методе установки плагина мы добавляем установленный атрибут к этому методу.Когда этот атрибут существует и имеет значение true, а _Vue был назначен конструктору Vue, верните плагин зарегистрирован, перерегистрировать не нужно.

Вторая функция заключается в том, что в конструкторе Vue установлено множество практических API, которые мы можем использовать в классе VueRouter.Конечно, мы также можем использовать его API, представив Vue, но как только пакет будет введен, весь Vue будет упакован. Упаковано, даже если конструктор будет передан в качестве параметра в инсталле, бывает, что когда мы пишем конфигурационный файл роутера, установочный плагин (Vue. Мы присваиваем параметр конструктора переменной и используем его в Класс VueRouter идеален, если не понимаете, то просто посмотрите на картинку⬇️

Далее разберем микс-ин, по сути, грубо говоря, это и есть крепление$router & $route:

export function install(Vue){  
  // 全局注册混入,每个 Vue 实例都会被影响
  Vue.mixin({
    // Vue创建前钩子,此生命周期$options已挂载完成
    beforeCreate() {
      // 通过判断组件实例this.$options有无router属性来判断是否为根实例
      // 只有根实例初始化时我们挂载了VueRouter实例router(main.js中New Vue({router})时)
      if (this.$options.router) {
        this._routerRoot = this;
        // 在 Vue 根实例添加 _router 属性( VueRouter 实例)
        this._router = this.$options.router;
        this._route = {};
      } else {
        // 为每个组件实例定义_routerRoot,回溯查找_routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
    },
  });

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

Смотрим что сделано:

Сначала напишите миксин, зарегистрируйте миксин глобально, чтобы он затронул каждый экземпляр Vue. Напишите хук beforeCreate в миксине, потому что этот жизненный циклoptionsСамое раннее крепление завершено. И из-за глобального смешения, поэтомуbeforeCreateВ хуке мы прописали проход в экземпляре компонентаthis.options монтируется первым. И из-за глобального смешивания в хуке beforeCreate мы написали проход через this.​options имеет атрибут маршрутизатора, чтобы определить, является ли он корневым экземпляром. Маршрутизатор экземпляра VueRouter монтируется только при инициализации корневого экземпляра (то есть, когда New Vue({router}) в main.js).

является корневым экземпляром:

Если это корневой экземпляр, добавьте к нему атрибут _router, значением является экземпляр VueRouter, и добавьте атрибут _routerRoot для его монтирования, который является корневым экземпляром.

Как проанализировано выше, здесь также должен быть объект маршрута, поэтому я добавил к нему атрибут _route в конце и установил его на пустой объект на данный момент, и я улучшу его позже.

не корневой экземпляр:

Если это не корневой экземпляр, это экземпляр дочернего компонента. Найдите его родительский экземпляр, чтобы определить, имеет ли его родительский экземпляр атрибут _routerRoot, а если нет, добавьте ссылку на него, чтобы убедиться, что каждый экземпляр компонента может иметь атрибут _routerRoot , то есть пусть каждый экземпляр компонента имеет атрибут _routerRoot.Каждый компонент может ссылаться и обращаться к корневому экземпляру.Обратите внимание, что это не повторяющиеся присваивания, а ссылки между объектами.

Наконец, чтобы сделать каждый компонент доступным$router $ $routeобъект, который мы добавили в прототип Vuerouterатрибут и прокси дляthis.routerRoot.router, такжеVueдобавлено в прототипсвойство маршрутизатора и прокси для `this._routerRoot._router`, также добавлено` в прототипе Vueroute属性并代理到this._routerRoot._route`, остальное — создать глобальный компонент:

// 全局注册组件router-view
Vue.component('RouterView', {});
// 全局注册组件router-link
Vue.component('RouterLink', {}); 

Эта часть пока относительно проста: два компонента регистрируются глобально с помощью Vue.component, а объекты конфигурации непосредственно пусты. Ниже приведена простая конфигурация этих двух глобальных компонентов, позволяющая запустить проект, в конце концов, операция по-прежнему сообщает об ошибках.

Предварительная сборка компонентов RouterView и RouterLink

Немного разделившись, мы вsrc/hello-vue-router/Создайте новый в каталогеcomponents/папка

существуетcomponentsновая папкаview.jsа такжеlink.jsДва файла, а затем вам нужно сначала изменить метод установки:

/*
 * @path: src/hello-vue-router/install.js
 * @Description: 插件安装方法install
 */
import View from "./components/view";
import Link from "./components/link";

export function install(Vue){
  // 全局注册组件router-view
  Vue.component('RouterView', view);

  // 全局注册组件router-link
  Vue.component('RouterLink', link);  
}

Видно, что мы вытащили объекты конфигурации двух компонентов отдельно и записали их в два файла, по сути, каждый файл экспортирует объект конфигурации компонента.

Первый взглядlink.js, компонент ссылки аналогичен тегу a. На самом деле он по умолчанию отображает тег a. Компонент получает параметр to, который может быть объектом или строкой, которая используется в качестве перехода.

<router-link to="/home">
<router-link :to="{path: '/home'}">

См. реализацию:

/*
 * @path: src/hello-vue-router/components/link.js
 * @Description: router-link
 */
export default {
  name: "RouterLink",
  props: {
    to: {
      type: [String, Object],
      require: true
    }
  },
  render(h) {
    const href = typeof this.to === 'string' ? this.to : this.to.path
    const router = this.$router
    let data = {
      attrs: {
        href: router.mode === "hash" ? "#" + href : href
      }
    };
    return h("a", data, this.$slots.default)
  }
}

Во-первых, props получает параметр to, обязательную опцию, которая может быть типа объекта или строки.В функции рендеринга сначала оценивается тип параметра to, и он объединяется в объект.

Затем получите доступ к корневому экземпляру в$router, это на самом деле прокси, после вывода вы узнаете, что этот прокси проксирует экземпляр VueComponent, и мы добавляем атрибут _routerRoot, указывающий на корневой экземпляр, к каждому экземпляру компонента при установке, здесь мы действительно хотим получить доступ к маршрутизатору объект Есть много видов.

// this._self._routerRoot._router
// this._routerRoot._router
// this.$router

Вы можете использовать любой, но третий тип исходного кода, мы также используем его, он может иметь наименьшее количество символов

Следующим шагом будет возврат VNode.На самом деле параметр h рендера это функция createElement, которая используется для создания VNode.Её параметры описаны на официальном сайте:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

Здесь мы хотим вернуть тег a, поэтому первым параметром является строка a, а вторым параметром является объект данных, соответствующий атрибуту тега.Чтобы вывести ему атрибут href, значением атрибута является параметр to, а режим на что нужно обратить внимание: Проблема в том, что в хеш-режиме перед всеми путями перехода добавляется знак #, поэтому необходимоrouter.modeСудя по режиму, третий параметр — это дочерний узел, т.е.router-linkЗначение, содержащееся в компоненте, можно получить, используя слот по умолчанию.this.$slots.defaultПолучить слот по умолчанию.

Хорошо, поехалиrouter-linkКомпонент почти готов, но еще есть проблемы в режиме истории, о которых мы поговорим позже.

увидеть сноваview.js, На самом деле нам не нужен компонент RouterView для рендеринга чего-либо.В лучшем случае это заполнитель для замены нашего UI модуля компонента, поэтому одному не нужен жизненный цикл, второму не требуется управление состоянием, а третьему не требуется различные мониторинги, популярные Дело в том, что нет необходимости создавать экземпляр.В качестве трехбесплатного компонента больше всего подходят функциональные компоненты.

/*
 * @path: src/hello-vue-router/components/view.js
 * @Description: router-view
 */
export default {
  name: "RouterView",
  functional: true, // 函数式组件
  render(h) {
    return h('div', 'This is RoutePage')
  }
}

Как и выше, прямо установите его в функциональный компонент, а затем функция рендеринга напрямую вернет div с содержимым'This is RoutePage'(функция h, то есть функция createElement, не имеет второго параметра, который можно опустить), здесь просто предварительная структура, логика будет рассмотрена позже, пусть сначала запустится страница, теперь вы открываете браузер и вы обнаружите, что ошибки нет, навигация также доступна, и вы можете нажать, чтобы переключить маршруты, то есть компонент модуля маршрутизацииrouter-viewвсегда только показыватьThis is RoutePage,следующим образом:

Предварительная конструкция класса Vuerouter

Метод установки можно пока закончить и подумать, что нам нужно сделать в классе VueRouter?

В первую очередь необходимо анализировать параметры при получении параметров.Передается объект, и основными из них являются два атрибута:

  • режим маршрутизации
  • Маршруты Маршрута маршрутизации Массив конфигурации

На самом деле базовый атрибут тоже важнее, но на это можно сначала не обращать внимания, после того, как логика проработает, будет время его улучшить

Думая о конфигурации режима, нам нужно инициализировать некоторые вещи в соответствующем режиме в соответствии с режимом маршрутизации, переданным режимом, чтобы реализовать мониторинг маршрутизации в этом режиме.

Затем снова подумайте о массиве маршрутов, что нам нужно сделать?

На самом деле самая важная конфигурация в этом массиве это путь маршрутизации и компонент маршрутизации соответствующий пути.Конечно, есть еще некоторые настройки перенаправления, динамической маршрутизации, имени маршрутизации, псевдонима маршрутизации, которые пока не рассматриваются , и будет постепенно улучшаться позже.

Вопрос в том, что нам нужно делать при прослушивании смены маршрута?

Разумеется, получить измененный путь маршрута, найти соответствующую конфигурацию пути в массиве маршрутов, получить его компоненты, а затем отрендерить полученные компоненты в соответствующийrouter-viewвходить.

Для конфигурации маршрутов цель очень ясна, потому что это объект массива с древовидной структурой, мы сопоставляем на основе пути, что очень неудобно, поэтому эту конфигурацию необходимо проанализировать заранее как{key : value}В этой структуре, разумеется, ключ — это наш путь, а значение — элемент конфигурации этого маршрута. После завершения анализа начните вводить код:

/*
 * @path: src/hello-vue-router/index.js
 * @Description: 入口文件 VueRouter类
 */
import { install } from "./install";
import { createMatcher } from "./create-matcher";
import { HashHistory } from "./history/hash";
import { HTML5History } from "./history/html5";
import { AbstractHistory } from "./history/abstract";
const inBrowser = typeof window !== "undefined";

export default class VueRouter(){
  constructor(options) {
    // 路由配置
    this.options = options;
    // 创建路由matcher对象,传入routes路由配置列表及VueRouter实例,主要负责url匹配
    this.matcher = createMatcher(options.routes);

    let mode = options.mode || "hash";

    // 支持所有 JavaScript 运行环境,非浏览器环境强制使用abstract模式,主要用于SSR
    if (!inBrowser) {
      mode = "abstract";
    }

    this.mode = mode;

    // 根据不同mode,实例化不同history实例
    switch (mode) {
      case "history":
        this.history = new HTML5History(this);
        break;
      case "hash":
        this.history = new HashHistory(this);
        break;
      case "abstract":
        this.history = new AbstractHistory(this);
        break;
      default:
        if (process.env.NODE_ENV !== "production") {
          throw new Error(`[vue-router] invalid mode: ${mode}`);
        }
    }
  }
}
VueRouter.install = install;

На самом деле логика в конструкторе класса VueRouter очень проста: он оценивает режим входящего режима, а затем инициализирует разные экземпляры класса.Хотя экземпляры разных классов создаются, методы экземпляров, включая атрибуты, одинаковы.

Полный VueRouter имеет три режима:

  • Хэш поддерживается всеми основными браузерами, но URL-адрес имеет знак #, что выглядит некрасиво.
  • URL-адрес истории выглядит хорошо, но некоторые старые браузеры его не поддерживают.
  • Аннотация поддерживает все среды, в основном используется для SSR Server Side SSR

То, что мы не очень хорошо знаем, может быть абстрактным режимом.На самом деле, этот режим официально определен как режим, который поддерживает любую среду, потому что этот режим предназначен для ручного моделирования среды маршрутизации, и исходный код также имеет ту же логическую схему. суд как выше(inBrowser), то есть когда в текущей среде нет оконного объекта, то есть небраузерная среда, он напрямую принудительно переходит в этот режим, так что этот режим тоже в основном используется для SSR, и он достаточно прост реализовать его позже, если у вас есть энергия.

Весь конструктор на самом деле не имеет сложной логики. Сначала определите, есть ли в текущей среде объект окна, то есть является ли это средой браузера.Если да, продолжайте идти.Если нет, принудительно сделайте значение режима абстрактным, затем определите значение атрибута режима, и используйте соответствующий класс для инициализации режима маршрутизации, чтобы он соответствовал трем режимам. Например, отсутствие совпадения вызовет ошибку напрямую. Независимо от того, какой режим здесь, мы реализуем некоторые из тех же методов в соответствующем классе и смонтируем инициализированный экземпляр в атрибут истории экземпляра VueRouter.

На самом деле перед проверкой параметра режима также вводится метод createMatcher.Возвращаемое значение этого метода монтируется в атрибут matcher экземпляра VueRouter.Что он делает?

Вы должны были примерно догадаться, и, как я уже сказал выше, это, вероятно, сборка{key : value}Объект структуры (называемый объектом pathMap) упрощает сопоставление соответствующего модуля маршрутизации по пути пути.

Затем мы шаг за шагом выведем, как инкапсулируется метод createMatcher.

Вывод метода createMatcher

Как вы думаете, метод createMatcher просто создает объект сопоставления pathMap? Нет, в этом случае имя функции должно называться createRouterMap, На самом деле это было действительно это имя в начале, но набор производных обнаружил, что он может не только создавать объект сопоставления pathMap, но иaddRoutes/addRoute/getRoutesЭти методы также могут быть реализованы здесь.

Что делает конструкция объекта карты pathMap? Соответствие маршрута! Когда вы вводите путь, вы можете получить соответствующую информацию о конфигурации маршрутизации. Объект pathMap эквивалентен распорядителю данных маршрутизации. Все записанные конфигурации маршрутизации находятся здесь. При динамическом добавлении маршрута проанализируйте и добавьте новый объект маршрутизации в объект pathMap Итак, мы объединили все методы сопоставления маршрутов и динамической маршрутизации в функцию createMatcher, назовем ее路由匹配器函数Что ж, основная функция заключается в создании объекта сопоставления маршрутов, и эта функция возвращает объект, содержащий четыре атрибута метода:

  • соответствие маршруту соответствие
  • Addroutes динамически добавляет маршруты (параметры должны соответствоватьroutesнабор требований к опциям)
  • addRoute динамически добавлять маршрут (добавлять новое правило маршрутизации)
  • getRoutes Получить список всех активных записей маршрута

createRouteMap создает карту маршрута

Прежде всего, мы должны построить объект pathMap, вытащить отдельный файл, чтобы написать этот метод, вsrc/hello-vue-router/Создайте новый в каталогеcreate-route-map.jsдокумент:

/*
 * @path: src/hello-vue-router/create-route-map.js
 * @Description: 生成路由映射
 */
// 生成路由映射
export function createRouteMap(routes){
  let routeMap = {}
  routes.forEach(route => {
    routeMap[route.path] = route
  })
  return routeMap
}

Как и выше, несколько строк кода генерируют объект карты маршрутизации pathMap. Нет проблем, но мы сопоставляем только один уровень выше, и в конфигурации маршрутизации может быть бесконечное количество уровней подмаршрутов, например следующая конфигурация:

const routes = [
  {
    path: "/about",
    name: "About",
    component,
  },
  {
    path: "/parent",
    name: "Parent",
    component,
    children:[
      {
        path: "child",
        name:"Child",
        component
      }
    ]
  }
];

Какой объект pathMap мы хотим сгенерировать, он выглядит так:

{
  "/about": {...},
  "/parent": {...},
  "/parent/child": {...}
}

Но текущая логика кода генерирует только следующее:

{
  "/about": {...},
  "/parent": {...}
}

Есть проблема? Есть большая проблема, один слой роутинга ок, а многоуровневый вложенный роутинг прямо gameover. Поэтому для рекурсивной обработки парсинга и изменения кода, это все еще старая процедура, сначала посмотрите на полный код, а затем пошагово разберите его.

export function createRouteMap(routes){
  const pathMap = Object.create(null);
  // 递归处理路由记录,最终生成路由映射
  routes.forEach(route => {
    // 生成一个RouteRecord并更新pathMap
    addRouteRecord(pathMap, route, null)
  })
  return pathMap
}

// 添加路由记录
function addRouteRecord(pathMap, route, parent){
  const { path, name } = route

  // 生成格式化后的path(子路由会拼接上父路由的path)
  const normalizedPath = normalizePath(path, parent)

  // 生成一条路由记录
  const record = {
    path: normalizedPath, // 规范化后的路径
    regex: "", // 利用path-to-regexp包生成用来匹配path的增强正则对象,用来匹配动态路由 (/a/:b)
    components: route.component, // 保存路由组件,省略了命名视图解析
    name,
    parent, // 父路由记录
    redirect: route.redirect, // 重定向的路由配置对象
    beforeEnter: route.beforeEnter, // 路由独享的守卫
    meta: route.meta || {}, // 元信息
    props: route.props == null ? {} : route.props// 动态路由传参
  }

  // 处理有子路由情况,递归
  if (route.children) {
    // 遍历生成子路由记录
    route.children.forEach(child => {
      addRouteRecord(pathMap, child, record)
    })
  }

  // 若pathMap中不存在当前路径,则添加pathList和pathMap
  if (!pathMap[record.path]) {
    pathMap[record.path] = record
  }
}

// 规格化路径
function normalizePath(
  path,
  parent
) {
  // 下标0为 / ,则是最外层path
  if (path[0] === '/') return path
  // 无父级,则是最外层path
  if (!parent) return path
  // 清除path中双斜杆中的一个
  return `${parent.path}/${path}`.replace(/\/\//g, '/')
}

На самом деле этот кусок кода относительно прост, да еще и закомментирован, всего несколько моментов.

Мы фактически форматируем каждый объект конфигурации маршрутизации в рекурсии и генерируем новый объект записи.Путь объекта фактически является полным путем, то есть, если исходный путь/В начале это означает, что это маршрут верхнего уровня, а путь — это он сам.Если исходный путь не начинается с/В начале указав, что это дочерний маршрут, затем нам нужно склеить родительский путь, по этой причине мы написали отдельную функцию normalizePath для генерации полного пути, то есть нормализации пути.

Поскольку родитель передается во время рекурсии, за исключением того, что маршрут верхнего уровня равен нулю, у дочерних маршрутов есть родители, а наша рекурсия дочернего маршрута происходит после создания объекта записи, поэтому каждый входящий родитель форматируется. Для хорошего объекта записи путь родителя также является полным путем, так что независимо от количества дочерних элементов можно указать полный путь.

Затем поговорим об объекте записи, мы также добавили к нему родительский атрибут, указывающий на его родительский объект, чтобы была связь между родителем и дочерним элементом, и некоторые настраиваемые параметры маршрутизации, такие как перенаправление.redirect, эксклюзивный охранник маршрутаbeforeEnter, метаинформацияmeta, название маршрутаnameОни также принимаются и помещаются в объект записи.

говорить в одиночествеregexатрибут, я думаю, все знают, что VueRouter поддерживает динамическую маршрутизацию, на самом деле он в основном использует трехсторонний пакетpath-to-regexpСоздайте расширенный регулярный объект, используемый для сопоставления пути с соответствующим динамическим маршрутом. После создания регулярного объекта поместите его наregexВ свойствах этот кусок особого значения для нашего почерка не имеет, поэтому я его писать не стал, а оставил пустым.Если вам интересно, можете прямо посмотреть исходники здесь.Главное этоpath-to-regexpИспользование этого пакета не сложно. Вдобавок последнийpropsАтрибуты используются для параметров динамической маршрутизации, так что пока их можно игнорировать.

В конце концов, сгенерированный объект PathMap[{path: record}...]В этом формате ключ представляет собой форматированный полный путь, а значение — форматированную запись объекта конфигурации маршрутизации.

На этом метод разбора объекта карты маршрута pathMap почти завершен.

createMatcher генерирует сопоставители маршрутов

Далее мыsrc/hello-vue-router/создать папкуcreate-matcher.jsФайл, согласно нашему анализу выше, примерно структурирован следующим образом:

/*
 * @path: src/hello-vue-router/create-route-map.js
 * @Description: 路由匹配器Matcher对象生成方法
 */
import { createRouteMap } from "./create-route-map";

export function createMatcher(routes){
  // 生成路由映射对象 pathMap
  const pathMap = createRouteMap(routes)

  // 动态添加路由(添加一条新路由规则)
  function addRoute(){ }

  // 动态添加路由(参数必须是一个符合 routes 选项要求的数组)
  function addRoutes(){ }

  // 获取所有活跃的路由记录列表
  function getRoutes(){ }

  // 路由匹配
  function match(){ }

  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

Метод создания объекта Matcher для сопоставления маршрутов — createMatcher , нам нужен только один параметр, то есть массив маршрутов, необходимый для создания объекта pathMap карты маршрутов (то есть маршрутов в файле конфигурации маршрутизатора).

На самом деле объект карты маршрута pathMap можно использовать только при сопоставлении маршрутов и динамическом добавлении маршрутов, и эти ситуации включены вcreateMatcherфункция, поэтому вcreateMatcherВнутри функции напрямую используйте только что написанныйcreateRouteMapМетод генерирует объект pathMap, который поддерживается внутри при вызове функции, потому чтоcreateMatcherНесколько методов, возвращаемых функцией, имеют ссылки на объект pathMap, что является типичным сценарием закрытия, поэтому в процессе инициализации всего экземпляра VueRoutercreateMatcherФункцию нужно вызвать только один раз, и все в порядке,createRouteMapЭтот метод также предлагает способы динамического изменения pathMap .

Основная реализация addRoutes

Первый взглядaddRoutesЭто относительно просто. Определение этого API фактически используется для динамического добавления маршрутов. Простая точка состоит в том, чтобы проанализировать входящий новый объект маршрута и добавить его к старому объекту pathMap. Параметр должен быть массивом, отвечающим требованиям route. , функция позволяет нам добавлять несколько конфигураций маршрутизации в любое время и в любом месте, поскольку параметры представляют собой массивы и имеют тот же формат, что и маршруты, поэтому их можно полностью использовать повторно.createRouteMapметод.

первыйcreateRouteMapМетод просто модифицируется, достаточно добавить параметр в ok, с логикой проблем нет.

// 新增 oldPathMap 参数
export function createRouteMap(routes, oldPathMap){
  // const pathMap = Object.create(null); old
  const pathMap = oldPathMap || Object.create(null); // new
  
  // ...
}

Как и выше, при динамическом добавлении просто передайте старый pathMap. Прежде чем мы напрямую объявили пустой объект pathMap, вы можете судить здесьoldPathMapНезависимо от того, существует ли параметр, его существование назначается PathMap, и нет объекта по умолчанию или пустого объекта. Это упростит настройку, синтаксический анализ и добавление конфигурации, синтаксический анализ и добавление к старому объекту сопоставления, это просто?addRoutesСпособ еще проще:

// 动态添加路由(参数必须是一个符合 routes 选项要求的数组)
function addRoutes(routes){
  createRouteMap(routes, pathMap)
}

Основная реализация getRoutes

Что касаетсяgetRoutes, еще проще, верни напрямуюpathMapобъект

// 获取所有活跃的路由记录列表
function getRoutes(){
  return pathMap
}

Основная реализация addRoute

addRouteНам нужно уделить немного внимания этому методу, потому что этот метод будет основным для динамического добавления маршрутов в будущей версии 4.0+ и версии 3.0+.addRoute & addRoutesОба метода сосуществуют, но хорошо выглядят в 4.0+addRoutesМетод был удален, давайте сначала посмотрим, как его использовать.

addRouteЕсть два параметра, также 2 использования:

  • Добавьте новое правило маршрутизации. Если правило маршрутизации имеетname, а файл с таким именем уже существует, он будет перезаписан.
  • Добавьте новую запись правила маршрутизации в качестве дочернего маршрута существующего маршрута. Если правило маршрутизации имеетname, а файл с таким именем уже существует, он будет перезаписан.

Давайте говорить по-народному. Первый — передать объект конфигурации маршрутизации, обратите внимание, что это не предыдущийroutesМассив представляет собой объект только с одной конфигурацией маршрутизации.Конечно, вы можете написать бесчисленное количество подмаршрутов в рамках этой конфигурации маршрутизации, но при добавлении в этой форме может быть добавлен только один объект маршрутизации, и за один раз добавляется только одна запись , Если текущая маршрутизация существует в конфигурацииnameТа же запись будет перезаписана следующим образом:

this.$router.addRoute({
  path: "/parent",
  name: "Parent",
  component,
  children:[
    {
      path: "child"
      // ...
    },
    // ...
  ]
})

Второй — два параметра, первый параметр — существующий маршрутname, второй параметр — это объект конфигурации маршрутизации, который аналогичен объекту конфигурации маршрутизации, используемому в приведенном выше методе, за исключением того, что этот метод будет рассматривать объект конфигурации маршрутизации как первый параметр.nameДобавляется подразрушение соответствующего объекта маршрутизации, и он просто основан на маршруте.nameНепосредственно добавляйте подмаршруты, и в процессе добавления есть повторяющиеся маршрутыnameТакже покрыты.

Это выглядит сложно, но на самом деле это очень просто написать.createRouteMapдобавить одинparentпараметры. ИсправлятьcreateRouteMapфункция:

// 新增 parentRoute 参数
export function createRouteMap(routes, oldPathMap, parentRoute){  
  const pathMap = oldPathMap || Object.create(null);

  routes.forEach(route => {
    // addRouteRecord(pathMap, route, null) old
    addRouteRecord(pathMap, route, parentRoute) // new
  })
  return pathMap
}

Как показано выше, третий параметр представляет родительский маршрут. Когда вам нужно добавить его к записи, вам нужно только получить родительский маршрут и передать его. Если нет третьего параметра, по умолчанию он равенundefinedЭто не повлияет на следующую логику.

напиши дальшеaddRouteметод:

// 动态添加路由(添加一条新路由规则)
function addRoute(parentOrRoute, route){
  const parent = (typeof parentOrRoute !== 'object') ? pathMap[parentOrRoute] : undefined
  createRouteMap([route || parentOrRoute], pathMap, parent)
}

Как указано выше,addRouteПервым параметром метода может быть строка или объект маршрутизации.createRouteMapПервым параметром метода является массив маршрутизации, поэтому, когда мы его вызываем, массив оборачивается напрямую.По умолчанию используется второй параметр.Если второй параметр не существует, первым параметром является объект маршрутизации, а затем старый Объект pathMap передается, и последний родитель Нам нужно судить в начале функции.

Когда первый параметр не является объектом, то есть на входе есть маршрутnameСтрока, давайте немного изменим здесь, используем маршрутизациюpathВместо этого (просто поймите смысл) напрямую выньте нормализованный маршрут и назначьте его родителю через ранее разобранный объект pathMap.Если это объект, должен быть только один параметр, а родитель напрямую назначается как undefined, что идеально.

Объясните, почему вы не используете маршрутизацию, как официальнуюnameСоответственно, в дополнение к объекту pathMap исходный код также анализирует объект namePath. То, что мы написали, является упрощенной версией. Эти аналогичные вещи включают обработку имен маршрутов, псевдонимов маршрутов, параметров перенаправления и динамических маршрутов. Я опустил это. и сделал Обработку пути маршрутизации может понять каждый.Большая часть другой обработки такая же, и это очень просто.Это не вызывает привыкания и может быть выполнено мной самостоятельно с помощью исходного кода, который я аннотировал.Общая структура то же самое, просто добавьте еще немного кода.

сопоставить маршрутизацию, сопоставить базовую реализацию

Наконец, функция сопоставления маршрутовmatchСпособ тоже очень простой:

// 路由匹配
function match(location){
  location = typeof location === 'string' ? { path: location } : location
  return pathMap[location.path]
}

matchМы передаем методу параметр.Этот параметр может быть строкой или объектом, который должен иметь атрибут пути, потому что путь должен использоваться для соответствия настроенным данным модуля маршрутизации.Используйте следующее:

// String | Object

match("/home")
match({path: "/home"})

В начале функции проверяется тип параметра и преобразуется в объект, а затем напрямую возвращается карта путей pathMap, не правда ли просто? Не волнуйтесь, эта часть будет оптимизирована в будущем.

Использование createMatcher и монтирование метода экземпляра

Просмотрите нашиcreateMatcherТо, что делается в методе, по сути, в основном генерирует объект отображения маршрута.pathMap, который возвращает четыре функции:

  • addRoutes
  • getRoutes
  • addRoute
  • match

Для этих методов, на самом деле, последний, который будет установлен на экземпляре VuerOter, при использовании это связано сthis.$router.addRoute()Таким образом, здесь только основная реализация, и она будет смонтирована на экземпляре позже, среди которыхmatchМетод будет дополнительно оптимизирован.

Итак, приходите и смотритеcreateMatcherИспользование функции и монтирование этих методов экземпляра снова возвращаются к классу VueRouter:

export default class VueRouter(){
  constructor(options) {
    this.options = options;
    // 创建路由matcher对象,传入routes路由配置列表及VueRouter实例,主要负责url匹配
    this.matcher = createMatcher(options.routes);
    
    // ...
  }
  
  // 匹配路由
  match(location) {
    return this.matcher.match(location)
  }
  
  // 获取所有活跃的路由记录列表
  getRoutes() {
    return this.matcher.getRoutes()
  }
  
  // 动态添加路由(添加一条新路由规则)
  addRoute(parentOrRoute, route) {
    this.matcher.addRoute(parentOrRoute, route)
  }
  
  // 动态添加路由(参数必须是一个符合 routes 选项要求的数组)
  addRoutes(routes) {
    this.matcher.addRoutes(routes)
  }
}

Как и выше, мы вызвали его прямо в конструкторе класса VueRoutercreateMatcherфункцию и монтирует возвращаемое значение в атрибут matcher экземпляра.На самом деле, этот объект содержит четыре метода, а затем монтирует эти методы в экземпляр, поэтому я не буду вдаваться в подробности.

Эти методы теперь присутствуют в экземпляре VueRouter, иthis.$routerПрокси для экземпляра VueRouter создается при установке, поэтому можно использовать эти методы.

Реализация истории родительского класса режима маршрута

Реализация сопоставителей маршрутов подходит к концу, помните, что еще есть в конструкторе класса VueRouter, кроме сопоставителей маршрутов? Правильно, параметр входящего режима проверяется, и класс создается для трех режимов путем оценки и инстанцирования, а затем монтируется в свойстве истории экземпляра VueRouter.

Затем мы будем реализовывать эти классы один за другим, а именноHTML5History | HashHistory | AbstractHistory. первый вsrc/hello-vue-router/новая папкаhistory/папку, создайте в этой папке три новых файла, соответствующих трем классам построения режима:

  • hash.js
  • html5.js
  • abstract.js

Затем определите родительский класс для трех классов шаблонов маршрутизации.

Мысль: зачем определять родительский класс?

Фактически, в экземпляре инициализацииthis.historyНекоторые методы монтирования являются последовательными.Хотя методы реализации могут быть непоследовательными, они не могут увеличить нагрузку на пользователей, поэтому использование должно быть унифицированным.Чтобы сохранить код и унифицировать, мы можем определить родительский класс и позволить трем Children Все классы наследуются от этого родительского класса.

Итак, во вновь созданном подклассеhistory/Под папкой создайте новуюbase.jsфайл и экспортировать класс History:

/*
 * @path: src/hello-vue-router/history/base.js
 * @Description: 路由模式父类
 */

export class History {
  constructor(router) {
    this.router = router;
    // 当前路由route对象
    this.current = {};
    // 路由监听器数组,存放路由监听销毁方法
    this.listeners = [];
  }
  
  // 启动路由监听
  setupListeners() { }

  // 路由跳转
  transitionTo(location) { }

  // 卸载
  teardown() {
    this.listeners.forEach((cleanupListener) => {
      cleanupListener();
    });

    this.listeners = [];
    this.current = "";
  }
}

Как и выше, конструктор класса History в основном делает три вещи:

  • Сохраните маршрутизатор экземпляра входящего маршрута
  • объявляет текущий объект маршрутизации текущим
  • Объявлен массив прослушивателей маршрута для хранения метода уничтожения прослушивателя маршрута.

Затем напишите несколько публичных методов:

  • Метод setupListeners для запуска прослушивателя маршрута
  • Способ перехода по маршруту прыжка
  • Режим разгрузки класса прослушивателя при разгрузке и стирании экземпляра Vuerooter

Временно напишите эти три метода, по сутиsetupListenersЗдесь только объявлен метод, в подклассе также будет перезаписана основная логика, а дальше толькоteardownЭтот метод удаления идеален.transitionToЭтот метод перехода маршрутизации и некоторые общедоступные методы, которые необходимо добавить в процессе реализации подклассов, будут постепенно улучшаться в будущем.

Сначала посмотрите на этот метод уничтожения и подумайте, почему его следует уничтожать?

На самом деле, как хэш, так и история этих двух режимов обязательно запишут некоторые слушатели в процессе реализации, и когда экземпляр VueRouter выгрузится, эти мониторы не будут уничтожены, это вызовет утечку памяти, поэтому мы пишем ручную деинсталляцию уничтоженного кода. очень просто

Во-первых, поддерживать общедоступный массив прослушивателей маршрутов.listeners, в будущем каждый раз, когда в подкласс будет записываться событие слушателя, метод прослушивателя выгрузки будет писаться напрямуюpushПодойдите к этому массиву, при прослушивании удаления VueRouter вручную вызовите метод удаления, метод заключается в его циклическом вызове.listenersТаким образом, методы в массиве уничтожают прослушиватель, и вы можете видеть, что последний шаг метода удаленияlistenersМассив и текущий объект маршрутаcurrentВсе опустело.

Сохраненный объект экземпляра маршрутизатора будет использоваться позже, может быть, что вы не понимаете, должен бытьcurrentЭтот объект, следующий акцент.

Мысли: как нам получить текущий объект маршрутизации?

отвечать:$route

Мысль: где должны храниться объекты маршрутизации? каков эффект?

Первый обзор использования$route, какими свойствами он обладает?

По сути, сохраняет текущий маршрутpath、hash、meta、query、paramsИ т. д. здесь фактически хранится все, что связано с текущей маршрутизацией, а официальное определение этого объекта маршрутизации доступно только для чтения.

а такжеcurrent, является текущим значением, на самом деле это объект маршрутизации, всякий раз, когда мы отслеживаем изменение пути маршрутизации, мы должны синхронно изменять объект маршрутизации, и когда объект маршрутизации изменяется,router-viewВид, который должен отображать компонент, также необходимо изменить, можно сказать, что этот объект маршрутизации является центром всего VueRouter.

Вы можете спросить, а разве вы только что не сказали, что этот объект доступен только для чтения? Как это еще может измениться? По сути, сам объект маршрутизации заморожен, мы только читаем свойства в объекте, но можем переключить весь объект маршрутизации!

Выше мыcurrentНачальное значение этого определения объекта маршрутизации — пустой объект. Фактически, поскольку объект маршрутизации является ориентированным на пользователя объектом с фиксированным форматом, для создания этого объекта маршрутизации с фиксированным форматом следует использовать унифицированный метод. Мы вызываем этот метод .createRoute.

метод createRoute

Или просто вынуть файл для реализации такого метода.

существуетsrc/hello-vue-router/Создайте новый в каталогеutils/папку, создайте новую в этой папкеroute.jsфайл, внедрить и экспортироватьcreateRouteметод.

Сначала создайте новый файл, скажемcreateRouteПеред методом давайте подумаем, когда нам нужно создать этот объект маршрутизации?

Сначала конечно нашcurrentПри инициализации свойства необходимо создать пустой объект маршрута Что еще?

Есть два способа изменить путь: один — напрямую изменить URL-адрес, другой — использоватьpushметод.

// No.1 oldURL => newURL
let oldURL = "http://localhost:8081/#/about"
let newURL = "http://localhost:8081/#/home?a=1"

// No.2
this.$router.push({
  path: "/home",
  query: {a: 1}
})

Видно, что при смене маршрута есть много свойств, как и в официальной документации.pushСвойства, поддерживаемые методом, следующие, см. документацию по конкретным функциям:

name
path
hash
query
params
append
replace

Когда путь меняется, нам нужно перейти на новый путь.Новый путь плюс эти атрибуты, которые можно переносить, называются целевыми информационными объектами. Маршрут текущего объекта маршрутизации должен содержать всю информацию о текущем маршруте, объект конфигурации маршрутизации, соответствующий пути + целевой информационный объект = вся информация, а отформатированная информация представляет собой текущий маршрут объекта маршрутизации.

Следовательно, чтобы обновить текущий объект маршрутизации, необходимо сопоставить объект конфигурации маршрутизации по пути, а затем объект конфигурации маршрутизации и целевой информационный объект объединяются и форматируются как маршрут. Где сделать такую ​​операцию обновления?

Оглядываясь назад на то, что мы писали ранееcreateMatcherфункция, которая возвращает метод соответствия следующим образом:

// 路由匹配
function match(location){
  location = typeof location === 'string' ? { path: location } : location
  return pathMap[location.path]
}

Здесь мы вернули объект конфигурации маршрутизации на тот момент. На самом деле наша конечная цель — сопоставить его с текущим объектом маршрутизации. Мы также проанализировали текущий объект маршрутизации = объект конфигурации маршрутизации + целевой информационный объект, поэтому он является наиболее полным для непосредственно соответствуют объекту маршрутизации data, теперь перепишем этот метод:

/*
 * @path: src/hello-vue-router/create-route-map.js
 * @Description: 路由匹配器Matcher对象生成方法
 */
import { createRouteMap } from "./create-route-map";
// 导入route对象创建方法
import { createRoute } from "./utils/route"

export function createMatcher(routes){
  const pathMap = createRouteMap(routes)
  
  // 路由匹配
  function match(location){
    location = typeof location === 'string' ? { path: location } : location
    return createRoute(pathMap[location.path], location) // 修改
  }
  
  // ...
}

как указано выше, вcreateMatcherвозвращается функциейmatchметод, непосредственно создайте новый объект маршрутизации и верните его. Из этого анализа мы можем определитьcreateRouteПараметры функции, как указано вышеcreateRouteВ методе есть 2 параметра, первый — это запись объекта сопоставления маршрута, а второй — местоположение целевого информационного объекта (именно поэтому мы называем параметр местоположения метода сопоставления и позволяем ему иметь как объектный, так и строковый форматы. причина).

мы часто используемpushНа самом деле параметром метода является объект местоположения, который может быть как строковым путем, так и объектом.Когда это объект, свойства, которые могут быть переданы, такие же, как и те, которые могут быть настроены методом push над.

Однако в свойствах, написанных вышеappend、replaceДа, две дополнительные функции, требующие дополнительного разбора,pushподдержка метода,router-linkКомпоненты также поддерживаются. Их функции см. в следующих документах. Мы временно опускаем синтаксический анализ этих двух параметров, поскольку они не являются основной логикой.

Анализ готов к внедрениюcreateRouteМетод, старые правила, сначала посмотрите на общий код, и постепенно анализируйте:

/*
 * @path: src/hello-vue-router/utils/route.js
 * @Description: route对象相关方法
 */
export function createRoute(record, location) {
  let route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || "/",
    hash: location.hash || "",
    query: location.query || {},
    params: location.params || {},
    fullPath: location.path || "/",
    matched: record && formatMatch(record),
  };
  return Object.freeze(route);
}

// 初始状态的起始路由
export const START = createRoute(null, {
  path: '/'
})

// 关联所有路由记录
function formatMatch(record) {
  const res = []
  while (record) {
    // 队列头添加,所以父record永远在前面,当前record永远在最后
    // 在router-view组件中获取匹配的route record时会用到
    // 精准匹配到路由记录是数组最后一个
    res.unshift(record)
    record = record.parent
  }
  return res
}

Как указано выше,createRouteВ методе объект маршрута строится по двум параметрам, принимающим некоторые значения друг от друга. Здесь следует отметить две вещи,fullPathПараметр на самом деле представляет собой полный путь path+qs+hash, но здесь мы пишем только путь, игнорируя сначала проблему с параметром.

а такжеmatchedДля этого свойства мы напрямую написалиformatMatchГенерируется функция, и в ней делается только одно: получить все объекты конфигурации маршрутизации, связанные с текущим путем.

параметр функциональной строкиrecordЭто объект конфигурации маршрутизации. При генерации объекта конфигурации маршрутизации мы добавили к нему атрибут parent, указывающий на его родительскую маршрутизацию. Если не помните, просмотрите его.createRouteMapметод.formatMatchФункция состоит в том, чтобы рекурсивно найти текущий путь, включая его родительский объект конфигурации маршрутизации, и сформировать массив, которыйmatchedПараметры, например, следующая конфигурация маршрутизации:

let routes = [
   {
    path: "/parent",
    name: "Parent",
    component,
    children:[
      {
        path: "child",
        name:"Child",
        component,
      }
    ]
  }
]

Затем эта конфигурация маршрутизации анализируется в pathMap следующим образом:

pathMap = {
  "/parent": {path:"/parent", ...},
  "/parent/child": {path:"/parent/child", ...},
}

Если новый путь для перехода/parent/child, при построении маршрута пройтиformatMatchМетод связывает все свои записи маршрутизации и, наконец, объект маршрутизации.matchedСвойства следующие:

[
  {path:"/parent", component, parent ...},
  {path:"/parent/child", component, parent ...}
]

Обратите внимание, потому чтоformatMatchКогда функция рекурсивно находит родителя, мы используемunshiftметод, поэтому последний элемент конечного массива должен быть модулем текущего пути.

На самом деле это делается для подготовки к вложенной маршрутизации, поскольку при наличии вложенной маршрутизации и сопоставлении дочерней записи маршрутизации это фактически означает, что родительская запись маршрутизации также должна быть сопоставлена. Например, чтобы сопоставить /foo/bar, когда сопоставляется сам /foo/bar, его родительский объект маршрутизации /foo также должен совпадать.Окончательный результат сопоставления выглядит следующим образом:

metched = [{path:"/foo", ...},{path:"/foo/bar"}] 
// “/foo/bar” 本身匹配模块在数组最后,而第一项是顶级路由匹配项

Таким образом, объект маршрутизацииmatchedСвойство представляет собой массив, а элементы массива — это объекты конфигурации сопоставленной маршрутизации. Порядок элементов массива — от объекта сопоставления маршрутизации верхнего уровня к самому объекту сопоставления текущей подмаршрутизации. На этом этапе выполняется простое создание маршрута. функция в порядке.

Идея переключается обратно на урок истории,currentМы не присвоили объекту начальное значение маршрутизации, поэтому мы находимся вroute.jsОбъект маршрутизации инициализации также записывается в файл, экспортируется и вызываетсяcreateRouteметод, первый параметр пустой, второй параметр записывает только значение атрибута пути"/"Объект:

// 初始状态的起始路由
export const START = createRoute(null, {
  path: '/'
})

окончательная модификацияbase.jsФайл класса истории, начальное значение объекта маршрутаSTARTимпортировать и назначатьcurrent:

// 导入初始化route对象
import { START } from "../utils/route";

export class History {
  constructor(router) {
    this.router = router;
    
    // 当前路由route对象
    //     this.current = {};
    // =>  this.current = START;
    this.current = START;
    
    this.listeners = [];
  }
  
 // ...
}

Здесь, в родительском классеtransitionToТо есть метод перехода по маршруту можно продолжать дополнять, при вызове метода перехода по маршруту будет передаваться целевой информационный объект.

  • обновить объект маршрутаcurrent

  • Обновить URL-адрес

  • Обновить вид

// 路由跳转
transitionTo(location, onComplete) {
  // 路由匹配,解析location匹配到其路由对应的数据对象
  let route = this.router.match(location);

  // 更新current
  this.current = route;

  // 更新URL
  this.ensureURL()

  // 跳转成功抛出回调
  onComplete && onComplete(route)
}

Как и выше, метод перехода маршрутизацииtransitionToПо сути, входящий — это объект локации,pushМетод также реализуется на основе этого метода.

Когда приходит новый целевой информационный объект, мы должны сначала создать новый объект маршрутизации. История является родительским классом. Позже мы напишем подкласс. Подкласс наследует родительский класс. Когда подкласс инициализирует экземпляр (индекс. Режим файла js часть оценки параметра), фактически переданная в текущем экземпляре VueRouter, поэтому наш родительский класс также может получить его, то есть в конструкторе нашего родительского класса.routerпараметр, вешаем его прямо в свойство экземпляра родительского классаrouter, чтобы мы могли пройтиthis.routerПолучите экземпляр Vuerouter.

Помните, мы смонтировали метод match в экземпляре VueRouter? Не забывайте просматривать код.

Мы используемthis.router.matchметод, передайте параметр местоположения, вы можете создать новый объект маршрутизации и, наконец, назначить новый объект маршрутизации дляcurrentАтрибуты.

OK, согласно нашей логике, изменение маршрута генерирует новый объект маршрута и назначает егоcurrentВот и все, осталось обновить URL и обновить представление.

Думаю: зачем обновлять URL?

На самом деле, если вы напрямую изменяете URL-адрес для перехода, вам не нужно обновлять URL-адрес, но если вы используете API для выполнения маршрутных переходов, напримерpushметод, мы можем управлять объектом маршрута обновления в кодеcurrent, представление также может быть обновлено, но URL-адрес не изменился, поэтому нам также необходимо обновить URL-адрес.

Итак, вопрос в том, как обновить URL?

Вы можете видеть, что в приведенном выше коде мы вызываемensureURLспособ обновления, иthisВызывается, по сути, этот метод не на родительском классе, а на подклассе.

почему быensureURLметод написан в подклассе?

Поскольку у нас есть 3 режима, и разные режимы заменяют URL-адреса по-разному, лучше всего написать свои собственные методы обновления URL-адресов для каждого подкласса.

Почему здесь можно вызывать методы подкласса?

Поскольку подкласс инициализирует экземпляр, а подкласс наследует родительский класс, можно понять, что методы и свойства родительского класса наследуются подклассом.transitionToМетод, конечно же, также наследуется, поэтому при вызове этого метода перехода внутреннийthisУказатель является подклассом, поэтому метод подкласса может быть вызван напрямую.

Что касается обновления вида, потому что он еще не идеаленrouter-viewКомпоненты и подклассы были написаны не очень хорошо, поэтому мы ставим их позже для улучшения.

Наконец, вызывается обратный вызов успешного перехода, и передается текущий параметр объекта маршрута.

Предварительное построение подклассов режима маршрутизации

Давайте сначала построим три подкласса, по сути, создадим разные подклассы в трех файлах, и пусть они все наследуют родительский класс, а мы будем реализовывать их один за другим позже.

hash.js

import { History } from './base'

export class HashHistory extends History {
  constructor(router){
    super(router);
  }
}

html5.js

import { History } from './base'

export class HTML5History extends History {
  constructor(router){
    super(router);
  }
}

abstract.js

import { History } from './base'

export class AbstractHistory extends History {
  constructor(router){
    super(router);
  }
}

Реализация класса HashHistory

приходитьhistory/в папкеhash.jsфайл, мы сначала реализуем класс HashHistory:

/*
 * @path: src/hello-vue-router/index.js
 * @Description: 路由模式HashHistory子类
 */
import { History } from './base';

export class HashHistory extends History {
  constructor(router) {
    // 继承父类
    super(router);
  }
  
  // 启动路由监听
  setupListeners() {
    // 路由监听回调
    const handleRoutingEvent = () => {
      let location = getHash();
      this.transitionTo(location, () => {
        console.log(`Hash路由监听跳转成功!`);
      });
    };

    window.addEventListener("hashchange", handleRoutingEvent);
    this.listeners.push(() => {
      window.removeEventListener("hashchange", handleRoutingEvent);
    });
  }
}

// 获取location hash路由
export function getHash() {
  let href = window.location.href;
  const index = href.indexOf("#");
  if (index < 0) return "/";

  href = href.slice(index + 1);

  return href;
}

Как и выше, мы позволяем классу HashHistory наследовать класс History, а подкласс наследует все от родительского класса. Мы впервые реализовали режим хешированияsetupListenersметод, то есть запустить метод прослушивания маршрута.

Давайте посмотрим на логику, главное следитьhashchangeСобытия, то есть при изменении хеш-маршрута будет срабатывать его обратный вызов.

Мышление: что нам нужно делать, когда прослушивание пути маршрутизации изменилось?

Если путь меняется, вам необходимо обновить текущий объект маршрутизации, обновить представление и т. д. Мы уже делали этот шаг раньше, да, этоtransitionToЭто делается в методе перехода, поэтому мы можем вызвать метод перехода маршрута непосредственно при прослушивании изменения маршрута.

Таким образом, обратный вызов сначала передаетgetHashИнструментальная функция получения текущего значения хэша и возврата пути маршрутизации хэша.Этот метод прост и не будет повторяться. Получив путь, назовите егоtransitionToметод.

Кроме того, после запуска слушателя мы отправляемlistenersМассив (унаследован от родительского класса)pushМетод уничтожения слушателя используется для уничтожения события слушателя при выгрузке, о котором также упоминалось выше.

Далее добавляем метод подкласса:

export class HashHistory extends History {
  constructor(router) {
    // 继承父类
    super(router);
  }
  
  // 启动路由监听
  setupListeners() { /** ... **/ }
  
  // 更新URL
  ensureURL() {
    window.location.hash = this.current.fullPath;
  }
  
  // 路由跳转方法
  push(location, onComplete) {
    this.transitionTo(location, onComplete)
  }

  // 路由前进后退
  go(n){
    window.history.go(n)
  }
  
  // 跳转到指定URL,替换history栈中最后一个记录
  replace(location, onComplete) {
    this.transitionTo(location, (route) => {
      window.location.replace(getUrl(route.fullPath))
      onComplete && onComplete(route)
    })
  }

  // 获取当前路由
  getCurrentLocation() {
    return getHash()
  }
}

// 获取URL
function getUrl(path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

// 获取location hash路由
export function getHash() { /** ... **/ }

Мы добавили 5 методов:

  • ensureURL

    • Обновите URL-адрес, его реализация на самом деле очень проста, обновите хэш URL-адреса панели навигации, используйтеwindow.location.hashС API все в порядке, в методе перехода родительского класса он вызывается после обновления текущего объекта маршрутизации.ensureURL, а обновленный объект маршрутизации вfullPathСвойство представляет собой полный хэш-путь, поэтому вы можете просто назначить его напрямую.
  • push

    • Метод перехода маршрутизации, этот метод уже реализован в родительском классе, поэтому подключается вpushвызвать родительский классtransitionToМетод прыгает просто отлично, и параметры те же.
  • go

    • Прямой и обратный маршрут фактически реализован независимо от того, является ли это переходом в режиме хеширования или в режиме истории, каждый переход изменяет URL-адрес, а запись о переходе сохраняется в браузере.window.historyстек, а браузер также предоставляетwindow.history.goМетод используется для прямой и обратной маршрутизации, поэтому его можно вызывать напрямую, а параметры одинаковы.
  • getCurrentLocation

    • Получите текущий адрес маршрутизации URL, так как это хеш-класс, мы реализовали его раньшеgetHashметод для получения маршрута в URL-адресе в хеш-режиме, поэтому просто верните вызывающее значение этого метода.
  • replace

    • Перейти к указанному URL-адресу и заменить последнюю запись в стеке истории

мы фокусируемся наreplaceметод:

Давайте сначала поговорим о роли, на самом деле это прыжок, просто используйтеreplaceПрыжок не будетwindow.historyСтек генерирует запись, то есть когда мы используем со страницыpushПри переходе на страницу b стек[a,b], повторное использованиеreplaceПри переходе со страницы b на страницу c стек по-прежнему[a, b], то в это время мы возвращаемся на предыдущую страницу и переходим прямо со страницы c на страницу a.

На самом деле, мы, вероятно, также знаем, что браузерыwindow.location.replaceМетод может достичь этой функции, но три части обновления (объект маршрутизации, URL, View) необходимо учитывать при прыжках в VuerOter.

Представьте, если бы мыreplaceНовый маршрут, что нам нужно сделать?

Сначала обновите текущий объект маршрутизации, а затем обновите URL-адрес.window.location.replaceОбновления не записываются, и представление окончательно визуализируется.

А? как будто сtransitionToпочти то же самое, то мы можем изменитьtransitionToметод, поместите его на исходный URL-адрес обновленияensureURLПрыгать в метод за вызовом вызовов успеха, поэтому мы называемtransitionToметод, используемый в обратном вызовеwindow.location.replaceОбновление URL делает свое дело.

Вы можете задать вопрос, будет лиensureURLМетод ставится в конце, в обратном вызовеreplaceНо обратный вызов все равно будет вызываться после выполненияensureURLметод?

На самом деле обратный вызов используетсяwindow.location.replaceПосле обновления URL-адреса URL-адрес уже актуален, затем вызовите его снова.ensureURLОбновите URL-адрес. Поскольку обновляемый URL-адрес совпадает с текущим URL-адресом, страница не будет переходить.

потому чтоensureURLМетод действительно вызываетwindow.location.hash, если текущий адрес страницыhttp://localhost:8080/#/aboutМы используем этот API, чтобы изменить его хэш/about,由于前后 hash 一致,其实等于啥也没做。 . .

Итак, мы модифицируемtransitionToпросто измените свой обратный вызов успеха и обновите URL-адресensureURLПорядок вызова методов может быть следующим:

transitionTo(location, onComplete) {
  let route = this.router.match(location);
  this.current = route;

  // 跳转成功抛出回调 放上面
  onComplete && onComplete(route)
  
  // 更新URL 放下面
  this.ensureURL()
}

Затем реализуйтеreplaceметод:

export class HashHistory extends History {

  // 跳转到指定URL,替换history栈中最后一个记录
  replace(location, onComplete) {
    this.transitionTo(location, (route) => {
      window.location.replace(getUrl(route.fullPath))
      onComplete && onComplete(route)
    })
  }
  
  // ...
}

// 获取URL
function getUrl(path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

Как указано выше, звонитеtransitionToМетод, в своем обратном вызовеwindow.location.replaceвсе вместе

Обратите внимание, что здесь мы написали еще один метод инструмента,getUrl, на самом деле это передача хеш-пути и возврат полного нового пути URL-адреса. Обычная операция повторяться не будет.

Вот, собственно, нашHashHitoryПодклассы почти в порядке.

Дальше идет процесс.

В предыдущей реализации класса VueRouter мы только инициализировали каждый подкласс модуля маршрутизации, но мы еще не включили мониторинг маршрутизации.Обратите внимание, что метод запуска мониторинга в подклассеsetupListeners, назад сноваsrc/hello-vue-router/index.jsфайл, класс VueRouter и добавьте к нему метод инициализации.

Инициализация экземпляра VueRouter

Сборка метода инициализации

Мысль: что должен делать класс VueRouter при инициализации?

Разумеется, для запуска мониторинга класса режима маршрутизации, поскольку мониторинг запущен, его необходимо смонтировать и уничтожить.

Мысли: когда он будет уничтожен?

Когда не нужно следить, когда он уничтожается! ! После удаления корневого экземпляра Vue нет необходимости его отслеживать, поэтому мы можем отслеживать удаление корневого экземпляра Vue.

Вопрос в том, как мы можем внешне контролировать выгрузку экземпляра Vue?

Эх!hook:Пригодится специальный прослушиватель событий префикса, который официально поддерживается Vue.

Маленькие советы:hook: специальный прослушиватель событий для префикса

Функция ловушки жизненного цикла в исходном коде передается черезcallHookфункция вызова,callHookЕсть функцияvm._hasHookEventсуждение, когда оноtrueна случай, еслиhook:События со специальными префиксами будут выполняться в соответствующем жизненном цикле.

После того, как событие прослушивателя будет проанализировано в компоненте, оно будет использовано$onЧтобы зарегистрировать обратные вызовы событий, используйте$onили$onceПри прослушивании событий, если имя события начинается сhook:в качестве префикса событие будет рассматриваться какhookEvent, при регистрации обратного вызова события,vm._hasHookEventбудет установлен наtrue, когда используешьcallHookПри вызове функции жизненного цикла из-за_hasHookEventдляtrue, будет выполняться напрямую$emit('hook:xxx'), поэтому зарегистрированная функция жизненного цикла будет выполнена.

  • пройти по шаблону@hook:createdЗарегистрируйтесь в этой форме.
  • в JS черезvm.$on('hook:created', cb)илиvm.$once('hook:created', cb)Зарегистрирован, VM относится к текущему экземпляру компонента.

Классический вопрос интервью,Как прослушать жизненный цикл дочернего компонента в родительском компоненте, ответ заключается в том, чтобы получить экземпляр дочернего компонента (vm) в родительском компоненте, а затем зарегистрироватьhook:Специальный прослушиватель событий префикс + хук жизненного цикла в порядке.

Здесь мы хотим прослушивать корневой экземпляр, поэтому нам нужно получить объект корневого экземпляра, а затем зарегистрировать прослушиватель, нам не нужно использовать его для прослушивания события уничтожения.$on,использовать$onceВы можете, поэтому запускать только один раз, слушатель будет удален после запуска, как показано ниже:

// vm 为根实例对象
vm.$once("hook:destroyed", () => {})

Зная об этих проблемах, продолжайте реализовывать метод init.Поскольку вы хотите получить корневой экземпляр объекта, тоinitПараметры метода есть.После завершения анализа приступайте к написанию кода!

export default class VueRouter{
  
	init(app) {
    // 绑定destroyed hook,避免内存泄露
    app.$once('hook:destroyed', () => {
      this.app = null

      if (!this.app) this.history.teardown()
    })

    // 存在即不需要重复监听路由
    if (this.app) return;

    this.app = app;

    // 启动监听
    this.history.setupListeners();
  }
  
  // ...
}

Как и выше, это на самом деле очень просто,initМетод передает параметр приложения, то есть корневой экземпляр Vue, который оценивается в методе.this.appНезависимо от того, существует он или нет, если есть прямой возврат, это означает, что прослушиватель был зарегистрирован.Если он не существует, экземпляр присваивается свойству app класса VueRouter, и, наконец, вызывается экземпляр VueRouter.historyатрибутsetupListenersМетод начинает мониторинг.

historyчто мыconstructorЭкземпляр класса шаблона маршрутизации, инициализированный вconstructorКонструктор находится вnew VueRouterбудет выполнено, когда , поэтому мы можем получитьhistoryпример.

И прослушиватель зарегистрированного уничтожения тоже очень прост, то есть использует корневой экземпляр, как упоминалось выше.$onceЗарегистрироватьhook:destroyedСлушайте, опустойте свойство приложения в обратном вызове и позвонитеhistoryМетод удаления экземпляраteardown, этот метод реализован в родительском классе режима маршрутизации, если вы его забудете, то сможете оглянуться назад.

ХОРОШО,initМетод временно закончен, когда мы хотим его вызвать?

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

Поскольку у метода init также есть слушатель запуска, его нужно вызывать после того, как все будет инициализировано, и в это время должен быть получен корневой экземпляр Vue.

Оглядываясь назад на все ссылки выше, единственное место, где можно получить корневой экземпляр, — это метод установки плагина.mixinВремя смешаться.

Итак, вsrc/hello-vue-router/install.jsметод установки файлаmixinДобавьте метод инициализации компонента маршрута выполнения в:

/*
 * @path: src/hello-vue-router/install.js
 * @Description: 入口文件 VueRouter类
 */
export function install(Vue){
  
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        this._routerRoot = this;
        this._router = this.$options.router;
        
        // 调用VueRouter实例初始化方法
        // _router即VueRouter实,此处this即Vue根实例
        this._router.init(this) // 添加项 
        
        this._route = {};
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
    },
  });
  
  // ...
}

Тогда вы найдете,mixinсередина_routeОбъект или пустой объект, мы достигли цели текущей маршрутизации класса шаблонов маршрутизацииcurrentатрибут, поэтому здесь ему можно присвоить значение, и код снова модифицируется следующим образом:

Vue.mixin({
  beforeCreate() {
    if (this.$options.router) {
      this._routerRoot = this;
      this._router = this.$options.router;
      this._router.init(this)

      // this._route = {}; old
      this._route =  this._router.history.current; // new
    } else {
      this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
    }
  },
});

На этом весь процесс нашего хеш-режима в основном завершен. Вы можете открыть ссылку проекта, чтобы увидеть, нет ли ошибок, и вы можете щелкнуть навигацию, чтобы переключить маршрут. Если есть ошибка, вы, должно быть, написали ее. неправильно, не я. . Хотя об ошибке не сообщается, модуль маршрутизации на странице не отображается, потому чтоrouter-viewКомпоненты еще не готовы.

Компонент RouterView идеален

В настоящее время наш компонент RouterView выглядит так:

/*
 * @path: src/hello-vue-router/components/view.js
 * @Description: router-view
 */
export default {
  name: "RouterView",
  functional: true,
  render(h) {
    return h('div', 'This is RoutePage')
  }
}

Как и выше, рендеринг компонента всегда представляет собой фиксированный div, и теперь вы можете начать его совершенствовать.

Компонент маршрутизации Динамический рендеринг

Идея очень проста: сначала нужно получить текущий объект маршрутизации, потому что текущий объект маршрутизацииmatchedВ массиве хранятся все объекты сопоставления маршрута, связанные с текущим путем.Последний элемент массива — это объект сопоставления маршрута самого текущего пути, поэтому нам нужно только вынуть последний элемент массива, а затем взять его компоненты свойство (то есть модуль маршрутизации, соответствующий текущему пути). ), просто передайте его функции рендеринга напрямую.

Начните модифицировать компонент RouterView:

export default {
  name: "RouterView",
  functional: true, // 函数式组件
  render(h,  { parent, data}) {
    // parent:对父组件的引用
    // data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
    
    // 标识当前渲染组件为router-view
    data.routerView = true

    let route = parent.$route
    let matched;
    if(route.matched){
      matched = route.matched[route.matched.length - 1]
    }

    if (!matched) return h();
  
    return h(matched.components, data)
  }
}

Если вы не знаете о функциональных компонентах, см. документациюДокументация по функциональным компонентам.

На самом деле код очень простой. Сначала мы идентифицируем компонент RouterView, который в данный момент рендерится. Код добавляет к данным атрибут. В итоге эти данные будут переданы в компонент в качестве второго параметра createElement. Когда мы захотим узнать является ли компонент Если RouterView отображается, об этом можно судить по этому свойству, которое хранится в экземпляре компонента.$vnodeобъект данных свойства.

Поскольку мы смонтировали$routeТаким образом, вы можете получить доступ к этому объекту маршрутизации через любой экземпляр, получить объект маршрутизации и использовать любой из них.matchedПоследний элемент массива атрибутов, то есть компонент маршрутизации, соответствующий текущему пути.

Наконец, вы можете вернуть компонент непосредственно в функцию h (createElement).

Вроде все ок, откройте страницу проекта и посмотрите.

В дополнение к пустой навигации на странице не сообщается об ошибке.Нажатие на навигацию запускает прослушиватель перехода (консоль имеет вывод), но рендеринг компонента отсутствует, как показано ниже:

что случилось? Пройдите процесс.

Сначала нажмите на навигационный переход, прослушайте изменение хеш-маршрута, перейдитеtransitionToМетоды, методы делают три вещи:

  • Обновить текущий объект маршрута
  • Обновить URL-адрес
  • Обновить рендеринг компонентов

Эх! Обновите рендеринг компонента, мы, кажется, еще не сделали этот шаг, и мы нашли проблему!

Сначала мы усовершенствовали компонент RouterView, но когда путь маршрутизации обновляется, как мы уведомляем компонент RouterView об обновлении рендеринга? ?

Подумайте об этом, что является ядром Vue? Конечно, он реагирует на данные.Основные данные RouterView$route, если мы сделаем это адаптивными данными, то при изменении они могут быть автоматически перерисованы напрямую!

Просто сделайте это, как написано ранее$route, который фактически закреплен в корневом экземпляре Vue_routeОбъект, пока_routeОбъект можно сделать отзывчивым.Конечно, адаптивный метод по-прежнему опирается на методы, предоставляемые Vue.В противном случае нам слишком трудоемко писать тип, реагирующий на данные.Более того, конструктор Vue сам предоставляет такой API, т.е. ,Vue.util.defineReactiveФункция также очень проста в использовании, измените метод установки:

Vue.mixin({
  beforeCreate() {
    if (this.$options.router) {
      this._routerRoot = this;
      this._router = this.$options.router;
      this._router.init(this) 

      // this._route =  this._router.history.current;  old
      Vue.util.defineReactive(this, '_route', this._router.history.current); // new
    } else {
      this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
    }
  },
});

Как показано выше, мы используемVue.util.defineReactiveAPI, добавьте реактивное свойство в корневой экземпляр (это)_routeИ назначьте его как объект маршрутизации, конструктор Vue можно использовать прямо здесь, потому чтоinstallПараметры метода включены в Vue.

так что всякий раз_routeПри изменении этого объекта компонент RouterView может быть автоматически отрисован.Давайте еще раз посмотрим на страницу и нажмем на навигацию:

блять, все то же самое, почему так? Снова инсульт.

Сначала нажмите на навигационный переход, прослушайте изменение хеш-маршрута, перейдитеtransitionToМетоды, методы делают три вещи:

  • Обновить текущий объект маршрута
  • Обновить URL-адрес
  • Обновить отрисовку компонента

Вроде нормально, а! Подождите, кажется, проблема снова обнаружена.При обновлении текущего объекта маршрутизации, похоже, только обновлениеcurrent, так и не обновил_route,_routeОбъектам присваивается значение только один раз при их инициализации. . Измени это! !

первый дляHistoryдобавить классlistenМетод и получить обратный вызов,listenВнутри функции функция обратного вызова напрямую сохраняется вHistoryКатегорияcbсвойства, вtransitionToв функцииcurrentпозвоните после обновленияcbПерезвонил и передал обновление для обновленияrouteобъект и_routeОбновление этого шага, размещенное в методе инициализации класса VueRouter, выглядит следующим образом:

// History父类中新增listen方法 保存赋值回调
listen(cb){
  this.cb = cb
}

transitionTo(location, onComplete) {
  let route = this.router.match(location);
  this.current = route;

  // 修改
  // 调用赋值回调,传出新路由对象,用于更新 _route
  this.cb && this.cb(route)

  onComplete && onComplete(route)
  this.ensureURL()
}

Затем метод INIT класса Vuerouter:

init(app) {
  app.$once('hook:destroyed', () => {
    this.app = null

    if (!this.app) this.history.teardown()
  })

  if (this.app) return;

  this.app = app;

  this.history.setupListeners();

  // 新增 
  // 传入赋值回调,为_route赋值,进而触发router-view的重新渲染 
  // 当前路由对象改变时调用
  this.history.listen((route) => {
    app._route = route
  })
}

Некоторые друзья могут запутаться, но на самом деле он очень прост для понимания, т. е. вызывается в методе inithistoryЭкземпляры наследуются от родительского классаlistenметод, передавая обновление_routeобратный звонок,listenФункция сохранит этот обратный вызов навсегда, и каждый раз, когда объект маршрутизации обновляется, его можно обновить, передав новый объект маршрутизации и вызвав его один раз._route.

Теперь откройте страницу и посмотрите на нее еще раз, обновите страницу, рендеринга нет, нажмите на навигацию, и она снова отобразится.

Мысль: почему компонент не отображается при обновлении?

На самом деле это потому, что при изменении пути маршрутизации мы можем его прослушать, а затем проделать все операции, но при инициализации страницы мы не разбираем начальный путь.

Знай проблему и решай ее! На самом деле это тоже просто: получить текущий путь маршрутизации прямо в методе init, а затем вызватьtransitionToМетод анализирует путь и отображает его.Снова измените метод init класса VueRouter:

init(app) {
  app.$once('hook:destroyed', () => {
    this.app = null

    if (!this.app) this.history.teardown()
  })

  if (this.app) return;

  this.app = app;

  // 新增
  // 跳转当前路由path匹配渲染 用于页面初始化
  this.history.transitionTo(
    // 获取当前页面 path
    this.history.getCurrentLocation(),
    () => {
      // 启动监听放在跳转后回调中即可
      this.history.setupListeners();
    }
  )

  this.history.listen((route) => {
    app._route = route
  })
}

Как и выше, помните, что написано в подклассе режима маршрутизацииgetCurrentLocationметод? По сути, это получение текущего пути маршрутизации и использованиеhistoryпримерtransitionToМетод проходит в текущем маршрутном пути, так как это метод init, он эквивалентен выполнению при инициализации страницы, то есть путь текущей страницы будет получен для парсинга и рендеринга один раз при обновлении страницы. Запускаем монитор.setupListenersФункция помещается в обратный вызов перехода для мониторинга, и это нормально.

Затем снова посмотрите на страницу:

Будь то обновление или прыжок, нет проблем, он может отображаться нормально, приятно!

Отрисовка компонента вложенного маршрута

Давайте снова протестируем вложенные маршруты!

Для подготовки сначала напишите родительскую страницу вsrc/views/новая папкаParent.vueфайл, написанный в коде:

<template>
  <div>
    parent page
    <router-view></router-view>
  </div>
</template>

Затем напишите дочернюю страницу вsrc/views/новая папкаChild.vueфайл, напишите код:

<template>
  <div>
    child page
  </div>
</template>

Исправлятьsrc/router/index.jsМассив конфигурации маршрутизации файла выглядит следующим образом:

const routes = [
  // ...
  
  //新增路由配置
  {
    path: "/parent",
    name: "Parent",
    component: ()=>import("./../views/Parent.vue"),
    children:[
      {
        path: "child",
        name:"Child",
        component:()=>import("./../views/Child.vue")
      }
    ]
  }
];

Затем изменитеsrc/App.vueНавигация по маршруту в файлах, новинкаParent & ChildДве навигации следующие:

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <!-- 新增 -->
      <router-link :to="{ path: '/parent' }">Parent</router-link> |
      <router-link :to="{ path: '/parent/child' }">Parent Child</router-link>
    </div>
    <router-view/>
  </div>
</template>

Хорошо, это очень простой вложенный маршрут, давайте посмотрим на эффект страницы!

Первые две страницы нормальные,parentКомпонент страницы не отображается, а консоль взрывается напрямую:

childСтраница выглядит следующим образом:

childПоскольку страница отображает только содержимое дочерней страницы, это вложенный маршрут, и содержимое страницы дочерней страницы записывается на родительской странице.router-viewОтрисовка посередине, поэтому при нажатии на дочернюю страницу обычно должно отображаться содержимое родительской страницы.

На самом деле все проблемы из-за того, что мы не учли ситуацию вложенности, когда писали компонент RouterView.Просмотрите код компонента RouterView:

export default {
  name: "RouterView",
  functional: true,
  render(h,  { parent, data}) {
    data.routerView = true

    let route = parent.$route
    let matched;
    if(route.matched){
      matched = route.matched[route.matched.length - 1]
    }

    if (!matched) return h();
  
    return h(matched.components, data)
  }
}

Проанализируйте с помощью текущего кода компонента RouterView, является ли текущий путь/parent/child, получить текущий объект маршрутизацииroute,мы знаемroute.matchedЗдесь хранятся все связанные объекты конфигурации маршрутизации после разрешения пути, которые должны выглядеть следующим образом:

[
  {path: "/parent", components, ...},
  {path: "/parent/child", components, ...}
]

И мы берем последний элемент и берем только модуль подмаршрутизации, поэтому отображается только компонент подмаршрутизации.

Тогда, если текущий путь/parent, который получается после разбора текущего объекта маршрутизацииroute.matchedМассив выглядит так:

[
  {path: "/parent", components, ...}
]

Возьмем последний элемент, рендерится только родительский компонент маршрутизации, т.к.router-viewкомпонента, продолжайте выполнять логику компонента, а затем визуализируйте родительский компонент. . . Он продолжает зацикливаться, поэтому он взрывает стек. .

Измените компонент RouterView следующим образом: просмотрите полный код, прежде чем объяснять его.

export default {
  name: "RouterView",
  functional: true, // 函数式组件
  render(h,  { parent, data}) {
    // parent:对父组件的引用
    // data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
    
    // 标识当前组件为router-view
    data.routerView = true

    let depth = 0;
    // 逐级向上查找组件,当parent指向Vue根实例结束循环
    while(parent && parent._routerRoot !== parent){
      const vnodeData = parent.$vnode ? parent.$vnode.data : {};
      // routerView属性存在即路由组件深度+1,depth+1
      if(vnodeData.routerView){
        depth++
      }

      parent = parent.$parent
    }


    let route = parent.$route
    
    if (!route.matched) return h();
    
    // route.matched还是当前path全部关联的路由配置数组
    // 渲染的哪个组件,走上面逻辑时就会找到depth个RouterView组件
    // 由于逐级向上时是从父级组件开始找,所以depth数量并没有包含当前路由组件
    // 假如depth=2,则route.matched数组前两项都是父级,第三项则是当前组件,所以depth=索引
    let matched = route.matched[depth]

    if (!matched) return h();

    return h(matched.components, data)
  }
}

Этот фрагмент может быть непростым для понимания.

Прежде всего, сделайте логотип для всех компонентов RouterView.

Затем начните сparentРодительский экземпляр обходит компонент уровень за уровнем и находит верхний корневой экземпляр из текущего родительского экземпляра, то есть когдаparent._routerRoot !== parentПосле установки вырваться из петли.

В логике обхода определите экземпляр$vnodeЕсть ли какой-либо атрибут данных под атрибутомrouterViewсвойства, естьdepth + 1, последний пустьparent = parent.$parent,$parentВы получаете экземпляр родительского компонента для запуска рекурсии.

Вы должны знать, что независимо от того, что вы делаете, объект маршрута объект маршрута, соответствующий текущему пути, всегда остается неизменным, иroute.matchedпредставляет собой массив конфигураций маршрутизации, связанных со всеми текущими путями.

Если текущий путь/a/b/c, трехуровневая вложенная маршрутизация, то ееroute.matchedДолжно быть следующим:

[
  {path: "/a", ...},
  {path: "/a/b", ...},
  {path: "/a/b/c", ...},
]

Вложенные три уровня, есть три компонента RouterView,App.vue、a.vue、b.vueодин из каждого, поэтому при рендеринге/a/b/c, страница должна выглядеть так:

// /a/b/c
a
 b
  c

когдаApp.vueКомпонент routerView страницы начинает рендеринг, и выполняется поиск логики компонента.depthуровень,из родительского экземпляраИтерация до корневого экземпляра в поискахrouterViewАтрибут компонентов, есть 0, поэтомуdepth = 0,route.matched[0]который/aкомпонент маршрутизации.

когдаa.vueКомпонент routerView страницы начинает рендеринг, и выполняется поиск логики компонента.depthуровень,из родительского экземпляраИтерация до корневого экземпляра в поискахrouterViewКомпонент атрибута, есть 1, поэтомуdepth = 1,route.matched[1]который/aкомпонент маршрутизации.

когдаb.vueКомпонент routerView страницы начинает рендеринг, и выполняется поиск логики компонента.depthуровень,из родительского экземпляраИтерация до корневого экземпляра в поискахrouterViewКомпоненты атрибута, их 2, поэтомуdepth = 2,route.matched[2]который/aкомпонент маршрутизации.

Взглянув на страницу еще раз, мы обнаружили, что обе страницы вложенного маршрута являются нормальными.

/ родитель:

/родитель/ребенок:

Итак, понял? Я думаю, что это достаточно подробно, и я не знаю, как прочитать его несколько раз, чтобы сопоставить точки останова или распечатать.

Метод монтирования экземпляра VueRouter идеален

В классе режима маршрутизации мы реализовали несколько методов, связанных с скачками маршрутизации, которые еще не были установлены на классе Vuerouter. Давайте монтируем их вместе, а также ранее установленныеaddRoute & addRoutesДва метода все еще нуждаются в улучшении.

назадsrc/hello-vue-router/index.jsдокумент:

export default class VueRouter {
  
  // 导航到新url,向 history栈添加一条新访问记录
  push(location) {
    this.history.push(location)
  }

  // 在 history 记录中向前或者后退多少步
  go(n) {
    this.history.go(n);
  }

  // 导航到新url,替换 history 栈中当前记录
  replace(location, onComplete) {
    this.history.replace(location, onComplete)
  }

  // 导航回退一步
  back() {
    this.history.go(-1)
  }
}

Как и выше, добавление нескольких методов, связанных с маршрутными переходами, на самом деле просто вызывает методы для уже реализованного экземпляра истории.

Затем смотрим на ранее смонтированныйaddRoute & addRoutesдва метода.

В настоящее время, когда эти два метода вызываются, они действительно добавляются, что нормально при нормальных обстоятельствах, но есть особый случай, то есть перед инициализацией пути текущей страницы динамически добавляется компонент маршрутизации текущей страницы. , В настоящее время, если мы используем текущий путь к странице. После загрузки API он просто анализирует и добавляет внутреннюю карту пути, но, поскольку текущий объект маршрутизации не обновляется, страница напрямую сообщает об ошибке.

Поэтому необходимо после динамического добавления выполнить операцию обновления маршрута, на самом деле она еще называетсяtransitionToМетод может перейти на текущий путь к странице, конечно, это необходимо, чтобы избежать инициализации маршрута, который равен текущему маршрутуSTART(начальное значение текущего объекта маршрута, написанное ранее).

Итак, измените эти две функции следующим образом:

// 新增START对象导入
import { START } from "./utils/route";

export default class VueRouter {
  
 // 动态添加路由(添加一条新路由规则)
  addRoute(parentOrRoute, route) {
    this.matcher.addRoute(parentOrRoute, route)
    // 新增
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }

  // 动态添加路由(参数必须是一个符合 routes 选项要求的数组)
  addRoutes(routes) {
    this.matcher.addRoutes(routes)
    // 新增
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
  
  // ...
}

Это относительно просто, поэтому я не буду вдаваться в подробности.

На этом процесс хэширования завершен.

Следующим шагом является пошаговая реализация режима истории, то есть заполнение класса HTML5History.

Реализация класса HTML5History

Хотя детали реализации классов HTML5History и HashHistory немного отличаются, API-интерфейсы, которые мы хотим написать, одинаковы, чтобы они могли полностью соответствовать унифицированным внешним вызовам.

приходитьhistory/в папкеhtml5.jsфайл, с опытом класса HashHistory выше, код будем вставлять прямо сюда, ибо ничего сложного.

/*
 * @path: src/hello-vue-router/history/html5.js
 * @Description: 路由模式HTML5History子类
 */
import { History } from './base'

export class HTML5History extends History {
  constructor(router) {
    // 继承父类
    super(router);
  }

  // 启动路由监听
  setupListeners() {
    // 路由监听回调
    const handleRoutingEvent = () => {

      this.transitionTo(getLocation(), () => {
        console.log(`HTML5路由监听跳转成功!`);
      });
    };

    window.addEventListener("popstate", handleRoutingEvent);
    this.listeners.push(() => {
      window.removeEventListener("popstate", handleRoutingEvent);
    });
  }

  // 更新URL
  ensureURL() {
    if (getLocation() !== this.current.fullPath) {
      window.history.pushState(
        { key: Date.now().toFixed(3) }, 
        "", 
        this.current.fullPath
      );
    }
  }

  // 路由跳转方法
  push(location, onComplete) {
    this.transitionTo(location, onComplete)
  }

  // 路由前进后退
  go(n){
    window.history.go(n)
  }

  // 跳转到指定URL,替换history栈中最后一个记录
  replace(location, onComplete) {
    this.transitionTo(location, (route) => {
      window.history.replaceState(window.history.state, '', route.fullPath)
      onComplete && onComplete(route)
    })
  }

  // 获取当前路由
  getCurrentLocation() {
    return getLocation()
  }
}

// 获取location HTML5 路由
function getLocation() {
  let path = window.location.pathname;
  return path;
}

Как и выше, мы можем легко реализовать класс HTML5Histoy, но есть проблема при использованииhistory, продолжайте нажиматьrouter-linkПри создании той же навигации каждый щелчок будет обновлять страницу, о чем мы и говорили ранее:router-linkОкончательный сгенерированный тег - это тег,historyРежим щелчка тега a, переход на страницу будет запущен по умолчанию, поэтому вам необходимо перехватить поведение по умолчанию события щелчка тега a,hashНе будет, потому что атрибут href, проанализированный в теге a в хеш-режиме,#начиная с номера.

Где перехватить? конечноrouter-linkкомпоненты.

Компоненты RouterLink готовы

Это также относительно просто: метка a, возвращаемая компонентом RouterLink, равномерно добавляется для предотвращения перехода по умолчанию, а затем добавляется ручной переход:

export default {
  name: "RouterLink",
  props: {
    to: {
      type: [String, Object],
      require: true
    }
  },
  render(h) {
    const href = typeof this.to === 'string' ? this.to : this.to.path
    const router = this.$router
    let data = {
      attrs: {
        href: router.mode === "hash" ? "#" + href : href
      },
      //新增
      on: {
        click: e => {
          e.preventDefault()
          router.push(href)
        }
      }
    };
    return h("a", data, this.$slots.default)
  }
}

Как и выше, во втором параметре функции createElement(h) мы добавили блокирующее событие перехода по умолчанию к событию клика, без перехода по умолчанию мы сделали ручной переход, то есть прямой вызовrouterпримерpushспособ прыгать.

Реализация класса AbstractHistory

Нет, на самом деле это очень просто реализовать, это использовать массив для имитации исторического стека вызовов, посмотреть исходный код и написать его за несколько минут, это полностью класс, состоящий из массива и различных массивов. операционные API. Я не буду вдаваться в подробности о проблемах с пространством. .

крючок маршрутизатора имплантата

Если вы следите за реализацией, то на этом этапе основное содержание VueRouter почти готово.Далее вы можете широко распространять свои идеи, а затем самостоятельно находить соответствующую реализацию в исходном коде для справки и, наконец, улучшать ее.router hook, поскольку хук маршрутизации — одна из оставшихся функций, которую трудно реализовать, это очень хорошая возможность потренироваться.

Советы:Существует три типа хуков маршрутизации:

  • Хук глобальной маршрутизации
  • Хук маршрутизации компонентов
  • Эксклюзивная маршрутизация перед входом в охрану

напиши в конце

Если вы все еще не знаете четко процесс после того, как увидели это, посмотрите на эту картинку еще раз, может быть, вы сможете напрямую открыть вторую вену Рена и Ду!

Основная логика всей реализации по-прежнему в порядке, но в деталях по-прежнему много проблем, потому что мы проигнорировали некоторую проверку и реализацию небольших функций, но все же очень полезно понимать исходный код VueRouter. Рекомендуется выполнить его вручную, а после завершения сразу перейти к полному исходному коду VueRouter. Ошибки приветствуются! Оригинал выжигает мозги, писать не просто, если поможет, ставьте лайк! !

Код проекта:hello-vue-router

в корневом каталогеsrc/hello-vue-routerВ папке находится полный код написанного от руки VueRouter, который был прокомментирован

в корневом каталогеvue-router-sourceПапка представляет собой аннотированный исходный код VueRouter V3.5.2.