ReasonML, также известный как OCaml с добавлением JavaScript, предлагает почти непревзойденную безопасность типов для разработки пользовательских интерфейсов. Приняв систему статического типа, вы можете устранить целый класс ошибок до обслуживания вашего приложения.
Обновление 4/11/19: ReasonReact 0.7.0 представил некоторые обновления базового API (включая хуки! 😍), поэтому некоторые из приведенных ниже фрагментов кода немного устарели. Я скоро обновлю это, чтобы эта публикация оставалась актуальной.
Мы собираемся изучить создание небольшого веб-приложения, которое использует конечную точку GraphQL с помощью ReasonML. Если хотите посмотреть готовый пример, вот репо.
Мы рассмотрим:
- Начало работы с проектом ReasonReact
- Настройка клиента с reason-apollo
- Отправка запросов
- Мутация данных
- И некоторые причуды и синтаксические особенности 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 вкладки терминала:
npm start
для компиляции ReasonML в JavaScriptnpm run webpack
для объединения JavaScriptserve 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>,
};
- Откройте модуль
VideoGame
(VideoGame.re) вверху, чтобы мы могли использовать весь его экспорт в модулеVideoGameList
. - Объявите тип компонента и сокращенную строку.
- Определите функцию make, которая ожидает одну опору,
items
. - Внутри функции рендеринга направьте элементы для преобразования объектов 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 и вам не терпится создать безопасные пользовательские интерфейсы, вот несколько ресурсов, из которых можно поучиться:
- В этом выступлении Шона Гроува с ReasonConf объясняется, почему GraphQL является лучшим выбором для обработки внешних данных. Многие проблемы с синтаксическим анализом JSON в типизированных языках можно полностью избежать с помощью GraphQL.
- Многие сторонние инструменты, которые мы используем в JavaScript, идут прямо из коробки с ReasonML. Эта статья Дэвида Копала объясняет, как, а также некоторые другие причины, по которым написание ReasonML - это так здорово.
- Блог Джареда Форсайта содержит отличные материалы о ReasonML и OCaml. Он один из самых активных участников сообщества.
- Я получаю большую часть своего обучения через Документы ReasonML и Документы BuckleScript. За ними легко следить, и они содержат важные сведения о вариантах дизайна при реализации языковых функций.
Если вы хотите быстро настроить свой собственный сервер GraphQL, ознакомьтесь с другой моей статьей Научитесь создавать сервер GraphQL с минимальными усилиями.
Я надеюсь написать больше статей о ReasonML и GraphQL в будущем. Если это вас заинтересует, то обязательно подпишитесь на меня в Medium и Twitter!