Введение в интерфейсную маршрутизацию и принцип реализации vue-router

Vue.js внешний фреймворк vue-router

Введение в внутреннюю маршрутизацию

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

http://www.xxx.com/login

Общий процесс можно представить так:

  1. Браузер делает запрос

  2. Сервер прослушивает запрос на порту 80 (или 443) и анализирует URL-адрес.

  3. В соответствии с конфигурацией маршрутизации сервера вернуть соответствующую информацию (это может быть строка html или данные json, изображения и т. д.)

  4. Браузер решает, как анализировать данные на основе Content-Type пакета.

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

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

1. хеш-режим

Благодаря популярности ajax взаимодействие с асинхронными запросами данных выполняется без обновления браузера. Более продвинутая версия асинхронного интерактивного взаимодействия — это SPA — одностраничное приложение. Одностраничные приложения не только не обновляются при взаимодействии со страницей, но и перескакивают страницы без обновления.Для реализации одностраничных приложений существует интерфейсная маршрутизация. Подобно маршрутизации на стороне сервера, интерфейсную маршрутизацию на самом деле очень просто реализовать: она сопоставляет разные пути URL, анализирует их и динамически отображает региональный HTML-контент. Но в этом есть проблема, то есть каждый раз при изменении url страница будет обновляться. Идея решения проблемы состоит в том, чтобы гарантировать, что страница не обновляется при изменении URL-адреса. До 2014 года все реализовывали маршрутизацию через хеш, а хэш URL был похож на:

http://www.xxx.com/#/login

Этот #. Последующее изменение значения хеша не приведет к тому, что браузер отправит запрос на сервер, если браузер не отправит запрос, страница не будет обновлена. Кроме того, каждый раз, когда значение хеш-функции изменяется, также срабатываетhashchangeЭто событие, благодаря которому мы можем узнать, какие изменения произошли с хеш-значением. Тогда мы можем отслеживатьhashchangeДля реализации операции обновления части страницы:

function matchAndUpdate () {
   // todo 匹配 hash 做 dom 更新操作
}

window.addEventListener('hashchange', matchAndUpdate)

2. режим истории

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

function matchAndUpdate () {
   // todo 匹配路径 做 dom 更新操作
}

window.addEventListener('popstate', matchAndUpdate)

Реализация маршрутизатора Vue

Давайте взглянемvue-routerКак это определяется:

import VueRouter from 'vue-router'
Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

new Vue({
  router
  ...
})

можно увидетьvue-routerчерезVue.useМетод внедряется в экземпляр Vue, и нам нужно использовать его глобально при его использовании.vue-routerизrouter-viewиrouter-linkкомпоненты иthis.$router/$routeтакой экземпляр объекта. Так как же реализуются эти операции? Далее я познакомлю вас с деталями в нескольких главах.vue-routerМир.

реализация vue-router -- установить

реализация vue-router -- новый VueRouter (опции)

Реализация vue-router -- HashHistory

реализация vue-router — история HTML5

Реализация vue-router — мониторинг изменения маршрута

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

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

new Router({
  id: 'router-view', // 容器视图
  mode: 'hash', // 模式
  routes: [
    {
      path: '/',
      name: 'home',
      component: '<div>Home</div>',
      beforeEnter: (next) => {
        console.log('before enter home')
        next()
      },
      afterEnter: (next) => {
        console.log('enter home')
        next()
      },
      beforeLeave: (next) => {
        console.log('start leave home')
        next()
      }
    },
    {
      path: '/bar',
      name: 'bar',
      component: '<div>Bar</div>',
      beforeEnter: (next) => {
        console.log('before enter bar')
        next()
      },
      afterEnter: (next) => {
        console.log('enter bar')
        next()
      },
      beforeLeave: (next) => {
        console.log('start leave bar')
        next()
      }
    },
    {
      path: '/foo',
      name: 'foo',
      component: '<div>Foo</div>'
    }
  ]
})

расположение идей

Первый управляется данными, поэтому мы можем передатьrouteобъект для представления текущего состояния маршрутизации, например:

current = {
    path: '/', // 路径
    query: {}, // query
    params: {}, // params
    name: '', // 路由名
    fullPath: '/', // 完整路径
    route: {} // 记录当前路由属性
}

current.routeВ нем хранится информация о конфигурации текущего маршрута, поэтому нам нужно только слушатьcurrent.routeменяется на динамическийrenderстраница.

Затем нам нужно отслеживать различные изменения маршрутизации и реагировать на них соответствующим образом. и реализоватьhashиhistoryмодель.

управляемый данными

Здесь мы расширяемvueМодель, управляемая данными, реализация простого захвата данных и обновление представления. Сначала определите нашуobserver

class Observer {
  constructor (value) {
    this.walk(value)
  }

  walk (obj) {
    Object.keys(obj).forEach((key) => {
      // 如果是对象,则递归调用walk,保证每个属性都可以被defineReactive
      if (typeof obj[key] === 'object') {
        this.walk(obj[key])
      }
      defineReactive(obj, key, obj[key])
    })
  }
}

function defineReactive(obj, key, value) {
  let dep = new Dep()
  Object.defineProperty(obj, key, {
    get: () => {
      if (Dep.target) {
        // 依赖收集
        dep.add()
      }
      return value
    },
    set: (newValue) => {
      value = newValue
      // 通知更新,对应的更新视图
      dep.notify()
    }
  })
}

export function observer(value) {
  return new Observer(value)
}

Далее нам нужно определитьDepиWatcher:

export class Dep {
  constructor () {
    this.deppend = []
  }
  add () {
    // 收集watcher
    this.deppend.push(Dep.target)
  }
  notify () {
    this.deppend.forEach((target) => {
      // 调用watcher的更新函数
      target.update()
    })
  }
}

Dep.target = null

export function setTarget (target) {
  Dep.target = target
}

export function cleanTarget() {
  Dep.target = null
}

// Watcher
export class Watcher {
  constructor (vm, expression, callback) {
    this.vm = vm
    this.callbacks = []
    this.expression = expression
    this.callbacks.push(callback)
    this.value = this.getVal()

  }
  getVal () {
    setTarget(this)
    // 触发 get 方法,完成对 watcher 的收集
    let val = this.vm
    this.expression.split('.').forEach((key) => {
      val = val[key]
    })
    cleanTarget()
    return val
  }

  // 更新动作
  update () {
    this.callbacks.forEach((cb) => {
      cb()
    })
  }
}

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

  // 响应式数据劫持
  observer(this.current)

  // 对 current.route 对象进行依赖收集,变化时通过 render 来更新
  new Watcher(this.current, 'route', this.render.bind(this))

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

  render() {
    let i
    if ((i = this.history.current) && (i = i.route) && (i = i.component)) {
      document.getElementById(this.container).innerHTML = i
    }
  }

хеш и история

ДалееhashиhistoryРеализация шаблона, здесь мы можем продолжать использоватьvue-routerИдея состоит в том, чтобы установить различные модели обработки. Давайте посмотрим на основной код, который я реализовал:

this.history = this.mode === 'history' ? new HTML5History(this) : new HashHistory(this)

Когда страница меняется, нам просто нужно слушатьhashchangeиpopstateСобытия, преобразование маршрутизацииtransitionTo:

  /**
   * 路由转换
   * @param target 目标路径
   * @param cb 成功后的回调
   */
  transitionTo(target, cb) {
    // 通过对比传入的 routes 获取匹配到的 targetRoute 对象
    const targetRoute = match(target, this.router.routes)
    this.confirmTransition(targetRoute, () => {
      // 这里会触发视图更新
      this.current.route = targetRoute
      this.current.name = targetRoute.name
      this.current.path = targetRoute.path
      this.current.query = targetRoute.query || getQuery()
      this.current.fullPath = getFullPath(this.current)
      cb && cb()
    })
  }

  /**
   * 确认跳转
   * @param route
   * @param cb
   */
  confirmTransition (route, cb) {
    // 钩子函数执行队列
    let queue = [].concat(
      this.router.beforeEach,
      this.current.route.beforeLeave,
      route.beforeEnter,
      route.afterEnter
    )
    
    // 通过 step 调度执行
    let i = -1
    const step = () => {
      i ++
      if (i > queue.length) {
        cb()
      } else if (queue[i]) {
        queue[i](step)
      } else {
        step()
      }

    }
    step(i)
  }
}

Таким образом, с одной стороны, мы проходимthis.current.route = targetRouteОбновление ранее захваченных данных достигается для достижения обновления представления. С другой стороны, мы реализуем базовую функцию ловушки через планирование очереди задач.beforeEach,beforeLeave,beforeEnter,afterEnter. Здесь почти то же самое, попутно реализуем несколько API:

  /**
   * 跳转,添加历史记录
   * @param location 
   * @example this.push({name: 'home'})
   * @example this.push('/')
   */
  push (location) {
    const targetRoute = match(location, this.router.routes)

    this.transitionTo(targetRoute, () => {
      changeUrl(this.router.base, this.current.fullPath)
    })
  }

  /**
   * 跳转,添加历史记录
   * @param location
   * @example this.replaceState({name: 'home'})
   * @example this.replaceState('/')
   */
  replaceState(location) {
    const targetRoute = match(location, this.router.routes)

    this.transitionTo(targetRoute, () => {
      changeUrl(this.router.base, this.current.fullPath, true)
    })
  }

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

  function changeUrl(path, replace) {
    const href = window.location.href
    const i = href.indexOf('#')
    const base = i >= 0 ? href.slice(0, i) : href
    if (replace) {
      window.history.replaceState({}, '', `${base}#/${path}`)
    } else {
      window.history.pushState({}, '', `${base}#/${path}`)
    }
  }

Это в основном здесь. Адрес источника:

Руки на роутере

Если вам интересно, вы можете играть сами. Реализация относительно сырая, если у вас есть какие-либо вопросы, пожалуйста, дайте указатели.