От роутинга к исходному коду vue-router, проведите вас через интерфейсную роутинг

внешний интерфейс исходный код
От роутинга к исходному коду vue-router, проведите вас через интерфейсную роутинг

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

Вот я и нашел время пополнить базовые знания по роутингу, и проверил исходники vue-router.В общем, поговорим о том, как vue-router комбинируется с vue для управления роутингом. В будущем будет более подробный анализ исходного кода~~~

Основное содержание этой статьи следующее

  • что такое маршрутизация
  • Внутренняя маршрутизация
  • Внешняя маршрутизация
  • vue-router
    • Как внедрить Vuerouter в VUE (установка плагина)
    • init VueRouter
    • Конструктор VueRouter
    • как изменить адрес
    • Как отобразить после изменения URL
    • Как идут, вперед и назад обрабатываются
    • setupListeners
    • хэш-маршрутизация
    • исторический маршрут

маршрутизация

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

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

  1. Браузер делает запрос
  2. Интерфейс сервера 80 или 443 прослушивает запрос, отправленный браузером, и анализирует путь URL.
  3. В соответствии с содержимым URL-адреса сервер запрашивает соответствующие ресурсы, которые могут быть html-ресурсами, ресурсами изображений и т. д., а затем обрабатывает и возвращает соответствующие ресурсы в браузер.
  4. Браузер получает данные, а затем оценивает, как анализировать ресурс в соответствии с типом контента.

Так что же такое маршрутизация? Мы можем просто понимать это как способ взаимодействия с сервером.По разным маршрутам мы запрашиваем разные ресурсы (HTML-ресурсы — только один путь).

Внутренняя маршрутизация

То, что мы представили выше, на самом деле является внутренней маршрутизацией.

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

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

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

Внешняя маршрутизация

Внешняя маршрутизация родилась из-за появления ajax. Мы все знаем, что ajax — это техническое решение для браузеров, обеспечивающее асинхронную загрузку. сервер напрямую преобразует весь HTML-код. Возвращаясь, пользователь будет вызывать обновление всей страницы каждый раз при небольшой операции (плюс предыдущая скорость сети по-прежнему очень низкая, поэтому можно представить себе работу пользователя)

В конце 1990-х годов Microsoft впервые внедрила технологию ajax (асинхронный JavaScript и XML), так что пользователю не нужно обновлять всю страницу для каждой операции, а взаимодействие с пользователем значительно улучшилось.

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

Конечно, есть и одностраничные приложения и MVVM, которые последовательно появлялись в поле зрения фронтендеров.

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

Более продвинутая версия асинхронного интерактивного взаимодействия — это SPA — одностраничное приложение. Одностраничные приложения не только не требуют обновления для взаимодействия со страницей, но и позволяют переходить на другую страницу без обновления. Поскольку переход на страницу не обновляется, то есть на бэкенд-запрос не возвращается html.

Итак, в большом приложении обычно есть десятки страниц (адресов URL), переходящих друг в друга.Как внешний интерфейс узнает, какому содержимому соответствует URL?

Ответ — фронтальная маршрутизация.

Следует понимать, что интерфейсный маршрут должен предоставлять интерфейс к интерфейсу к интерфейсу в соответствии с различными страницами URL-адреса.

image.pngПреимущества: хороший пользовательский опыт, нет необходимости каждый раз получать все с сервера и быстро показывать пользователю Недостатки: при использовании клавиш браузера «вперед» и «назад» запрос будет отправлен повторно, кеш используется нерационально, и отдельная страница не может запомнить предыдущую позицию прокрутки и не может запомнить позицию прокрутки при движении вперед и назад.

Какую проблему решает интерфейсная маршрутизация

  • Маршрутизация внешнего интерфейса позволяет внешнему интерфейсу самостоятельно поддерживать логику маршрутизации и отображения страниц. Каждое изменение страницы не требует уведомления сервера.
  • Улучшенный интерактивный опыт: не нужно каждый раз извлекать ресурсы с сервера и быстро показывать их пользователям

Каковы недостатки внешней маршрутизации?

  • Больше всего критикуют за то, что это плохо для SEO.
  • При использовании клавиш браузера вперед и назад запрос будет отправлен повторно для получения данных, а кеш не будет использоваться разумно.

Каков принцип реализации фронтенд-маршрутизации?

Поняв, что такое фронтенд-маршрутизация и какие задачи решает фронтенд-маршрутизация, давайте подробнее рассмотрим принцип реализации фронтенд-маршрутизации.

Принцип реализации внешней маршрутизации на самом деле очень прост: он обнаруживает изменения в URL-адресах, перехватывает URL-адреса, а затем анализирует и сопоставляет правила маршрутизации.

хэш-маршрутизация

Раньше все реализовывали маршрутизацию через хеш, и способ маршрутизации хэша такой же, как<a>Точка привязки ссылки такая же, и она добавляется после адреса#, например, мой личный блог https://cherryblog.site/#/  #И следующий контент, мы называем его хешем местоположенияimage.png

Затем мы нажимаем на другие вкладки и обнаруживаем, что хотя URL-адрес адресной строки браузера изменился, страница не обновлялась. Открываем консоль, видим, что переключение вкладок только отправляет интерфейс для запроса данных интерфейса на сервер, а не перезапрашивает html-ресурсы.image.pngЭто связано с тем, что изменение хеша не приведет к тому, что браузер отправит запрос на сервер, поэтому страница не будет обновлена. Но каждый раз, когда хэш меняется, он срабатываетhaschange мероприятие. Таким образом, мы можем контролироватьhaschangeреагировать на изменения.

В нашей текущей (2021 г.) разработке внешнего интерфейса обычно есть корневой узел<div id="root"></div>, а затем вставьте отображаемое содержимое в этот корневой узел. Затем замените вставленный компонент контента в соответствии с маршрутом.image.png

исторический маршрут

Одна проблема с хеш-маршрутизацией заключается в том, что#Так что не так уж и "красиво"

14 лет спустя с момента выпуска стандарта HTML5. Есть еще два API,pushState а такжеreplaceState, через эти два API URL-адрес можно изменить без отправки запроса. Такжеonpopstate мероприятие. С их помощью вы можете использовать другой способ реализации внешнего маршрута, но принцип тот же, что и у HASH.

С внедрением HTML5 больше не будет URL-адресов для одностраничной маршрутизации.#, становится красивее. а потому что нет#, поэтому, когда пользователь обновляет страницу и выполняет другие операции, браузер все равно будет отправлять запрос на сервер. Чтобы избежать такой ситуации, для данной реализации требуется поддержка сервера, а все маршруты нужно перенаправлять на корневую страницу. Подробнее см.: [Режим истории HTML5](Режим истории HTML5)

Обратите внимание, что прямой вызовhistory.popState() а такжеhistory.poshState()не срабатываетpopState. Вызывается только при выполнении действий браузераpopState, например нажатие кнопок браузера вперед и назад или вызовы JShistory.back() илиhistory.forward()  image.png

vue-router

Затем давайте посмотрим, как vue-router реализует интерфейсную маршрутизацию в сочетании с vue.

В общем, используйте Vue.util.defineReactive, чтобы установить _route экземпляра в реактивный объект. Методы push, replace будут активно обновлять атрибут _route. А переход, возврат или нажатие кнопок «вперед» и «назад» обновит _route в обратном вызове onhashchange или onpopstate. Обновление _route вызывает повторную визуализацию RoterView.

Затем мы подробно рассмотрим, как это достигается

Как внедрить vueRouter в vue (установка плагина)

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

В vue-router способ установки следующий.

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

// 导出 vue 实例
export let _Vue

// install 方法 当 Vue.use(vueRouter)时 相当于 Vue.use(vueRouter.install())
export function install (Vue) {
  // 如果已经注册过了并且已经有了 vue 实例,那么直接返回
  if (install.installed && _Vue === Vue) return
  install.installed = true

  // 保存Vue实例,方便其它插件文件使用
  _Vue = Vue

  const isDef = v => v !== undefined

  // 递归注册实例的方法
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  /**
   * 递归的将所有的 vue 组件混入两个生命周期 beforeCreate 和 destroyed
   * 在 beforeCreated 中初始化 vue-router,并将_route响应式
   */
  Vue.mixin({
    beforeCreate () {
      // 初始化 vue-router
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 将 _route 变成响应式对象
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  
  /**
   * 给Vue添加实例对象 $router 和 $route
   * $router为router实例
   * $route为当前的route
   */
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

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

  /**
   * 注入两个全局组件
   * <router-view>
   * <router-link>
   */
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  /**
   * Vue.config 是一个对象,包含了Vue的全局配置
   * 将vue-router的hook进行Vue的合并策略
   */
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

чтобы убедиться, чтоVueRouterвыполнить только один раз, при выполненииinstallДобавить логотип, когда логикаinstalled. Сохраните Vue в глобальной переменной, чтобы облегчить использование Vue плагинами.

Ядро установки VueRouter осуществляется черезmixin, который смешивается со всеми компонентами приложения Vue.beforeCreateа такжеdestroyedфункция крючка.

А также добавить объект экземпляра в Vue

  • _routerRoot: указывает на экземпляр vue
  • _router: указывает на экземпляр vueRouter

Некоторые геттеры инициализируются на прототипе Vue.

  • $router, текущий экземпляр Router
  • $route, информация о текущем маршрутизаторе

Vue.util.defineReactive, это метод для наблюдателей в Vue для захвата данных, захвата _route, и когда _route запускает метод установки, он уведомляет зависимые компоненты.

проходить позадиVue.componentметод определяет глобальный<router-link>а также<router-view>два компонента.<router-link>Подобно тегу,<router-view>выход из маршрута, в<router-view>Переключайте маршруты для рендеринга разных компонентов Vue. Наконец, определена стратегия слияния средств защиты маршрута, и принята стратегия слияния Vue.

init VueRouter

Только что мы упомянули, что метод инициализации VueRouter будет выполняться во время установки (this._router.init(this)), то давайте посмотрим, что делает метод init. Проще говоря, это монтирование экземпляра Vue к текущему экземпляру маршрутизатора.

Затем install выполнит метод инициализации VueRouter (this._router.init(this)). Проходит при выполнении inithistory.transitionToДелайте маршрутные переходы.matcherСопоставитель маршрутов является основной функцией последующего переключения маршрутов, сопоставления маршрутов и компонентов.


  init (app: any /* Vue component instance */) {
    this.apps.push(app)

    // main app previously initialized
    // return as we don't need to set up new history listener 
    if (this.app) {
      return
    }

    // 在 VueRouter 上挂载 Vue 实例
    this.app = app

    const history = this.history

    // setupListeners 里会对 hashchange 事件进行监听
    // transitionTo 是进行路由导航的函数
    if (history instanceof HTML5History || history instanceof HashHistory) {
      const setupListeners = routeOrError => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }

    // 路由全局监听,维护当前的route
    // 因为 _route 在 install 执行时定义为响应式属性,
    // 当 route 变更时 _route 更新,后面的视图更新渲染就是依赖于 _route
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }

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

Конструктор VueRouter относительно прост

  • Определены некоторые свойства и методы.
  • Создайте функцию сопоставления совпадений, эта функция очень важна, вы можете найти маршрут
  • Установите значения по умолчанию и делайте откаты, которые не поддерживают историю H5.
  • Создание различных объектов истории в соответствии с разными режимами
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // 创建 matcher 匹配函数
    this.matcher = createMatcher(options.routes || [], this)

    // 默认使用 哈希路由
    let mode = options.mode || 'hash'
    
    // h5的history有兼容性 对history做降级处理
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    
    if (!inBrowser) {
      mode = 'abstract'
    }
   
    this.mode = mode

    // 分发处理
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

При создании экземпляра vueRouter vueRouter определяет некоторые API на основе истории:push,replace,back,go,forward, также определяет сопоставления маршрутов, добавляет методы динамического обновления маршрутизатора и т. д.

как изменить адрес

Так как же VueRouter выполняет переходы маршрутизации? То есть мы используем_this_.$router.push('/foo', increment)Как сделать так, чтобы визуализированный вид отображал компонент Foo.

const router = new VueRouter({
  mode: 'history',
  base: __dirname,
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar },
    { path: encodeURI('/é'), component: Unicode },
    { path: '/query/:q', component: Query }
  ]
})

Помните, что мы только что сделали в конструкторе vue-router? Позвольте мне помочь вам вспомнить. В конструкторе мы выбираем разные типы истории для создания экземпляров в соответствии с разными режимами (история h5 или история хэшей или abstract ), а затем вызываем history.transitionTo во время инициализации для выполнения сопоставления инициализации маршрута, то есть для завершения навигации по первому маршруту.

мы вhistory/base.jsможно найти в файлеtransitionToметод.transitionToМожет получить три параметраlocation,onComplete,onAbort, которые являются целевым путем, обратным вызовом для успешного переключения пути и обратным вызовом для неудачного переключения пути.

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

В обратном вызове будет вызван метод replaceHash или pushHash. Они обновляют хэш местоположения. При совместимости с historyAPI будет использоваться history.replaceState или history.pushState. Если он не совместим с historyAPI, он будет использовать window.location.replace или window.location.hash.

Метод handleScroll обновит положение нашей полосы прокрутки.

transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
) {
    // 调用 match方法得到匹配的 route对象
    const route = this.router.match(location, this.current)
    
    // 过渡处理
    this.confirmTransition(
        route,
        () => {
            // 更新当前的 route 对象
            this.updateRoute(route)
          
            // 更新url地址 hash模式更新hash值 history模式通过pushState/replaceState来更新
            onComplete && onComplete(route)
           
            this.ensureURL()
    
            // fire ready cbs once
            if (!this.ready) {
                this.ready = true
                this.readyCbs.forEach(cb => {
                  cb(route)
                })
            }
        },
        err => {
            if (onAbort) {
                onAbort(err)
            }
            if (err && !this.ready) {
                this.ready = true
                this.readyErrorCbs.forEach(cb => {
                cb(err)
                })
            }
        }
    )
}

Как отобразить компонент после изменения URL-адреса

До сих пор можно было сделать так, чтобы объекты истории в разных режимах имели одинаковую производительность.push  replaceФункция (подробности см. в разделе реализации ниже)

Итак, как правильно отрендерить после замены маршрута.

Помните принцип отзывчивости vue, о котором мы говорили ранее? Мы уже установили _router как реактивный при установке. Всякий раз, когда _router изменяется, RouterView будет отображаться. (Мы обновляем _route в обратном вызове transitionTo)

go, forward, back

Все методы go, forward и back, определенные в VueRouter, являются методами go, которые вызывают свойство history.

Метод go в хэше вызывает history.go. Как он обновляет RouteView? Ответ заключается в том, что хеш-объект добавляет прослушиватель для событий popstate или hashchange в методе setupListeners. Обновление RoterView будет запущено при обратном вызове события.

setupListeners

Когда мы нажимаем кнопку «назад, вперед» или вызываем метод «назад, вперед, вперед». Мы не обновляем активно _app.route и current. Как мы запускаем RouterView для обновления? Прислушиваясь к событиям popstate или hashchange в окне. В обратном вызове события вызовите метод transitionTo, чтобы завершить обновление до _route и current.

Или можно сказать, что при использовании методов push и replace обновление хэша происходит после обновления _route. При использовании go, back обновление хэша происходит перед обновлением _route.

setupListeners () {
  const router = this.router
  const expectScroll = router.options.scrollBehavior
  const supportsScroll = supportsPushState && expectScroll
  if (supportsScroll) {
    setupScroll()
  }
  window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
    const current = this.current
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      if (supportsScroll) {
        handleScroll(this.router, route, current, true)
      }
      if (!supportsPushState) {
        replaceHash(route.fullPath)
      }
    })
  })
}

хэш-маршрутизация

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    // 添加 hashchange 事件监听
    window.addEventListener(
      hashchange,
      () => {
        const current = this.current
        // 获取 hash 的内容并通过路由配置,把新的页面 render 到 ui-view 的节点
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      }
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
    push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  go (n: number) {
    window.history.go(n)
  }
}

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

Маршрутизация истории H5

По сути, это в основном похоже на реализацию хеширования, главное отличие в том, что

  • Событие не то
  • Реализация методов Push и Replace отличается
export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {
    super(router, base)

    this._startLocation = getLocation(this.base)
  }

  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    // 通过监听 popstate 事件
    window.addEventListener('popstate', () => {
      const current = this.current

      // Avoiding first `popstate` event dispatched in some browsers but first
      // history route not updated since async guard at the same time.
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return
      }

      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    })
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }

  go (n: number) {
    window.history.go(n)
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      // 使用 pushState 更新 url,不会导致浏览器发送请求,从而不会刷新页面
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      // replaceState 跟 pushState 的区别在于,不会记录到历史栈
      replaceState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  ensureURL (push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath)
      push ? pushState(current) : replaceState(current)
    }
  }

  getCurrentLocation (): string {
    return getLocation(this.base)
  }
}


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