Подробное объяснение механизма маршрутизации SPA (без денег не понять~~)

задняя часть внешний интерфейс JavaScript браузер


предисловие

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

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

  • использоватьURLизhash(#)
  • использоватьH5новый методHistory interface

использоватьURLизHash(#)

существуетH5Когда это еще не популярно, обычноSPAоба используютurlизhash(#)В качестве привязки получите значение после #, отслеживайте его изменения, а затем визуализируйте соответствующую подстраницу.Официальный сайт NetEase Cloud Musicявляется использование этой технологии.

Например, ваш адресhttp://localhost:8888/#/abcзатем используйтеlocation.hashВыход#/abc.

Тогда я начну сlocationоб этом объекте.

Первый взглядlocationКаковы официальные атрибуты

Атрибуты описывать
hash Установите или верните URL-адрес, начинающийся с # (якорь)
host Установите или верните имя хоста и номер порта текущего URL-адреса
hostname Установить или вернуть имя хоста текущего URL
href Установить или вернуть полный URL
pathname Задает или возвращает часть пути текущего URL-адреса
port Установить или вернуть номер порта текущего URL-адреса
protocol Установить или вернуть протокол текущего URL
search Задает или возвращает часть URL-адреса, начинающуюся с ?

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

windowВ объекте происходит событие, которое специально отслеживаетсяhashизмениться, то естьonhashchange, сначала нам нужно прослушать это событие:

<body>
  <h1 id="id"></h1>
  <a href="#/id1">id1</a>
  <a href="#/id2">id2</a>
  <a href="#/id3">id3</a>
</body>

<script>
  window.addEventListener('hashchange', e => {
    e.preventDefault()
    document.querySelector('#id').innerHTML = location.hash
  })
</script>

img

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

  • Найдите содержимое узла и измените его (это то, что мы продемонстрировали выше).
  • importОдинJSфайл, внутри файлаexportстрока шаблона
  • использоватьAJAXзагрузить соответствующийHTMLтрафарет

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

importСпособ

определитьJSфайл с именемdemo1.js, введите в него содержимое:

const str = `
  <div>
    我是import进来的JS文件
  </div>
`
export default str

в основном файлеimportзайди и протестируй (используяChromeОбязательно используйте сервер для открытия или откройте напрямую с помощью Firefox):

<body>
  <h1 id="id"></h1>
  <a href="#/id1">id1</a>
  <a href="#/id2">id2</a>
  <a href="#/id3">id3</a>
</body>
<!-- 在 HTML 导入文件记得要加上 type="module" -->
<script type="module">
  import demo1 from './demo1.js'
  document.querySelector('#id').innerHTML = demo1
  window.addEventListener('hashchange', e => {
    e.preventDefault()
    document.querySelector('#id').innerHTML = location.hash
  })
</script>

img

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

Например,vueРамка,.vueфайл — это пользовательский тип файла, использующий классHTMLГрамматика описываетVueкомпоненты. каждый.vueФайл содержит три типа языковых блоков верхнего уровня.<template>,<script>а также<style>,vue-loaderбудет анализировать файл, извлекая каждый языковой блок, передавая другиеloaderобрабатывать и, наконец, собирать их вCommonJSмодуль,module.exportsиз одногоVue.jsкомпонентный объект. .

AJAXСпособ

В этой статье подробно объясняется механизм маршрутизации.AJAXнепосредственно использоватьJQueryЭто колесо.

определитьHTMLфайл с именемdemo2.html, напишите в нем какой-нибудь контент (поскольку на главной странице уже естьhead,bodyт.д. корневые теги, этот файл просто записывает теги, которые необходимо заменить):

<div>
  我是AJAX加载进来的HTML文件
</div>

Пишем в основной файл и тестируем:

<body>
  <h1 id="id"></h1>
  <a href="#/id1">id1</a>
  <a href="#/id2">id2</a>
  <a href="#/id3">id3</a>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="module">
  // import demo1 from './demo1.js'
  // document.querySelector('#id').innerHTML = demo1
  $.ajax({
    url: './demo2.html',
    success: (res) => {
      document.querySelector('#id').innerHTML = res
    }
  })
  window.addEventListener('hashchange', e => {
    e.preventDefault()
    document.querySelector('#id').innerHTML = location.hash
  })
</script>

img

видно, использоватьAJAXЗагруженный файл также вступил в силу.

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

<body>
  <h1 id="id">我是空白页</h1>
  <a href="#/id1">id1</a>
  <a href="#/id2">id2</a>
  <a href="#/id3">id3</a>
</body>
<script type="module">
  import demo1 from './demo1.js'
  // 创建一个 newRouter 类
  class newRouter {
    // 初始化路由信息
    constructor() {
      this.routes = {};
      this.currentUrl = '';
    }
    // 传入 URL 以及 根据 URL 对应的回调函数
    route(path, callback = () => {}) {
      this.routes[path] = callback;
    }
    // 切割 hash,渲染页面
    refresh() {
      this.currentUrl = location.hash.slice(1) || '/';
      this.routes[this.currentUrl] && this.routes[this.currentUrl]();
    }
    // 初始化
    init() {
      window.addEventListener('load', this.refresh.bind(this), false);
      window.addEventListener('hashchange', this.refresh.bind(this), false);
    }
  }
  // new 一个 Router 实例
  window.Router = new newRouter();
  // 路由实例初始化
  window.Router.init();

  // 获取关键节点
  var content = document.querySelector('#id');

  Router.route('/id1', () => {
    content.innerHTML = 'id1'
  });
  Router.route('/id2', () => {
    content.innerHTML = demo1
  });
  Router.route('/id3', () => {
    $.ajax({
      url: './demo2.html',
      success: (res) => {
        content.innerHTML = res
      }
    })
  });
</script>

Эффект следующий:

img

Пока что используйтеhash(#)Реализовано внешнее управление маршрутизацией.

использоватьH5новый методHistory interface

Используется вышеhashХорошо реализовать маршрутизацию по методу, но проблема в том, что это слишком некрасиво ~ Если это не отображается в WeChat или другомURLизAPPНеважно, используете ли вы его в обычном браузере, но вы столкнетесь с проблемами, если будете использовать его в обычном браузере.

таким образом,H5изHistoryрежим решает эту проблему.

существуетH5До,HistoryТолько с несколькимиAPI:

API иллюстрировать
back() откат к последнему посещенномуURL(так же, как браузер, нажав кнопку «Назад»)
forward() вперед назадURL(то же самое, что и браузер, нажав кнопку «Вперед»)
go(n) nПолучить целое число, перейти на страницу, указанную целым числом, напримерgo(1)эквивалентноforward(),go(-1)эквивалентноback(),go(0)Эквивалентно обновлению текущей страницы

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

Однако пришелH5эпоха, новыйH5Это дает ему больше новых возможностей:

кеш туда и обратно

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

PS: этот кеш сохраняет данные страницы, состояние DOM и js, фактически оставляя всю страницу нетронутой.

Добавить запись в стек истории: pushstate (состояние, заголовок, URL)

Поддержка браузера:IE10+

  • состояние: аJSОбъекты (не более 640кБ), в основном используемые вpopstateполучается как параметр в событии. Если вам не нужен этот объект, вы можете заполнить его здесьnull
  • title: заголовок новой страницы, некоторые браузеры (например, Firefox) игнорируют этот параметр, поэтому обычноnull
  • url: адрес новой истории,Может быть адресом страницы, но также и значением привязки,новыйurlдолжен соответствовать текущемуurlВ том же домене, иначе будет выброшено исключение.Если этот параметр специально не отмечен, он будет установлен на текущий документurl

Каштан:

// 现在是 localhost/1.html
const stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');

// 浏览器地址栏将立即变成 localhost/2.html
// 但!!!
// 不会跳转到 2.html
// 不会检查 2.html 是否存在
// 不会在 popstate 事件中获取
// 不会触发页面刷新

// 这个方法仅仅是添加了一条最新记录

Помимо этого, следует отметить несколько моментов:

  • БудуurlНе срабатывает, если установлено значение привязкиhashchange
  • В соответствии с политикой того же происхождения, если установленоразныеАдрес доменного имени сообщит об ошибке. Цель этого состоит в том, чтобы пользователи не думали, что они являются одним и тем же веб-сайтом. Если такого ограничения нет, это будет легко выполнитьXSS,CSRFи т. д. атака

Изменить текущую историю: replaceState(state, title, url)

Поддержка браузера:IE10+

  • Параметры имеют тот же смыслpushstate
  • Изменить текущую запись истории вместо добавления новой
  • тоже не заводитсяpopstate

history.state

Поддержка браузера:IE10+

  • возвращает текущую историюstate.

popstate

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

Примечание: если только звонитьpushStateметод илиreplaceStatemethod , событие не будет запущено, только пользователь щелкнет браузеротступатькнопка ивперед, продолжатькнопку или используйтеJavaScriptпередачаback,forward,goметод срабатывает. Кроме того, это событие предназначено только для одного и того же документа, если переключение истории просмотров приводит к загрузке другого документа, это событие не будет запущено.

Каштан:

window.onpopstate= (event) => {
&emsp;&emsp;console.log(event.state) //当前历史记录的state对象
}

выполнить

Зная так много, давайте начнемHistoryМаршрутизация шаблонов сейчас!

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

<body>
  <h1 id="id">我是空白页</h1>
  <a class="route" href="/id1">id1</a>
  <a class="route" href="/id2">id2</a>
  <a class="route" href="/id3">id3</a>
</body>
import demo1 from './demo1.js'
  // 创建一个 newRouter 类
  class newRouter {
    // 初始化路由信息
    constructor() {
      this.routes = {};
      this.currentUrl = '';
    }
    route(path, callback) {
      this.routes[path] = (type) => {
        if (type === 1) history.pushState( { path }, path, path );
        if (type === 2) history.replaceState( { path }, path, path );
        callback()
      };
    }
    refresh(path, type) {
      this.routes[this.currentUrl] && this.routes[this.currentUrl](type);
    }
    init() {
      window.addEventListener('load', () => {
        // 获取当前 URL 路径
        this.currentUrl = location.href.slice(location.href.indexOf('/', 8))
        this.refresh(this.currentUrl, 2)
      }, false);
      window.addEventListener('popstate', () => {
        this.currentUrl = history.state.path
        this.refresh(this.currentUrl, 2)
      }, false);
      const links = document.querySelectorAll('.route')
      links.forEach((item) => {
        // 覆盖 a 标签的 click 事件,防止默认跳转行为
        item.onclick = (e) => {
          e.preventDefault()
          // 获取修改之后的 URL
          this.currentUrl = e.target.getAttribute('href')
          // 渲染
          this.refresh(this.currentUrl, 2)
        }
      })
    }
  }
  // new 一个 Router 实例
  window.Router = new newRouter();
  // 实例初始化
  window.Router.init();

  // 获取关键节点
  var content = document.querySelector('#id');

  Router.route('/id1', () => {
    content.innerHTML = 'id1'
  });
  Router.route('/id2', () => {
    content.innerHTML = demo1
  });
  Router.route('/id3', () => {
    $.ajax({
      url: './demo2.html',
      success: (res) => {
        content.innerHTML = res
      }
    })
  });

Демонстрационная диаграмма показана ниже:

img

Суммировать

При нормальных обстоятельствах,hashа такжеhistoryВы можете, если вас больше не волнует внешний вид,#Символический смесительURLВыглядит чуть менее красиво. Кроме того, согласноMozilla Develop Networkзнакомство, звонокhistory.pushState()по сравнению с прямой модификациейhash, имеются следующие преимущества:

  • pushState()установить новыйURLможет быть таким же, как текущийURLгомологичный произвольныйURL;а такжеhashтолько модифицируемый#последняя часть, поэтому может быть установлена ​​только с текущимURLтот же документURL
  • pushState()установить новыйURLможно совместить с текущимURLТочно, это также добавит запись в стек; иhashНовый набор значений должен отличаться от исходного, чтобы инициировать действие по добавлению записи в стек.
  • pushState()пройти черезstateObjectПараметры могут добавлять в запись данные любого типа;hashМожно добавлять только короткие строки;
  • pushState()Доступны дополнительные настройкиtitleСвойства для последующего использования.

Посмотрите на это с другой стороныhistoryРежим полон счастья, и он кажется полностью заменимымhashрежим, а на самом делеhistoryНе все хорошо, хотя это легко сделать в браузере, но пройти действительно необходимоURLПерейти к серверной частиHTTPПо запросу появляется разница между ними. Особенно когда пользователь вручную вводитURLПосле нажатия Enter или при обновлении (перезапуске) браузера.

  • hashрежим, толькоhashСодержимое перед символом будет включено в запрос, напримерhttp://www.qqq.com, так что для бэкенда, даже если маршрут не полностью пройден, он не вернется404ошибка.
  • historyрежим, интерфейсURL должени фактический запрос к бэкендуURLпоследовательно, какhttp://www.qqq.com/book/id. Если в бэкенде отсутствует пара/book/idОбработчик маршрута вернется404ошибка.Vue-RouterОфициальный сайт описывает это так: «Однако, чтобы хорошо играть в этот режим, ему также нужна поддержка фоновой конфигурации... Итак, вам нужно добавить ресурс-кандидат, который охватывает все ситуации на стороне сервера: еслиURLне соответствует никаким статическим ресурсам, он должен возвращать то же самоеindex.htmlстраница, эта страница - тыappзависимые страницы. "
  • должен быть в бэкенде (ApacheилиNginx) для простой настройки маршрутизации и с интерфейсной маршрутизацией404Поддержка страницы.

Наконец, я сожалею, что продвигаю свою работу на основеTaroБиблиотека компонентов, написанная фреймворком:MP-ColorUI.

Я очень рад, что могу сыграть главную роль, спасибо.

Щелкните здесь для документации

Нажмите здесь, чтобы узнать адрес GitHub