Интервьюер: Вы разбираетесь во внешней маршрутизации?

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

Серия интервьюеров (3): Реализация внешней маршрутизации


прошлое


Каталог статей

  1. Реализация внешней маршрутизации на основе хэша
  2. Обновление внешней маршрутизации на основе хэша
  3. Реализация внешней маршрутизации на основе истории H5

предисловие

Внешняя маршрутизацияЭто необходимая функция для современных SPA-приложений, и каждый современный интерфейсный фреймворк имеет соответствующую реализацию, такую ​​как vue-router, react-router.

Мы не хотим исследовать реализацию vue-router или react-router, потому что независимо от того, какой это маршрут, это не что иное, как более совместимая реализация хэша или реализация истории H5, и несколько фреймворков нужно только соответствующим образом инкапсулировать. .

Предварительное заявление:Мы не оценивали входящие параметры вовремя, чтобы избежать ошибок, не рассматривали вопросы совместимости, а реализовывали только основные методы.


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

Очевидным признаком хеш-маршрутизации является#, Мы в основном выполняем переходы маршрутизации, прослушивая изменения хэша в URL-адресе.

Преимущество хеша заключается в том, что он имеет лучшую совместимость. Он работает в старой версии IE. Проблема в том, что она всегда существует в URL.#Недостаточно красиво, а хеш-маршрутизация больше похожа на Hack, чем на стандартную, я считаю, что с развитием более стандартизированныхHistory APIЭто постепенно подорвет рынок хеш-маршрутизации.

1.1 Инициализировать класс

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

class Routers {
  constructor() {
    // 以键值对的形式储存路由
    this.routes = {};
    // 当前路由的URL
    this.currentUrl = '';
  }
}

1.2 Реализовать хранение и выполнение хэшей маршрутизации

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

  1. Сохраните хэш маршрута и соответствующую функцию обратного вызова
  2. После запуска изменения хэша маршрута выполните соответствующую функцию обратного вызова.
class Routers {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
  }
  // 将path路径与对应的callback函数储存
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }
  // 刷新
  refresh() {
    // 获取当前URL中的hash路径
    this.currentUrl = location.hash.slice(1) || '/';
    // 执行当前hash路径的callback函数
    this.routes[this.currentUrl]();
  }
}

1.3 Мониторинг соответствующих событий

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

class Routers {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
    this.refresh = this.refresh.bind(this);
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl]();
  }
}

Соответствующий эффект выглядит следующим образом:

Полный пример

кликните сюдаhash routerв поисках синего океана (@xiaomuzhu) on CodePen.


2. Добавьте резервную функцию

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

2.1 Реализовать функцию возврата

Нам нужно создать массивhistoryдля хранения прошлых хеш-маршрутов, таких как/blue, и создайте указательcurrentIndexПрийти сназада такжевперед, продолжатьФункция перемещается, чтобы указать на другой хеш-маршрут.


class Routers {
  constructor() {
    // 储存hash与callback键值对
    this.routes = {};
    // 当前hash
    this.currentUrl = '';
    // 记录出现过的hash
    this.history = [];
    // 作为指针,默认指向this.history的末尾,根据后退前进指向history中不同的hash
    this.currentIndex = this.history.length - 1;
    this.refresh = this.refresh.bind(this);
    this.backOff = this.backOff.bind(this);
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    // 将当前hash路由推入数组储存
    this.history.push(this.currentUrl);
    // 指针向前移动
    this.currentIndex++;
    this.routes[this.currentUrl]();
  }
  // 后退功能
  backOff() {
    // 如果指针小于0的话就不存在对应hash路由了,因此锁定指针为0即可
    this.currentIndex <= 0
      ? (this.currentIndex = 0)
      : (this.currentIndex = this.currentIndex - 1);
    // 随着后退,location.hash也应该随之变化
    location.hash = `#${this.history[this.currentIndex]}`;
    // 执行指针目前指向hash路由对应的callback
    this.routes[this.history[this.currentIndex]]();
  }
}

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

Нажмите, чтобы просмотреть примеры ошибокhash routerв поисках синего океана (@xiaomuzhu) on CodePen.

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

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    // 将当前hash路由推入数组储存
    this.history.push(this.currentUrl);
    // 指针向前移动
    this.currentIndex++;
    this.routes[this.currentUrl]();
  }

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

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

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

class Routers {
  constructor() {
    // 储存hash与callback键值对
    this.routes = {};
    // 当前hash
    this.currentUrl = '';
    // 记录出现过的hash
    this.history = [];
    // 作为指针,默认指向this.history的末尾,根据后退前进指向history中不同的hash
    this.currentIndex = this.history.length - 1;
    this.refresh = this.refresh.bind(this);
    this.backOff = this.backOff.bind(this);
    // 默认不是后退操作
    this.isBack = false;
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    if (!this.isBack) {
      // 如果不是后退操作,且当前指针小于数组总长度,直接截取指针之前的部分储存下来
      // 此操作来避免当点击后退按钮之后,再进行正常跳转,指针会停留在原地,而数组添加新hash路由
      // 避免再次造成指针的不匹配,我们直接截取指针之前的数组
      // 此操作同时与浏览器自带后退功能的行为保持一致
      if (this.currentIndex < this.history.length - 1)
        this.history = this.history.slice(0, this.currentIndex + 1);
      this.history.push(this.currentUrl);
      this.currentIndex++;
    }
    this.routes[this.currentUrl]();
    console.log('指针:', this.currentIndex, 'history:', this.history);
    this.isBack = false;
  }
  // 后退功能
  backOff() {
    // 后退操作设置为true
    this.isBack = true;
    this.currentIndex <= 0
      ? (this.currentIndex = 0)
      : (this.currentIndex = this.currentIndex - 1);
    location.hash = `#${this.history[this.currentIndex]}`;
    this.routes[this.history[this.currentIndex]]();
  }
}

Посмотреть полный примерHash Routerв поисках синего океана (@xiaomuzhu) on CodePen.

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


3. Новая схема маршрутизации HTML5

3.1 History API

Мы можем напрямую запрашивать методы и свойства History API в браузере.

Конечно, наши часто используемые методы на самом деле ограничены.Информация об API истории.

Давайте кратко рассмотрим часто используемые API.

window.history.back();       // 后退
window.history.forward();    // 前进
window.history.go(-3);       // 后退三个页面

history.pushStateОн используется для добавления записей истории в историю просмотров, но не вызывает переход.Этот метод принимает три параметра, а именно:

state: объект состояния, связанный с указанным URL,popstateКогда событие срабатывает, объект передается в функцию обратного вызова. Если вам не нужен этот объект, вы можете заполнить его здесьnull.
title: заголовок новой страницы, но в настоящее время все браузеры игнорируют это значение, поэтому его можно заполнитьnull.
url: новый URL-адрес, который должен находиться в том же домене, что и текущая страница. Адресная строка вашего браузера отобразит этот URL.

history.replaceStateПараметры метода те же, что иpushStateМетод точно такой же, разница в том, что он изменяет текущую запись в истории просмотров вместо добавления записи, а также не вызывает переход.

popstateсобытие, событие popstate запускается всякий раз, когда изменяется история просмотра того же документа (то есть объекта истории).

Обратите внимание, что только вызовpushStateметод илиreplaceStatemethod , это событие не запускается, только пользователь нажимает кнопку браузера «назад» и «вперед» или использует JavaScript для вызоваback,forward,goметод срабатывает.

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

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

3.2 Внедрение маршрутизации по новому стандарту

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

Таким образом, мини-маршрутизацию на самом деле очень просто реализовать.

class Routers {
  constructor() {
    this.routes = {};
    // 在初始化时监听popstate事件
    this._bindPopState();
  }
  // 初始化路由
  init(path) {
    history.replaceState({path: path}, null, path);
    this.routes[path] && this.routes[path]();
  }
  // 将路径和对应回调函数加入hashMap储存
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  // 触发路由对应回调
  go(path) {
    history.pushState({path: path}, null, path);
    this.routes[path] && this.routes[path]();
  }
  // 监听popstate事件
  _bindPopState() {
    window.addEventListener('popstate', e => {
      const path = e.state && e.state.path;
      this.routes[path] && this.routes[path]();
    });
  }
}

Нажмите, чтобы просмотреть маршрутизацию H5H5 Routerв поисках синего океана (@xiaomuzhu) on CodePen.


резюме

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

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


следующее уведомление

Подготовьте статью о следующем выпускедвусторонняя привязкатему, потому что многие знают толькоObject.definedProperty, не могу не углубить:

  1. такжезахват данных,а такжеProxyКаковы преимущества и недостатки по сравнению с?
  2. Помимо перехвата данных, есть ли другой способ добиться двусторонней привязки?
  3. Как другие подходы (например, грязное обнаружение, наблюдаемый шаблон, модель данных и т. д.) сравниваются с захватом данных?

Так как задействовано слишком много фреймворков и точек знаний, я уже начал маленькую 2000 слов, и думаю, делить ее или нет.Часть втораяРазместите это, но я считаю, что это решает все ваши сомнения по поводу двусторонней привязки.