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

Вступление

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

Маршрутизация в приложении Reason-React основана на использовании языковых функций ReasonML, таких как сопоставление с образцом, а также на том факте, что ReasonReact поставляется со встроенным маршрутизатором , который охватывает самые основные варианты использования.

Объем этой статьи можно резюмировать следующим образом:
1. Базовая маршрутизация
2. Расширенная маршрутизация
3. Реализация компонента маршрутизатора

ReasonReact.Router

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

module type Router = {
  type watcherID;
  type url = {
    path: list(string),
    hash: string,
    search: string,
  };
  let watchUrl: (url => unit) => watcherID;
  let unwatchUrl: watcherID => unit;
};

Запись url состоит из hash, path и search, и мы можем подписаться и отказаться от подписки на любые изменения URL-адресов с помощью watchUrl и unwatchUrl.

watcherID возвращается при подписке на watchUrl и требуется, когда нужно отказаться от подписки через unwatchUrl.

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

Нашему компоненту App верхнего уровня необходимо выполнить маршрутизацию на выбранную страницу, которая на данный момент может быть представлена ​​через http: // localhost: 3000 # users. Итак, мы заинтересованы в доступе к текущему URL хешу и переходу к соответствующему компоненту.

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

type page =
  | Dashboard
  | Users;

Предположим, у нас есть настройка модуля Приложение, а также компоненты страницы. Компонент Out App может выглядеть примерно так:

module App = {
  type state = {route: page};
  type action =
    | UpdatePage(page);
  let component = ReasonReact.reducerComponent("App");
  let make = _children => {
    ...component,
    initialState: () => {route: Dashboard},
    reducer: (action, _state) =>
      switch (action) {
      | UpdatePage(route) => ReasonReact.Update({route: route})
      },
    render: ({state}) =>
      <div>
        (
          switch (state.route) {
          | Dashboard => <Dashboard />
          | Users => <Users />
          }
        )
      </div>,
  };
};

Вы могли заметить, что уже определен тип action и что Dashboard определяется как маршрут по умолчанию внутри initialState.
Где бы мы подписались на какие-либо события? Вероятно, внутри didMount, а затем отмена подписки внутри willUnmount. Интересно, что ReactReason также представил подписки через V0.3.1, что означает, что мы можем передать кортеж подписки и отмены подписки, который вызывается при монтировании и, соответственно, размонтировании компонента.

subscriptions: self => [
  Sub(
    () =>
      ReasonReact.Router.watchUrl(url =>
        switch (url.hash) {
        | "users" => self.send(UpdatePage(Users))
        | _ => self.send(UpdatePage(Dashboard))
        }
      ),
    ReasonReact.Router.unwatchUrl,
  ),
],

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

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

module type Router = {
  ...
  let dangerouslyGetInitialUrl: unit => url;
};

Если мы переработаем initialState и воспользуемся опасноGetInitialUrl, компилятор будет жаловаться, потому что hash является строкой. Чего не хватает, так это функции сопоставления, которая принимает url и возвращает соответствующую страницу и наоборот. Давайте реализуем модуль, который этим займется.

module type Mapper = {
  let toPage: ReasonReact.Router.url => page;
  let toUrl: page => string;
};

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

module Mapper: Mapper = {
  let toPage = (url: ReasonReact.Router.url) =>
    switch (url.hash) {
    | "users" => Users
    | _ => Dashboard
    };
  let toUrl = page =>
    switch (page) {
    | Users => "users"
    | _ => "dashboard"
    };
};

Теперь, когда у нас есть модуль Mapper, мы можем обновить initialState, чтобы вернуть соответствующий маршрут.

initialState: () => {
  route: ReasonReact.Router.dangerouslyGetInitialUrl() 
    |> Mapper.toPage,
},

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

subscriptions: self => [
  Sub(
    () =>
      ReasonReact.Router.watchUrl(url =>
        self.send(UpdatePage(Mapper.toPage(url)))
      ),
      ReasonReact.Router.unwatchUrl,
    ),
  ]

У нас есть очень простой механизм маршрутизации. Посмотрите полный пример здесь.

Более продвинутая маршрутизация

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

Первый шаг - расширить тип страницы.

type page =
  | Dashboard
  | Users
  | User(int);

Затем нам нужно обновить модуль Mapper.

module Mapper: Mapper = {
  let toPage = (url: ReasonReact.Router.url) =>
    switch (url.path) {
    | ["users"] => Users
    | ["user", id] => User(int_of_string(id))
    | _ => Dashboard
    };
  let toUrl = page =>
    switch (page) {
    | Users => "users"
    | User(id) => "user/" ++ string_of_int(id)
    | _ => "dashboard"
    };
};

Мы переключились с хеша на путь при доступе к URL-адресу. Наконец, нам нужно адаптировать функцию render для случая User(int).

render: ({state}) =>
  <div>
    (
      switch (state.route) {
      | Dashboard => <Dashboard />
      | Users => <Users />
      | User(id) => <User id />
      }
    )
  </div>

С помощью всего лишь пары изменений мы смогли охватить более сложный сценарий. Нам не так много нужно знать, чтобы реализовать маршрутизацию в приложении ReasonReact. В последнем разделе мы создадим компонент Router.

Посмотрите полный пример здесь

Создание компонента маршрутизатора

Возможно, мы захотим создать компонент Router, который позаботится об обработке маршрута. Давайте подумаем, как можно создать запись Config.

module type Config = {
  type route;
  let toUrl: route => string;
  let toRoute: ReasonReact.Router.url => route;
};

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

module CreateRouter = (Config: Config) => {
  type route = Config.route;
  type renderProps = {
    updateRoute: route => unit,
    route,
  };
  type state = {route};
  type action =
    | UpdateRoute(route);
  let component = ReasonReact.reducerComponent("Router");
  let make = (~render, _children) => {
    ...component,
      initialState: () => {
      route: ReasonReact.Router.dangerouslyGetInitialUrl()
        |> Config.toRoute,
    },
    subscriptions: self => [
      Sub(
        () =>
          ReasonReact.Router.watchUrl(url =>
            self.send(UpdateRoute(Config.toRoute(url)))
          ),
        ReasonReact.Router.unwatchUrl,
      ),
    ],
    reducer: (action, _state) =>
      switch (action) {
      | UpdateRoute(route) => ReasonReact.Update({route: route})
      },
    render: self =>
      render({
        updateRoute: route => self.send(UpdateRoute(route)),
        route: self.state.route,
      }),
  };
};

Есть небольшая разница внутри render, где мы будем использовать render prop и также передать текущий маршрут. как updateRoute для предоставленной функции рендеринга.

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

type page =
  | Dashboard
  | Users
  | User(int);
module Config = {
    type route = page;
    let toRoute = (url: ReasonReact.Router.url) =>
      switch (url.path) {
      | ["user", id] => User(int_of_string(id))      
      | ["users"] => Users
      | _ => Dashboard
      };
    let toUrl = route =>
      switch (route) {
      | User(id) => "/user/" ++ string_of_int(id)
      | Users => "/users"
      | Dashboard => "/"
      };
  };
module Router = CreateRouter(Config);

После определения Config теперь мы можем использовать компонент Router внутри компонента App.

module App = {
  let component = ReasonReact.statelessComponent("App");
  let make = _self => {
    ...component,
    render: _self =>
      <Router
        render=(
          ({route}) =>
            switch (route) {
            | Dashboard => <Dashboard />
            | User(id) => <User id />
            | Users => <Users />
            }
        )
      />,
  };
};

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

module Link = {
  let component = ReasonReact.statelessComponent("Link");
  let make = (~route, ~toUrl, ~render, _children) => {
    ...component,
    render: _self => {
      let href = toUrl(route);
      let onClick = e => {
        ReactEventRe.Mouse.preventDefault(e);
        ReasonReact.Router.push(href);
      };
      <a href onClick> (render()) </a>;
    },
  };
};

Затем используйте компонент Link всякий раз, когда мы хотим обновить маршрут.

<Link route=Users toUrl=Config.toUrl render=(() => str("Users!")) />

Посмотрите полный пример здесь.

Outro

Маршрутизация в основном идет прямо из коробки с ReasonReact.

Ссылки

Маршрутизатор ReasonReact

Подписки ReasonReact

Базовый пример

Расширенный пример

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

Если есть более эффективные способы решения этих проблем или у вас есть вопросы, оставьте комментарий здесь или в Twitter.