使用 Hasura 在 ReasonML 中使用 GraphQL 和无服务器构建博客 CMS
ReasonML 简介
ReasonReact
我们将要创造什么
使用 Hasura 添加 GraphQL 后端
将 GraphQL 添加到我们的应用
添加查询和突变
添加订阅
总结和下一步
这是本系列博文的第一篇,我们将使用 Hasura 的 GraphQL API 创建博客内容管理系统 (CMS),并使用无服务器函数进行逻辑处理,并在客户端使用 ReasonML 语法编写现代且健壮的代码。让我们开始吧。
ReasonML 简介
首先,在开始实际编写代码之前,我们先来讨论一下为什么选择 ReasonML。虽然这可以单独写一篇博文来探讨,但我还是会尝试简单介绍一下。ReasonML 为我们提供了一个基于 Ocaml 的出色类型系统,但就语法而言,它与 JavaScript 非常接近。ReasonML 是由React 的开发者Jordan Walke发明的,并被 Facebook Messenger 用于生产环境。最近,许多公司也采用了 Reason 并将其用于生产环境,因为它有一个非常酷的范式:“只要能编译,就能正常工作”。
这句话听起来很大胆,但事实上,Reason 本质上是 OCaml 语言的一种新语法,它使用了Hindley Milner类型系统,因此可以在编译时推断类型。
这对我们开发人员来说意味着什么?
这意味着通常我们不会编写那么多类型,如果有的话,就像我们在 TypeScript 中编写的那样,并且可以信任编译器来推断这些类型。
说到编译,Reason 可以编译为 OCaml,而 OCaml 又可以编译为各种目标平台,例如二进制、iOS、Android 等。此外,我们还可以借助 Bucklescript 编译器将其编译为人类可读的 JavaScript。事实上,这正是我们将在博客文章中实现的。
那么 npm 以及我们在 JavaScript 领域所习惯的所有这些包怎么样?
事实上,BuckleScript 编译器为我们提供了强大的外部函数接口(FFI),让你可以在 Reason 代码中使用 JavaScript 包、全局变量,甚至原始 JavaScript。你唯一需要做的就是准确地输入它们,即可从类型系统中获益。
顺便说一句,如果你想了解更多关于 ReasonML 的信息,我在 Youtube 上直播了 10 小时的编码训练营,你可以在我的频道上观看
ReasonReact
在使用 Reason 进行前端开发时,我们将使用ReasonReact。社区也有一些 VueJs 的绑定,但在 Web 开发方面,我们主要会选择 ReasonReact。如果您之前听说过 Reason 和 ReasonReact,那么 ReasonReact 最近进行了一次重大更新,使其编写起来更加简单。现在创建 Reason 组件的语法不仅非常流畅,而且看起来也比 JavaScript 更好,这在过去是无法实现的。此外,随着 hooks 的引入,创建 ReasonReact 组件和管理状态变得更加容易。
入门
ReasonReact 官方文档建议使用bsb init
命令来创建新项目,但说实话,你可能想知道如何从 JavaScript 和 Typescript 迁移到其他语言。因此,在本例中,我们将首先使用 create-react-app 创建项目。
我们将首先运行以下命令:
npx create-react-app reason-hasura-demo
它将使用 JavaScript 创建我们的基本 React 应用程序,现在我们将其更改为 ReasonReact。
安装
如果这是您第一次在您的环境中设置 ReasonML,它将像安装 bs-platform 一样简单。
yarn global add bs-platform
另外,通过安装适当的编辑器插件来配置你的 IDE
我使用reason-vscode扩展来实现这一点。我还强烈建议使用"editor.formatOnSave": true,
vscode 设置,因为 Reason 有一个叫做 Prettier for Reason 的工具,refmt
它基本上是内置在 Prettier 中的,所以你的代码在保存时会被正确格式化。
将 ReasonML 添加到你的项目中
现在是时候添加 ReasonML 了。我们将安装bs-platform
依赖reason-react
项。
yarn add bs-platform --dev --exact
yarn add reason-react --exact
然后进入配置。为此,创建bsconfig.json
具有以下配置的文件:
{
"name": "hasura-reason-demo-app",
"reason": { "react-jsx": 3 },
"bsc-flags": ["-bs-super-errors"],
"sources": [
{
"dir": "src",
"subdirs": true
}
],
"package-specs": [
{
"module": "es6",
"in-source": true
}
],
"suffix": ".js",
"namespace": true,
"bs-dependencies": [
"reason-react"
],
"ppx-flags": [],
"refmt": 3
}
我们还将编译和监视脚本添加到我们的 package.json 中
"re:build": "bsb -make-world -clean-world",
"re:watch": "bsb -make-world -clean-world -w",
如果您运行这些脚本,基本上会发生的情况是,.re
项目中的所有文件都将与您的.re
文件一起编译为 javascript。
开始配置我们的根端点
让我们编写第一个原因文件,通过将 index.js 从
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
到
%raw | |
"import './index.css'"; | |
[@bs.module "./serviceWorker"] | |
external register_service_worker: unit => unit = "register"; | |
[@bs.module "./serviceWorker"] | |
external unregister_service_worker: unit => unit = "unregister"; | |
ReactDOMRe.renderToElementWithId( | |
<App /> | |
"root", | |
) | |
unregister_service_worker(); |
基本上我在这里做的是将我的 App 组件渲染到 dom 中
并且
我从文件中导入注册和取消注册方法,serviceWorker.js
以便可以在 Reason 中使用 Javascript。
要运行我们的项目,我们需要运行
npm run re:watch
因此我们的 Bucklescript 将首次构建文件,并在添加新文件时监视更改。
在不同的选项卡中,我们运行npm start
并查看我们的 React 应用程序。
基本造型
ReasonML 的样式可以是类型化的(因为bs-css
它基于)emotion
或非类型化的。为了简单起见,我们将使用非类型化的。让我们从“create-react-app”中删除 index.css 和 App.css,创建styles.css
文件并导入两个包:
yarn add animate.css
yarn add tailwind --dev
现在在我们的styles.css
文件中,我们将导入 tailwind
@tailwind base;
@tailwind components;
@tailwind utilities;
并添加样式构建脚本package.json
"rebuild-styles": "npx tailwind build ./src/styles.css -o ./src/index.css",
编写我们的第一个组件。
我们将 App.css 文件重命名为 App.re,删除其所有内容,并编写简单的 ReasonReact 组件。
不错吧?使用 ReasonML,我们不需要导入或导出包,实际上,每个文件都是一个模块,所以如果我们的文件名是 App.re,我们可以简单地在不同的文件中使用组件。
字符串到元素
在 ReasonReact 中,如果要在组件中添加文本,可以使用ReasonReact.string
此外,我更喜欢以下语法:
你会在这个项目中经常看到它。这种语法是反向应用运算符或管道运算符,它使你能够链接函数,所以f(x)
基本上写成x |> f
。
现在你可能会说,但等一下,这在 ReasonReact 中会很繁琐。每个字符串都需要用 ReasonReact.string 包装。有很多方法可以做到这一点。
一种常见的方法是在某处创建utils.re
类似这样的文件
let ste = ReasonReact.string
它将我们的代码缩短为
通过该项目,我使用ReasonReact.string
管道,以便代码更加具有自我描述性。
我们将要创造什么
现在我们有了 ReasonReact 应用程序,是时候看看我们将在本节中创建什么了:
这个应用程序将是一个简单的博客,它将使用由Hasura自动生成的 GraphQL API,将使用订阅和 ReasonReact。
将应用程序与组件分离
我们将应用程序分成诸如Header
、、PostsList
和Post
AddPostsForm
等组件Modal
。
标题
标题将用于顶部导航栏以及在右上角呈现“添加新帖子”按钮,当单击它时,它将打开一个带有我们的模态窗口AddPostsForm
。Header
将获取openModal
和isModalOpened
道具,并且只是一个演示组件。
我们还将使用 javascriptrequire
在标题中嵌入 SVG 徽标。
当使用 ReasonReact 包装器为 React 合成事件单击时,标题按钮将停止传播ReactEvent.Synthetic
,并将调用openModal
作为标记参数传递的 prop(所有 props 都作为 ReasonReact 中的标记参数传递)。
莫代尔
Modal
组件也将是一个简单且具有展示性的组件
对于我们文件中的模态功能App.re
,我们将使用useReducer
Reason 包装的 React hook,如下所示:
请注意,我们useReducer
使用模式匹配来对action
variant进行模式匹配。例如,如果我们忘记了Close
action,项目将无法编译,并在编辑器中给出错误。
帖子列表,帖子
PostsList 和 Post 都只是带有虚拟数据的展示组件。
添加帖子表单
这里我们将使用 React setState
hook 来控制表单。这也相当简单:
onChange
事件在 Reason 中看起来会有些不同,但这主要是因为它的类型安全特性:
<input onChange={e => e->ReactEvent.Form.target##value |> setCoverImage
}/>
使用 Hasura 添加 GraphQL 后端
现在是时候为我们的 ReasonReact 应用设置 GraphQL 后端了。我们将使用Hasura来实现。
简而言之,Hasura 在新的或现有的 Postgres 数据库之上自动生成 GraphQL API。您可以在以下博客文章中阅读有关 Hasura 的更多信息,或在 YouTube [频道]( https://www.youtube.com/c/hasurahq ) 上关注 Hasura 。
我们将前往hasura.io并单击 Docker 镜像,转到文档部分,了解如何在 docker 上设置 Hasura。
我们还将安装 Hasura cli并运行hasura init
创建一个文件夹,其中包含我们在控制台中执行的所有操作的迁移。
一旦 Hasura 控制台运行,我们就可以设置我们的帖子表:
和用户表:
我们需要通过返回帖子表 -> 修改并设置用户表的外键来连接我们的帖子和用户:
我们还需要设置帖子和用户之间的关系,以便用户对象出现在自动生成的 GraphQL API 中。
现在让我们前往控制台并创建第一个虚拟用户:
mutation {
insert_users(objects: {id: "first-user-with-dummy-id", name: "Test user"}) {
affected_rows
}
}
现在让我们尝试插入一个新帖子:
mutation {
insert_posts(objects: {user_id: "first-user-with-dummy-id", title: "New Post", content: "Lorem ipsum - test post", cover_img: "https://images.unsplash.com/photo-1555397430-57791c75748a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"}) {
affected_rows
}
}
如果我们现在查询我们的帖子,将会获得我们客户端所需的所有数据:
query getPosts{
posts {
title
cover_img
content
created_at
user {
name
avatar_url
}
}
}
将 GraphQL 添加到我们的应用
让我们安装一堆依赖项来将 GraphQL 添加到我们的 ReasonReact 应用程序中并开始实时获取博客文章。
yarn add @glennsl/bs-json apollo-boost apollo-link-ws graphql react-apollo reason-apollo subscriptions-transport-ws
当我们使用 Reason 时,我们希望对端点运行一个自省查询,这样我们就能以 JSON 格式获取 graphql 模式自省数据。稍后,它将用于在编辑器中完成 graphql 查询的补全和类型检查,这非常酷,也是我们迄今为止最好的体验。
yarn send-introspection-query http://localhost:8080/v1/graphql
我们还需要补充bs-dependencies
我们的bsconfig.json
"bs-dependencies": [
"reason-react",
"reason-apollo",
"@glennsl/bs-json"
],
"ppx-flags": ["graphql_ppx/ppx"]
我们graphql_ppx
在这里添加了 ppx 标志 - 这将允许我们稍后在 ReasonML 中编写 GraphQL 语法。
现在让我们创建一个新ApolloClient.re
文件并设置我们的基本 ApolloClient
添加查询和突变
查询
让我们转到我们的PostsList.re
组件并添加我们之前在 Hasura graphiql 中运行的相同查询:
现在我们可以使用GetPostsQuery
带有 render 属性的组件来加载我们的帖子了。但在此之前,我想接收我输入的 GraphQL API 结果,所以我需要将其转换为Records。
PostTypes.re
就像在文件中添加类型一样简单
open PostTypes
组件的最终版本PostsList
如下所示:
突变
要向我们的添加变异AddPostForm
,我们以与查询相同的方式开始:
更改将在渲染道具中执行。我们将使用以下函数来创建变量对象:
let addNewPostMutation = PostMutation.make(~title, ~content, ~sanitize, ~coverImg, ());
要执行变异本身,我们只需运行
mutation(
~variables=addNewPostMutation##variables,
~refetchQueries=[|"getPosts"|],
(),
) |> ignore;
最终代码如下:
添加订阅
要添加订阅,我们需要对 进行更改ApolloClient.re
。记住,我们不需要在 Reason 中导入任何内容,所以我们只需开始编写即可。
让我们添加webSocketLink
并创建一个链接函数,用于ApolloLinks.split
在需要使用订阅或查询和突变时定位到 WebSocket httpLink
。最终的 ApolloClient 版本如下所示:
现在要从查询更改为订阅,我们需要在 graphql 语法中将单词更改为,并使用query
而不是subscription
ReasonApollo.CreateSubscription
ReasonApollo.CreateQuery
总结和下一步
在这篇博文中,我们使用 Hasura 创建了一个实时客户端和后端,但还没有讨论无服务器。我们将在下一篇博文中探讨无服务器业务逻辑。同时,享受阅读并开始使用 ReasonML。
您可以在此处查看代码:
https://github.com/vnovick/reason-demo-apps/tree/master/reason-hasura-demo并在 Twitter 上关注我@VladimirNovick以获取更新。