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

Обновление 4/11/19: ReasonReact 0.7.0 представил некоторые обновления базового API (включая хуки! 😍), поэтому некоторые из приведенных ниже фрагментов кода немного устарели. Я скоро обновлю это, чтобы эта публикация оставалась актуальной.

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

Мы рассмотрим:

  1. Начало работы с проектом ReasonReact
  2. Настройка клиента с reason-apollo
  3. Отправка запросов
  4. Мутация данных
  5. И некоторые причуды и синтаксические особенности ReasonML

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

Если у вас есть опыт работы с JavaScript и GraphQL, но вы хотите изучить ReasonML, продолжайте читать, но держите документацию под рукой.

Начало работы - создание проекта ReasonReact

Чтобы начать работу с ReasonML, мы должны сначала установить cli, bs-platform, который обрабатывает начальную загрузку проекта. Вы также должны получить плагин редактора, который помогает в разработке приложений ReasonML. Если вы используете VSCode, я предпочитаю плагин reason-vscode by Jared Forsyth.

npm install -g bs-platform

Это устанавливает компилятор BuckleScript, который превращает наш ReasonML в замечательный JavaScript, который уже прошел проверку типов и может быть запущен в браузере.

Теперь мы можем инициализировать наш проект и сразу приступить к нему.

bsb -init reason-graphql-example -theme react
cd reason-graphql-example
npm install
  • Аргумент init указывает имя инициализируемого проекта.
  • Аргумент theme указывает шаблон, который мы хотим использовать. Обычно я просто выбираю тему реакции.
  • Мы запускаем npm install для установки зависимостей, как и в любом другом проекте JavaScript.

Когда проект готов, мы можем попробовать его построить. На двух отдельных панелях терминала запустите:

npm start
# and
npm run webpack
  • npm start сообщает BuckleScript (с помощью команды bsb) о необходимости создания проекта следить за изменениями в ваших файлах .re.
  • npm run webpack запускает веб-пакет для создания вашего основного пакета JavaScript

Совет: вы заметите, что выходные данные веб-пакета находятся в папке build, а файл index.html - в папке src. Мы можем немного упростить обслуживание проекта, переместив файл index.html в папку сборки и переписав тег скрипта так, чтобы он указывал на соседний файл Index.js.

После всего этого вы можете обслуживать свою папку сборки, используя http-server build, serve build, или, без сервера, open build/index.html и проверять проект.

Когда я разрабатываю проект ReasonML, я использую 3 вкладки терминала:

  1. npm start для компиляции ReasonML в JavaScript
  2. npm run webpack для объединения JavaScript
  3. serve build для обслуживания сборки на порту

Прежде чем мы перейдем к интересным вещам, мы все равно должны очистить шаблон и настроить react-apollo.

Удалите файлы Component1.re и Component2.re, а затем обновите Index.re до следующего:

ReactDOMRe.renderToElementWithId(<App />, "root");

Обновите index.html до:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>ReasonML GraphQL Example</title>
  </head>
  <body>
    <div id="root"></div>

    <script src="./Index.js"></script>
  </body>
</html>

Наконец, создайте файл App.re и добавьте следующее:

/* App.re */
let str = ReasonReact.string;
let component = ReasonReact.statelessComponent("App");

let make = _children => {
  ...component,
  render: _self => 
    <div> 
      <h1> {"ReasonML + ReasonReact + GraphQL" |> str} </h1>
    </div>
};

Несколько кратких заметок об этом функциональном компоненте без сохранения состояния:

  • Компонент ReasonReact, будь то компонент без состояния или редуктор, должен иметь спецификацию и распространяться внутри функции make.
  • компонент должен реализовывать метод render
  • Я назначаю ReasonReact.string на str, чтобы облегчить сокращение. Это делает JSX немного менее шумным.

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

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

Инициализация Reason Apollo

Чтобы настроить Apollo, мы запустим:

npm install -S reason-apollo react-apollo apollo-client apollo-cache-inmemory apollo-link apollo-link-context apollo-link-error apollo-link-http graphql graphql-tag apollo-link-ws apollo-upload-client subscriptions-transport-ws

Это похоже на большую команду установки.

Это так, но в нашем коде ReasonML используется только первый пакет, reason-apollo. Однако причина-apollo - это библиотека привязок, которая зависит от этих других пакетов JavaScript.

Чтобы сделать написание запросов GraphQL более удобным, нам понадобится еще одна зависимость разработчика.

npm install -D graphql_ppx

После установки мы можем открыть наш файл bsconfig.json и обновить ключи «bs-dependencies» и «ppx-flags» следующим образом:

// bsconfig.json
{
  "bs-dependencies": [
    "reason-react",
    "reason-apollo"
  ],
  "ppx-flags": [
    "graphql_ppx/ppx"
  ],

  // other fields...
}

Массив «bs-dependencies» сообщает BuckleScript включить эти модули npm в процесс сборки. Массив «ppx-flags» позволяет нашей IDE знать, как предварительно обрабатывать определенные директивы.

В нашем случае это позволяет нам писать запросы GraphQL с проверкой полей.

В папке src создайте файл с именем Client.re. Здесь мы объявим наш экземпляр клиента Apollo.

/* Client.re */
let inMemoryCache = ApolloInMemoryCache.createInMemoryCache();

let httpLink =
  ApolloLinks.createHttpLink(~uri="https://video-game-api-pvibqsoxza.now.sh/graphql", ());

let instance =
  ReasonApollo.createApolloClient(~link=httpLink, ~cache=inMemoryCache, ());

Примечание. Если этот uri, https://video-game-api-pvibqsoxza.now.sh/graphql не работает, отправьте мне сообщение в Twitter или здесь, в комментариях, и я обновлю его так быстро, как возможный.

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

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

/* Index.re */
ReactDOMRe.renderToElementWithId(
  <ReasonApollo.Provider client=Client.instance>
    <App />
  </ReasonApollo.Provider>,
  "root",
);

Это немного похоже на стандартное приложение React JS, но с некоторыми оговорками. Обратите внимание на отсутствие операторов импорта. В ReasonML у нас есть доступ ко всем пространствам имен, встроенным в наше приложение. С точки зрения Index.re мы видим модули Client и App.

Когда мы создаем файл .re в нашей папке src, он становится модулем. Мы также можем явно объявить наши модули в наших файлах.

Пришло время использовать наш API.

Отправка запросов и отображение списка

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

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

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

type VideoGame {
  id: ID!
  title: String!
  developer: String!
  completed: Boolean!
}

type Query {
  videoGames: [VideoGame!]!
}

type Mutation {
  completeGame(id: ID!): VideoGame!
}

В настоящее время у нас есть один запрос и одна мутация, оба из которых работают с этим VideoGame типом. Адепт GraphQL заметит, что каждое возвращаемое значение не допускает значения NULL, то есть эти ответы не могут возвращать неустановленные поля или нулевые объекты.

Graphql_ppx дает нам сценарий с именем send-introspection-query,, который сообщает ReasonML, как понимать нашу схему, генерируя файл graphql_schema.json.

С его помощью запросы, которые мы пишем, являются типобезопасными и проверяются во время компиляции.

npm run send-introspection-query https://video-game-api-pvibqsoxza.now.sh/graphql

Давайте определим наш первый запрос поверх App.re, чуть ниже объявления component.

/* App.re */ 

module VideoGames = [%graphql
  {|
  query VideoGames {
    videoGames {
      id
      title
      developer
      completed
    }
  }
|}
];

module VideoGamesQuery = ReasonApollo.CreateQuery(VideoGames);

/* let make = ... */

По сравнению с JavaScript в react-apollo, этот код будет наиболее похож на:

const VideoGames = gql`
  query VideoGames {
    videoGames {
      id
      title
      developer
      completed
    }
  }
`

// later in render
render() {
  return (
    <Query query={VideoGames}> {/* ... */} </Query>
  )
}

Теперь давайте обновим функцию рендеринга:

/* App.re */
let make = _children => {
  ...component,
  render: _self => {
    let videoGamesQuery = VideoGames.make();
    <div>
      <h1> {"ReasonML + ReasonReact + GraphQL" |> str} </h1>
      <VideoGamesQuery variables=videoGamesQuery##variables>
        ...{
         ({result}) =>
           switch (result) {
           | Loading => <div> {"Loading video games!" |> str} </div>
           | Error(error) => <div> {error##message |> str} </div>
           | Data(data) => <VideoGameList items=data##videoGames />
           }
         }
      </VideoGamesQuery>
    </div>;
  }
};
  • мы используем самую интересную функцию ReasonML - сопоставление с образцом, чтобы обрабатывать каждый из вариантов, которым может быть ответ на запрос.
  • обратите внимание на оператор ##, который используется для доступа к полям в переменной videoGamesQuery и переменной data. Это не записи ReasonML, это JSON, и их нужно использовать немного иначе.

Попрощайтесь с ветвями операторов if-else. Сопоставление с образцом в сочетании с вариантами делает логику более линейной и понятной.

Это также сокращает проверку переходов до постоянного, а не линейного времени, что делает ее более эффективной.

Если код ReasonML когда-нибудь покажется более подробным, просто помните, что мы по-прежнему обеспечиваем идеальную безопасность типов при его компиляции. Нам все еще нужно создать компонент VideoGamesList, а также определить тип записи videoGame.

Начиная с типа записи, создайте новый файл с именем VideoGame.re и добавьте следующее:

/* VideoGame.re */

[@bs.deriving jsConverter]
type videoGame = {
  id: string,
  title: string,
  developer: string,
  completed: bool,
};

Тип videoGame в том виде, в котором он представлен здесь, имеет 4 поля, ни одно из которых не является необязательным. Указанная выше директива BuckleScript добавляет пару экспортируемых служебных методов, которые позволяют нам конвертировать между записями ReasonML и объектами JavaScript.

Совет: когда Apollo возвращает ответ, он возвращает нетипизированные объекты JavaScript. Директива jsConverter дает нам экспортированный метод с именем videoGameFromJs, который мы можем использовать для сопоставления данных запроса Apollo с полностью типизированным ReasonML.

Чтобы увидеть этот механизм в действии, создайте новый файл с именем VideoGameList.re и добавьте:

/* VideoGameList.re */
open VideoGame;

let str = ReasonReact.string;
let component = ReasonReact.statelessComponent("VideoGameList");

let make = (~items, _children) => {
  ...component,
  render: _self =>
    <ul style={ReactDOMRe.Style.make(~listStyleType="none", ())}>
      {
        items
        |> Array.map(videoGameFromJs)
        |> Array.map(item =>
             <li key={item.id}>
             	<input
                  id={item.id}
                  type_="checkbox"
                  checked={item.completed}
                />
                <label htmlFor={item.id}>
                  {item.title ++ " | " ++ item.developer |> str}
                </label>
             </li>
           )
        |> ReasonReact.array
      }
    </ul>,
};
  1. Откройте модуль VideoGame (VideoGame.re) вверху, чтобы мы могли использовать весь его экспорт в модуле VideoGameList.
  2. Объявите тип компонента и сокращенную строку.
  3. Определите функцию make, которая ожидает одну опору, items.
  4. Внутри функции рендеринга направьте элементы для преобразования объектов JS в записи ReasonML, сопоставьте записи с JSX и, наконец, выведите их в виде массива.

В этом блоке кода есть несколько новых особенностей ReasonML, которых мы еще не видели.

  • мы записываем свойства компонента как помеченные аргументы в функцию make. Считается, что такой аргумент, как ~items, помечен, если он имеет ~. Аргументы без метки, например children, должны находиться в конце списка аргументов.
  • мы определяем собственный стиль для тега ul, используя ReactDOMRe.Style. Эта функция make принимает помеченные аргументы, за которыми следует оператор unit, который представляет собой просто пустой набор скобок.
  • атрибут «тип» во входном теге записывается как type_, потому что «тип» уже является ключевым словом в ReasonML.
  • конкатенация строк должна выполняться с использованием оператора ++

Примечание. Конвейер меняет порядок вызовов функций, чтобы потенциально улучшить читаемость. С оператором |> объект items применяется к каждой функции в качестве последнего аргумента.

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

items 
  |> Array.map(videoGameFromJs)
  |> Array.map(renderItem)
  |> ReasonReact.array;

ReasonReact.array(
  Array.map(
    renderItem,
    Array.map(
      videoGameFromJs,
      items
    )
  )
);

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

yarn send-introspection-query https://video-game-api-pvibqsoxza.now.sh/graphql

Это генерирует graphql_schema.json файл, который Reason Apollo использует для проверки ваших запросов. Если ваше приложение ReasonML запрашивает поле, которого нет в схеме, или если оно неправильно обрабатывает необязательные типы данных, оно не будет компилироваться.

Строгая типизация служит прекрасной проверкой работоспособности при написании запросов и изменений.

Когда все будет сказано и сделано, вы должны увидеть следующее.

Мутация данных

Одна вещь, которую вы могли заметить, это то, что установка флажков ничего не дает. Это ожидаемо, поскольку мы еще не подключили мутацию.

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

Внутри VideoGameList.re добавьте эти модули в начало файла сразу под вызовом для создания компонента.

/* VideoGameList.re */
module CompleteGame = [%graphql
  {|
  mutation CompleteGame($id: ID!) {
    completeGame(id: $id) {
      id
      completed
    }
  }
|}
];

module CompleteGameMutation = ReasonApollo.CreateMutation(CompleteGame);

Что касается рендеринга мутации, он будет очень похож на версию JavaScript. Я помещу этот код сюда, а затем пройдусь по нему, начиная с тега <li>.

/* VideoGameList.re */

<li key={item.id}>
  <CompleteGameMutation>
    ...{
        (mutate, {result}) => {
          let loading = result == Loading;
          <div>
            <input
              id={item.id}
              type_="checkbox"
              checked={item.completed}
              onChange={
                _event => {
                  let completeGame =
                    CompleteGame.make(~id=item.id, ());
                  mutate(~variables=completeGame##variables, ())
                  |> ignore;
                }
              }
            />
            <label
              htmlFor={item.id}
              style={
                ReactDOMRe.Style.make(
                  ~color=loading ? "orange" : "default",
                  (),
                )
              }>
              {item.title ++ " | " ++ item.developer |> str}
            </label>
          </div>;
        }
      }
  </CompleteGameMutation>
</li>

Подобно компоненту Apollo VideoGamesQuery, который мы использовали ранее, компонент CompleteGameMutation, который мы видим здесь, передает своим потомкам функцию изменения, а также объект результатов.

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

Я не специалист по UX, но думаю, что на сегодня хватит.

Заключение

ReasonML - довольно мощный и выразительный язык. Если вы новичок в ReasonML и вам не терпится создать безопасные пользовательские интерфейсы, вот несколько ресурсов, из которых можно поучиться:

  1. В этом выступлении Шона Гроува с ReasonConf объясняется, почему GraphQL является лучшим выбором для обработки внешних данных. Многие проблемы с синтаксическим анализом JSON в типизированных языках можно полностью избежать с помощью GraphQL.
  2. Многие сторонние инструменты, которые мы используем в JavaScript, идут прямо из коробки с ReasonML. Эта статья Дэвида Копала объясняет, как, а также некоторые другие причины, по которым написание ReasonML - это так здорово.
  3. Блог Джареда Форсайта содержит отличные материалы о ReasonML и OCaml. Он один из самых активных участников сообщества.
  4. Я получаю большую часть своего обучения через Документы ReasonML и Документы BuckleScript. За ними легко следить, и они содержат важные сведения о вариантах дизайна при реализации языковых функций.

Если вы хотите быстро настроить свой собственный сервер GraphQL, ознакомьтесь с другой моей статьей Научитесь создавать сервер GraphQL с минимальными усилиями.

Я надеюсь написать больше статей о ReasonML и GraphQL в будущем. Если это вас заинтересует, то обязательно подпишитесь на меня в Medium и Twitter!