Новый уровень React: React Router
React

Новый уровень React: React Router

Оригинал: Leveling Up With React: React Router, Brad Westfall

Данная статья — первая из трех частей серии о React, написанной Бредом Вестфолом (Brad Westfall).

Серия статей
Часть 1: React Router (вы здесь!)
Часть 2: Компоненты-контейнеры
Часть 3: Redux

Когда я начал изучать React, я нашел множество руководств для начинающих (например, 1, 2, 3, 4), которые показывают, как создать компонент и отобразить его в DOM. Они прекрасно справляются со своей задачей — дают представление о JSX и своствах компонента. Но я хотел увидеть картину в целом — как React используется в реальном мире в одностраничных приложениях (SPA). Поскольку эта серия охватывает большой объем материала, я не буду рассматривать базовые понятия. Я буду исходить из предположения, что вы уже знаете как создать и отобразить хотя бы один компонент.

Вот несколько полезных материалов для начинающих:

Исходные коды

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

Чтобы сохранить примеры простыми и краткими, мы будем исходить из предположения, что React и React Router подключаются с CDN. Таким образом, вам не нужно будет вызывать require() или import в каждом примере. К концу урока мы познакомимся с Webpack и Babel. И да, мы будем использовать ES6!

React Router

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

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

Например, вот так выглядит рендеринг одного компонента:


var Home = React.createClass({
  render: function() {
    return (<h1>Welcome to the Home Page</h1>);
  }
});

ReactDOM.render((
  <Home />
), document.getElementById('root'));

Вот так компонент будет отрисовываться с помощью React Router:


...

ReactDOM.render((
  <Router>
    <Route path="/" component={Home} />
  </Router>
), document.getElementById('root'));

Обратите внимание, что <Router> и <Route> — это разные вещи. Технически они являются компонентами, но сами не создают DOM-элементов. Хотя внешне выглядит, что компонент <Router> будет отрисован в элементе root, но на самом деле мы только определяем правила, по которым работает приложение. Забегая вперед, скажу, что компоненты не всегда создают элементы DOM, иногда они просто координируют работу внутренних компонентов.

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

Несколько маршрутов

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

Сила React Router в том, что он позволяет вывести разные компоненты в зависимости от того, какой путь в данный момент активен:


ReactDOM.render((
  <Router>
    <Route path="/" component={Home} />
    <Route path="/users" component={Users} />
    <Route path="/widgets" component={Widgets} />
  </Router>
), document.getElementById('root'));

Каждый компонент будет показывать свой компонент, когда URL будет совпадать со свойством path. В каждый момент времени только один из этих компонентов будет выведен в элементе root. Таким образом, мы один раз монтируем корневой компонент, который меняет содержимое при изменении маршрута.

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

Повторное использование лайаута

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

Давайте представим, что мы разрабатываем веб-приложение, напоминающее такой набросок:

Этот макет может быть разбит на повторно используемые части подобным образом:

Мышление в терминах вложенности компонентов позволяет разбивать интерфейс на повторно используемые части.

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

Теперь Search Layout может быть родительским компонентом для любой страницы поиска. И в то время как одни страницы будут отображаться внутри Search Layout, другие будут рендериться непосредственно в Main Layout:

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


<div id="root">

  <!-- Main Layout -->
  <div class="app">
    <header class="primary-header"><header>
    <aside class="primary-aside"></aside>
    <main>

      <!-- Search Layout -->
      <div class="search">
        <header class="search-header"></header>
        <div class="results">

          <!-- User List -->
          <ul class="user-list">
            <li>Dan</li>
            <li>Ryan</li>
            <li>Michael</li>
          </ul>

        </div>
        <div class="search-footer pagination"></div>
      </div>

    </main>
  </div>

</div>
Помните, что элемент root всегда будет присутствовать на странице, это начальный элемент на странице. Мы назвали его root, потому что все приложение будет монтироваться в него. Но это не единственное «правильное название», нет какого-либо соглашения по его именованию. Я выбрал root, поэтому буду использовать его и далее в примерах. Обратите внимание, что монтировать приложение неповредственно в <body> крайне нежелательно.

После создания статического HTML, переведем его в компоненты React:


var MainLayout = React.createClass({
  render: function() {
    // Note the `className` rather than `class`
    // `class` is a reserved word in JavaScript, so JSX uses `className`
    // Ultimately, it will render with a `class` in the DOM
    return (
      <div className="app">
        <header className="primary-header"><header>
        <aside className="primary-aside"></aside>
        <main>
          {this.props.children}
        </main>
      </div>
    );
  }
});

var SearchLayout = React.createClass({
  render: function() {
    return (
      <div className="search">
        <header className="search-header"></header>
        <div className="results">
          {this.props.children}
        </div>
        <div className="search-footer pagination"></div>
      </div>
    );
  }
});

var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        <li>Dan</li>
        <li>Ryan</li>
        <li>Michael</li>
      </ul>
    );
  }
});

Не путайтесь в понятиях «Layout» и «Component». Все три являются обычными компонентами React. Я решил назвать два из них лайаутами, потому что именно такую роль они и выполняют.

Мы будем использовать «вложенные маршруты», чтобы отрендерить UserList внутри SearchLayout, который, в свою очередь, будет выводиться внутри MainLayout. Заметим, что когда UserList выводится внутри родительского SearchLayout, родитель использует this.props.children, чтобы задать его расположение. Все компоненты имеют свойство this.props.children, которое заполняется автоматически, если компонент имеет вложенные элементы. Если компонент не имеет вложенных элементов, свойство this.props.children равно null.

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

Итак, как подключить вложенные компоненты? React Router делает это за нас, когда мы используем вложенные маршруты:


ReactDOM.render((
  <Router>
    <Route component={MainLayout}>
      <Route component={SearchLayout}>
        <Route path="users" component={UserList} />
      </Route> 
    </Route>
  </Router>
), document.getElementById('root'));

Компоненты будут вложены в соответствии с вложенностью маршрутов. Когда посетитель перейдет на страницу /users, React Router смонтирует компонент UserList в SearchLayout, а затем оба эти компонента в MainLayout. В итоге на странице /users мы получим три вложенных компонента в элементе root.

Обратите внимание, что у нас нет маршрута для главной страницы или страницы виджетов. Они были опущены для простоты, давайте их добавим:


ReactDOM.render((
  <Router>
    <Route component={MainLayout}>
      <Route path="/" component={Home} />
      <Route component={SearchLayout}>
        <Route path="users" component={UserList} />
        <Route path="widgets" component={WidgetList} />
      </Route> 
    </Route>
  </Router>
), document.getElementById('root'));
Вы, вероятно, обратили внимание, что в JSX, как в XML, компонент Route может быть написан как одним тегом <Route />, так и двумя: <Route>...</Route>. Это относится к любому компоненту или обычным элементам DOM. Например, <div /> в JSX преобразуется в <div></div> в HTML.

Для краткости, представим, что компонент WidgetList напоминает UserList.

Начальный маршрут

React Router очень выразителен и часто предоставлет больше одного способа сделать что-либо. Например, мы можем переписать пример выше следующим образом:


ReactDOM.render((
  <Router>
    <Route path="/" component={MainLayout}>
      <IndexRoute component={Home} />
      <Route component={SearchLayout}>
        <Route path="users" component={UserList} />
        <Route path="widgets" component={WidgetList} />
      </Route> 
    </Route>
  </Router>
), document.getElementById('root'));

Оба примера работают совершенно одинаково.

Дополнительные атрибуты маршрута

В одних случаях, <Router> имеет атрибут component без атрибута path, как SearchLayout в примере выше. В других случаях, наоборот, у есть атрибут path, но нет атрибута component. Чтобы понять, почему, давайте рассмотрим пример:


<Route path="product/settings" component={ProductSettings} />
<Route path="product/inventory" component={ProductInventory} />
<Route path="product/orders" component={ProductOrders} />

Часть пути /products повторяется. Мы можем удалить повторение, обернув все три маршрута в новый <Route>:


<Route path="product">
  <Route path="settings" component={ProductSettings} />
  <Route path="inventory" component={ProductInventory} />
  <Route path="orders" component={ProductOrders} />
</Route>

Опять же, React Router показывает свою выразительность. Тест: вы заметили проблему с обоими решениями? Сейчас у нас нет правила для пути /product.

Чтобы исправить это, мы можем добавить IndexRoute:


<Route path="product">
  <IndexRoute component={ProductProfile} />
  <Route path="settings" component={ProductSettings} />
  <Route path="inventory" component={ProductInventory} />
  <Route path="orders" component={ProductOrders} />
</Route>

Используйте <Link> вместо <a>

При создании ссылок на маршруты необходимо использовать <Link to=””> вместо <a href=””>. Не беспокойтесь, при использовании компонента <Link> React Router в конечном итоге создаст обычный якорь в DOM. Использование <Link> необходимо React Router для некоторой его магии.

Давайте добавим ссылки (якори) в MainLayout:


var MainLayout = React.createClass({
  render: function() {
    return (
      <div className="app">
        <header className="primary-header"></header>
        <aside className="primary-aside">
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/users">Users</Link></li>
            <li><Link to="/widgets">Widgets</Link></li>
          </ul>
        </aside>
        <main>
          {this.props.children}
        </main>
      </div>
    );
  }
});

Атрибуты компонента <Link> будут переданы ссылке при создании. Например, следующий JSX:


<Link to="/users" className="users">

Превратится в DOM:


<a href="/users" class="users">

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

Активные ссылки

Отличная особенность компонента <Link> в том, что он знает, когда он активен:


<Link to="/users" activeClassName="active">Users</Link>

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

История браузера

Чтобы избежать путаницы, я до сих пор не упомянул одну важную деталь. Компонент <Router> должен знать, какая стратегия отслеживания истории используется. В документации React Router рекомендуется использовать browserHistory следующим образом:


var browserHistory = ReactRouter.browserHistory;

ReactDOM.render((
  <Router history={browserHistory}>
    ...
  </Router>
), document.getElementById('root'));

В предыдущей версии React Router атрибут history не требовался, по умолчанию использовалась стратегия hashHistory. Как следует из названия, она использует # в URL, как, например, роутер в Backbone.js.

С hashHistory URL выглядят так:

  • example.com
  • example.com/#/users?_k=ckuvup
  • example.com/#/widgets?_k=ckuvup

Что это за уродливые параметры запроса?

Если реализована стратегия browserHistory, пути выглядят более органично:

  • example.com
  • example.com/users
  • example.com/widgets

Есть один нюанс на сервере при иcпользовании browserHistory на клиенте. Если пользователь начинает работу с приложением с example.com, а затем переходит на страницы /users или /widgets, React Router обрабатывает этот сценарий как ожидается. Но если пользователь сразу попадет на страницу example.com/widgets или обновит страницу example.com/widgets, браузер должен будет сделать запрос на сервер для пути /widgets. И если на сервере не настроена соответстующая маршрутизация, будет возвращена ошибка 404:

Чтобы решить эту проблему, React Router рекомендует настроить маршрутизацию на сервере. При такой стратегии, какой бы путь не был запрошен, будет отдаваться некий HTML файл, а React Router отобразит правильный компонент.

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

Может ли React Router изоморфно использоваться и на сервере, и на клиенте? Конечно, это возможно, но это выходит за рамки данного урока.

Редирект с browserHistory

Объект browserHistory — это синглтон, который может использоваться в любых файлах. Если вам нужно вручную перенаправить пользователя, вы можете вызвать метод push объекта browserHistory:


browserHistory.push('/some/path');

Шаблоны маршрутов

React Router обрабатывает шаблоны маршрутов так же, как и другие маршрутизаторы:


<Route path="users/:userId" component={UserProfile} />

Этот маршрут сработает, когда пользователь посетит любую страницу, адрес которой начинается с users/, например, /users/1, /users/143 или /users/abc (значение вам нужно валидировать по своему усмотрению).

React Router передаст значение :userId в качестве свойства в UserProfile. Это свойство будет доступно как this.props.params.userId в компоненте UserProfile.

Демо

На данный момент у нас достаточно кода, чтобы показать демонстрацию:

See the Pen React-Router Demo by Brad Westfall (@bradwestfall) on CodePen.

Если вы пощелкаете по ссылкам в примере, вы можете убедиться, что кнопки назад и вперед в браузере работают. Это основная причина, по которой существует стратегия history. Также заметим, что при переходе по ссылкам не делается никаих запросов на сервер, кроме самого первого. Здорово, не правда ли?

ES6

В нашем примере React, ReactDOM и ReactRouter — это глобальные переменные из CDN. Компоненты Router и Route доступны в объекте ReactRouter. Поэтому использовать их нужно было так:


ReactDOM.render((
  <ReactRouter.Router>
    <ReactRouter.Route ... />
  </ReactRouter.Router>
), document.getElementById('root'));

Нам необходимо использовать префикс для всех компонентов из ReactRouter. Или же мы можем использовать синтаксис деструктуризации ES6:


var { Router, Route, IndexRoute, Link } = ReactRouter

Этот код «разворачивает» части ReactRouter в обычные переменные, которые можно использовать напрямую.

С этого момента в примерах серии будут использоваться различные синтаксические конструкции из ES: деструктуризация, оператор расширения, import/export и другие. При встрече каждой новой синтаксической конструкции будет приведено краткое ее описание, в репозитории на GitHub также есть объяснения конструкций ES6.

Сборка с webpack и Babel

Как я говорил в начале, к этой серии статей есть репозиторий на GitHub, где вы можете поэкспериментировать с кодом. Так как мы хотим приблизиться к разработке реального SPA, нам потребуются такие инструенты как webpack и Babel.

webpack будет собирать несколько JavaScript файлов в один для браузера. Babel будет конвертировать код ES6 (ES2015) в ES5, поскольку не все браузеры пока поддерживают ES6.

Если вы еще мало знакомы с этими инструментами, не волнуйтесь, в примерах уже все настроено, поэтому можете сосредоточиться на React. Но не забудьте просмотреть файл README.md в качестве дополнительной документации по рабочему процессу.

Будьте осторожны с устаревшим синтаксисом

Ища информацию о ReactRouter в Google, вы можете наткнуться на множество статей или страниц на StackOverflow, которые были написаны для React Router версии pre-1.0. Многие особенности этой версии сейчас устарели. Вот краткий список:

  • <Route name="" /> устарел. Вместо него используйте .
  • <Route handler="" /> устарел. Вместо него используйте .
  • <NotFoundRoute /> устарел. Альтернатива
  • <RouteHandler /> устарел.
  • willTransitionTo устарел. См. onEnter
  • willTransitionFrom устарел. См. onLeave
  • «Locations» теперь называется «histories».

Полный список для версий 1.0.0 и 2.0.0.

Заключение

Есть еще ряд особенностей React Router, о которых не было рассказано в статье, поэтому не забудьте познакомиться с документацией. Разработчики React Router также создали пошаговый урок и сделали доклад на React.js Conf о том, как был разработан React Router.

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться