V2 Router имеет много новых функций. Посмотрим на самые интересные.

Определить маршруты

Прежде всего, когда мы запускаем приложение Framework7, мы должны передать маршруты по умолчанию в параметре массива routes:

var app = new Framework7({
  routes: [
    {
      name: 'about',
      path: '/about/',
      url: './pages/about.html',
    },
    {
      name: 'news',
      path: '/news/',
      url: './pages/news.html',
      options: {
        animate: false,
      },
    },
    {
      name: 'users',
      path: '/users/',
      templateUrl: './pages/users.html',
      options: {
        context: {
          users: ['John Doe', 'Vladimir Kharlampidi', 'Timo Ernst'],
        },
      },
      on: {
        pageAfterIn: function (e, page) {
          // do something after page gets into the view
        },
        pageInit: function (e, page) {
          // do something when page initialized
        },
      }
    },
    // Default route, match to all pages (e.g. 404 page)
    {
      path: '(.*)',
      url: './pages/404.html',
    },
  ],
});

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

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

var view1 = app.views.create('.view-1', {
  el: '.view-1',
  routes: [
    {
      path: '/users/',
      url: './pages/users.html',
    },
    {
      path: '/user/',
      url: './pages/user.html',
    },
  ],
})

Если у вас есть приложение с несколькими представлениями / маршрутизатором, и вы хотите иметь некоторый вид / маршрутизатор для дополнительных маршрутов и не хотите, чтобы эти дополнительные маршруты были доступны в других представлениях, вы можете указать routesAdd параметр в View init:

var view2 = app.views.create('.view-2', {
  el: '.view-2',
  // This routes are only available in this view
  routesAdd: [
    {
      path: '/blog/',
      url: './pages/blog.html',
    },
    {
      path: '/post/',
      url: './pages/post.html',
    },
  ],
})

Хорошо, теперь посмотрим, что означает каждое свойство маршрута:

Путь маршрута

Путь / URL-адрес маршрута, который будет отображаться в адресной строке окна браузера (если pushState включен), когда следующий маршрут будет загружен либо с помощью api, либо по ссылке с тем же путем.

Также есть поддержка динамических путей. Итак, если у вас есть следующий путь в вашем маршруте '/blog/users/:userId/posts/:postId/’ и щелкните ссылку с /blog/users/12/posts/25 href, тогда на загруженной странице мы получим доступ к объекту `route.params`, содержащему { userId: 12, postId: 25 }

Сопоставление пути маршрута обрабатывается библиотекой Path To Regexp, поэтому все, что там поддерживается, также поддерживается в Framework7. Например, если вы хотите добавить маршрут по умолчанию, который соответствует всем путям, мы можем использовать регулярное выражение, например:

// Default route, match to all pages (e.g. 404 page)
{
  path: '(.*)',
  url: './pages/404.html',
},

Содержание маршрута

В предыдущих примерах мы указали параметр url для каждого маршрута. Этот параметр определяет, как и что загружать для этого маршрута. Он принимает разные свойства (не только URL):

  • url - загружать содержимое страницы через Ajax,
  • content - создает динамическую страницу из указанной строки содержимого,
  • pageName - загрузить страницу из DOM с таким же атрибутом data-name (предыдущая встроенная страница),
  • el - загрузить страницу из DOM по переданному HTMLElement,
  • template - загрузить содержимое страницы из переданной строки или функции шаблона Template7,
  • templateUrl - загружать содержимое страницы с url-адреса через Ajax и компилировать его с помощью Template7,
  • component - загрузить страницу из переданного компонента маршрутизатора Framework7 (см. Ниже),
  • componentUrl - загружать страницы как компонент через Ajax,
  • async - выполнять необходимые асинхронные манипуляции и возвращать содержимое конкретного маршрута

Вот пример всех возможных вариантов:

routes: [
  // Load via Ajax
  {
    path: '/about/',
    url: './pages/about.html',
  },
  // Dynamic page from content
  {
    path: '/news/',
    content: `
      <div class="page">
        <div class="page-content">
          <div class="block">
            <p>This page created dynamically</p>
          </div>
        </div>
      </div>
    `,
  },
  // By page name (data-name="services") presented in DOM
  {
    path: '/services/',
    pageName: 'services',
  },
  // By page HTMLElement
  {
    path: '/contacts/',
    el: document.querySelector('.page[data-name="contacts"]'),
  },
  // By template
  {
    path: '/template/:name/',
    template: `
      <div class="page">
        <div class="page-content">
          <div class="block">
            <p>Hello {{$route.params.name}}</p>
          </div>
        </div>
      </div>
    `,
  },
  // By template URL
  {
    path: '/blog/',
    templateUrl: './pages/blog.html',
  },
  // By component
  {
    path: '/posts/',
    component: {
      // look below
    },
  },
  // By component url
  {
    path: '/post/:id/',
    componentUrl: './pages/component.html',
  },
  // Async
  {
    path: '/something/',
    async: function (routeTo, routeFrom, resolve, reject) {
      // Requested route
      console.log(routeTo);
      // Get external data and return template7 template
      $.getJSON('http://some-endpoint/', function (data) {
        resolve(
          // How and what to load: template
          {
            template: '<div class="page">{{users}}</div>'
          },
          // Custom template context
          {
            context: {
              users: data,
            },
          }
        );
      });
    }
  }
],

Варианты маршрута

Объект с дополнительными параметрами маршрута (необязательно):

  • animate - должна ли страница быть анимированной или нет (перезаписывает настройки роутера по умолчанию),
  • history - должна ли страница сохраняться в истории роутера,
  • pushState - должна ли страница сохраняться в состоянии браузера (перезаписывает глобальный параметр маршрутизатора pushState),
  • reloadCurrent - заменить текущую страницу на новую из маршрута,
  • reloadPrevious - заменить предыдущую страницу в истории на новую из маршрута,
  • reloadAll - загрузить новую страницу и удалить все предыдущие страницы из истории и DOM,
  • context - настраиваемый контекст для страницы Template7 / Component (с использованием параметров template, templateUrl, component или componentUrl)

Маршрут События

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

var app = new Framework7({
  routes: [
    // ...
    {
      path: '/users/',
      url: './pages/users.html',
      on: {
        pageBeforeIn: function (event, page) {
          // do something before page gets into the view
        },
        pageAfterIn: function (event, page) {
          // do something after page gets into the view
        },
        pageInit: function (event, page) {
          // do something when page initialized
        },
        pageBeforeRemove: function (event, page) {
          // do something before page gets removed from DOM
        },
      }
    },
    // ...
  ],
});

Обратите внимание, что такие события маршрута на самом деле являются событиями DOM, поэтому каждый такой обработчик будет принимать event в качестве первого аргумента с самим событием и page в качестве второго аргумента с данными страницы.

Вложенные маршруты

Также возможно наличие вложенных маршрутов (маршрутов в маршрутах):

routes = [
  {
    path: '/faq/',
    url: './pages/faq.html',
  },
  {
    path: '/catalog/',
    url: './pages/catalog.html',
    routes: [
      {
        path: 'computers/',
        url: './pages/computers.html',
      },
      {
        path: 'monitors/',
        url: './pages/monitors.html',
      },
      ...
    ],
  }
];

Что это значит? Для лучшего понимания, на самом деле (под капотом) такие маршруты будут объединены в следующие:

routes = [
  {
    path: '/faq/',
    url: './pages/faq.html',
  },
  {
    path: '/catalog/',
    url: './pages/catalog.html',
  }
  {
    path: '/catalog/computers/',
    url: './pages/computers.html',
  },
  {
    path: '/catalog/monitors/',
    url: './pages/monitors.html',
  },
];

Допустим, мы на /catalog/ странице и имеем следующие ссылки:

  1. <a href=”computers/”>Computers</a> - будет работать как положено. Ссылка будет объединена с текущим маршрутом (/catalog/ + computers/), и у нас будет /catalog/computers/, который есть в наших маршрутах.
  2. <a href=”./computers/”>Computers</a> - будет работать так же, как случай 1, потому что ./ в начале пути означает тот же подуровень.
  3. <a href=”/catalog/computers/”>Computers</a> - также будет работать так же, как и в случае 1, потому что / (косая черта) в начале означает root. И у нас такой корневой маршрут есть в объединенных маршрутах.
  4. <a href=”/computers/”>Computers</a> - не сработает, поскольку / (косая черта) в начале означает root. И такого /computers/ корневого маршрута в наших маршрутах нет и не будет.

Маршрутизируемые вкладки

Вкладки в версии 2 также могут быть маршрутизируемыми. Что означают маршрутизируемые вкладки и чем они хороши? Во-первых, это дает возможность переходить на вкладки по обычным ссылкам, а не по так называемым специальным вкладкам-ссылкам. Во-вторых, при переходе по такому маршруту вы можете загрузить страницу с открытой нужной вкладкой. В-третьих, при включенном Push State та же вкладка будет открываться при перемещении назад и вперед по истории. И последнее, но не менее важное: при использовании маршрутизируемых вкладок вы можете загружать содержимое вкладок так же, как и для страниц, то есть используя url, content, template, templateUrl, component или componentUrl:

routes = [
  {
    path: '/about-me/',
    url: './pages/about-me/index.html',
    // Pass "tabs" property to route
    tabs: [
      // First (default) tab has the same url as the page itself
      {
        path: '/',
        id: 'about',
        // Fill this tab content from content string
        content: `
          <div class="block">
            <h3>About Me</h3>
            <p>...</p>
          </div>
        `
      },
      // Second tab
      {
        path: '/contacts/',
        id: 'contacts',
        // Fill this tab content via Ajax request
        url: './pages/about-me/contacts.html',
      },
      // Third tab
      {
        path: '/cv/',
        id: 'cv',
        // Load this tab content as a component via Ajax request
        componentUrl: './pages/about-me/cv.html',
      },
    ],
  }
]

На странице / about-me / у нас может быть, например, следующая структура:

<div class="page">
  <div class="navbar">
    <div class="navbar-inner">
      <div class="title">About Me</div>
    </div>
  </div>
  <div class="toolbar tabbar">
    <div class="toolbar-inner">
      <a href="./" class="tab-link" data-route-tab-id="about">About</a>
      <a href="./contacts/" class="tab-link" data-route-tab-id="contacts">>Contacts</a>
      <a href="./cv/" class="tab-link" data-route-tab-id="cv">>CV</a>
    </div>
  </div>
  <div class="tabs tabs-routable">
    <div class="tab page-content" id="about"></div>
    <div class="tab page-content" id="contacts"></div>
    <div class="tab page-content" id="cv"></div>
  </div>
</div>

Практически то же самое, что и с обычными вкладками, но с той разницей, что в ссылках вкладок и вкладках больше нет классов «tab-link-active» и «tab-active». Эти классы и вкладки будут переключаться маршрутизатором. И есть новый атрибут «data-route-tab-id», он необходим переключателю вкладок, чтобы понимать, какая ссылка относится к выбранному маршруту.

Маршрутизируемые модальные окна

Модальные окна также маршрутизируемы. Под модальными окнами здесь мы подразумеваем следующие компоненты: Popup, Popover, Actions Sheet, Login Screen, Sheet (ранее модальное окно Picker). Вероятно, здесь больше вариантов использования Popup и Login Screen.

И те же функции, что и у маршрутизируемых вкладок:

  • дает возможность открывать модальные окна по обычным ссылкам вместо так называемых специальных ссылок или API,
  • с включенным состоянием Push, то же модальное окно будет открываться, когда вы обновляете браузер, перемещаетесь назад и вперед по истории,
  • с маршрутизируемыми модальными окнами вы можете загружать сам модальный файл и его содержимое так же, как и для страниц, то есть используя url, content, template, templateUrl, component или componentUrl
routes = [
  ...
  // Creates popup from passed HTML string
  {
    path: '/popup-content/',
    popup: {
      content: `
        <div class="popup">
          <div class="view">
            <div class="page">
              ...
            </div>
          </div>
        </div>
      `
    }
  },
  // Load Login Screen from file via Ajax
  {
    path: '/login-screen-ajax/',
    loginScreen: {
      url: './login-screen.html',
      /* login-screen.html contains: 
        <div class="login-screen">
          <div class="view">
            <div class="page">
              ...
            </div>
          </div>
        </div>
      */
    },
  },
  // Load Popup from component file
  {
    path: '/popup-component/',
    loginScreen: {
      componentUrl: './popup-component.html',
      /* popup-component.html contains:
        <template>
          <div class="popup">
            <div class="view">
              <div class="page">
                ...
              </div>
            </div>
          </div>
        </template>
        <style>...</style>
        <script>...</script>
      */
    },
  },
      
  // Use async route to check if the user is logged in:
  {
    path: '/secured-page/',
    async(routeTo, routeFrom, resolve, reject) {
      if (userIsLoggedIn) {
        resolve({
          url: 'secured-page.html',
        });
      } else {
        resolve({
          loginScreen: { 
            url: 'login-screen.html'
          } ,
        });
      }
    },
  }
]

Согласно приведенному выше примеру:

  • при нажатии на ссылку с атрибутом href «/ popup-content /» открывается всплывающее окно с указанным строковым содержимым,
  • когда вы нажимаете ссылку с атрибутом href «/ login-screen-ajax /», он выполняет запрос Ajax к файлу «login-screen.html» и открывает его как экран входа в систему,
  • когда вы нажимаете ссылку с атрибутом href «/ popup-component /», он выполняет Ajax-запрос к файлу «popup-component.html», анализирует его как компонент маршрутизатора и открывает как всплывающее окно,
  • когда вы нажимаете ссылку с атрибутом href «/ secured-content /», она загружает страницу из «secured-page.html», если пользователь вошел в систему, или открывает экран входа в систему из файла «login-screen.html», если пользователь не вошел в систему в.

Компонент маршрутизатора

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

$(document).on('page:init', function (e) {
  var page = e.detail.page;
  if (page === 'page1') {
    // page1 logic here
  }
  if (page === 'page2') {
    // page2 logic here
  }
  if (page === 'page3') {
    // page3 logic here
  }
  ...etc
});

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

Так что же такое компонент маршрутизатора? Компонент маршрутизатора в основном представляет собой объект со следующими свойствами (все свойства необязательны):

  • template - строковый шаблон. Будет скомпилирован как шаблон Template7
  • render - функция рендеринга для рендеринга компонента. Должен возвращать компонентную html-строку или HTMLElement
  • data - функция, должна возвращать данные контекста компонента
  • style - строка с составляющими стилями CSS. Стили будут добавлены в документ после того, как компонент будет смонтирован (добавлен в DOM), и удалены после того, как компонент будет уничтожен (удален из DOM).
  • methods - объект с компонентными методами
  • on - объект с обработчиками событий страницы
  • beforeCreate, created, beforeMount, mounted, beforeDestroy, destroyed - методы перехватчиков жизненного цикла компонентов.

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

routes = [
  ...
  {
    path: '/some-page/',
    component: {
      template: `
        <div class="page">
          <div class="navbar">
            <div class="navbar-inner">
              <div class="title">{{title}}</div>
            </div>
          </div>
          <div class="page-content">
            <a @click="openAlert" class="red-link">Open Alert</a>
            <div class="list simple-list">
              <ul>
                {{#each names}}
                  <li>{{this}}</li>
                {{/each}}
              </ul>
            </div>
          </div>
        </div>
      `,
      style: `
        .red-link {
          color: red;
        }
      `,
      data: function () {
        return {
          title: 'Component Page',
          names: ['John', 'Vladimir', 'Timo'],
        }
      },
      methods: {
        openAlert: function () {
          var self = this.$app.dialog.alert('Hello world!');
        },
      },
      on: {
        pageInit: function () {
          // do something on page init
        },
        pageAfterOut: function () {
          // page has left the view
        },
      }
    },
  },
  ...
]

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

Все методы компонента и компилятор Template7 выполняются в контексте компонента.

Что такое контекст компонента. Контекст компонента - это объект, который вы вернули в data методе, расширенный следующими полезными свойствами:

  • $, $$, $dom7 - одинаковые псевдонимы для Dom7,
  • $app - экземпляр приложения Framework7,
  • $root - корневые данные и методы, которые вы указали в тех же свойствах data и methods в инициализации приложения (new Framework7),
  • $route - текущий маршрут. Содержит объект с маршрутом query, hash, params, path и url,
  • $router - ссылка на связанный экземпляр роутера,
  • $theme - используется объект со свойствами md и ios, которые true в соответствующей теме.

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

routes = [
  ...
  {
    path: '/some-page/',
    componentUrl: './some-page.html',
  },
  ..
];

И в some-page.html:

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-inner">
        <div class="title">{{title}}</div>
      </div>
    </div>
    <div class="page-content">
      <a @click="openAlert">Open Alert</a>
      <div class="list simple-list">
        <ul>
          {{#each names}}
            <li>{{this}}</li>
          {{/each}}
        </ul>
      </div>
    </div>
  </div>
</template>
<style>
  .red-link {
    color: red;
  }
</style>
<script>
  return {
    data: function () {
      return {
        title: 'Component Page',
        names: ['John', 'Vladimir', 'Timo'],
      }
    },
    methods: {
      openAlert: function () {
        var self = this.$app.dialog.alert('Hello world!');
      },
    },
    on: {
      pageInit: function () {
        // do something on page init
      },
      pageAfterOut: function () {
        // page has left the view
      },
    }
  }
</script>

Что ж, теперь он намного чище. Теги <template> и <style> будут автоматически преобразованы в те же свойства экспортируемого компонента.

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

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

Переименованные события страницы

События страницы также переработаны. Теперь на каждой странице следующие события:

  • pageMounted (page:mounted) - срабатывает сразу после добавления HTML-элемента страницы в DOM
  • pageInit (page:init) - срабатывает после инициализации всех компонентов страницы
  • pageBeforeIn (page:beforein) - запускается прямо перед анимацией страницы для текущего представления (как для прямого, так и для обратного направлений навигации)
  • pageAfterIn (page:afterin) - запускается сразу после анимации страницы для текущего представления (как для прямого, так и для обратного направлений навигации)
  • pageBeforeOut (page:beforeout) - запускается прямо перед анимацией страницы вне текущего представления (как для прямого, так и для обратного направлений навигации)
  • pageAfterOut (page:afterout) - запускается сразу после анимации страницы вне текущего представления (как для прямого, так и для обратного направлений навигации)
  • pageBeforeRemove (page:beforeremove) - срабатывает прямо перед удалением страницы из DOM

Начальная страница

Начальную страницу также можно правильно загрузить с помощью routes. В макете приложения мы должны оставить поле View пустым:

<body>
  <div id="app">
    <div class="view"></div>
  </div>
</body>

В маршрутах мы можем указать «домашний» маршрут, например:

routes: [
  {
    path: '/',
    url: './home.html'
  },
  ...
]

И когда мы запускаем просмотр, нам (рекомендуется) нужно указать, что это URL-адрес по умолчанию:

app.views.create('.view', {
  url: '/'
})

Вот и все, теперь при загрузке приложения содержимое домашней страницы будет загружено из файла «home.html».

Перенаправление и псевдоним

Начиная с beta.16 в маршрутах также поддерживаются перенаправления и псевдонимы.

Перенаправление

routes = [
  {
    path: '/foo/',
    url: 'somepage.html',
  },
  {
    path: '/bar/',
    redirect: '/foo/',
  }
]

Это означает, что когда мы запрашиваем /bar/ URL-адрес, маршрутизатор будет перенаправлять на /foo/ URL-адрес, а затем искать маршрут, соответствующий этому новому URL-адресу. В этом случае он будет соответствовать первому маршруту с path/foo/ и загружать страницу из somepage.html.

Псевдоним

routes = [
  {
    path: '/foo/',
    url: 'somepage.html',
    alias: '/bar/',
  },
  {
    path: '/foo2/',
    url: 'anotherpage.html',
    alias: ['/bar2/', '/baz/', '/baz2/'],
  }
]

Псевдоним в этом случае в основном означает, что один и тот же маршрут может иметь несколько путей доступа. Итак, согласно приведенному выше примеру:

  • если мы запрашиваем страницу по /foo/ или /bar/ URL, тогда первый маршрут будет соответствовать, и мы получим страницу, загруженную из «somepage.html»
  • если мы запрашиваем страницу по /foo2/, /bar2/, /baz/, /baz2/ URL, тогда будет совпадать второй маршрут, и мы будем загружать страницу из «anotherpage.html»

Улучшенное состояние push

Состояние push в версии 2 сильно переработано и улучшено. Теперь он более корректно обрабатывает историю, поддерживает модальные окна, вкладки и значительно улучшает восстановление истории при перезагрузке приложения.

Новый API

Теперь у роутера всего 2 основных метода:

  1. router.navigate(url[, options]) - перейти к указанному URL-адресу с указанными параметрами (необязательно).
  2. router.back([url, options]) - вернуться в историю. Оба параметра необязательны.

Резюме

Как видите, Router v2 - это почти что-то совершенно новое. Он был переработан, улучшен по всем параметрам. Да, потребуется время, чтобы принять и понять его новую философию, но оно того стоит. И в новом маршрутизаторе есть гораздо больше, в том числе переработанные переходы между страницами (теперь они основаны на JS, а не на CSS), поддержка панели навигации внутри страницы для темы iOS, восстановление положения прокрутки, новые события маршрутизатора и т. Д. Но более подробная информация о таких функциях будет рассмотрена в документации v2.