在ReScript x Next.js环境中引入GraphQL库”Relay”

在基于Next.js,ReScript和tailwind的环境中,尝试使用GraphQL进行开发。

图灵完备查询语言

Facebook开发的查询语言。
用户可以在客户端指定响应格式。

当在选择使用React作为GraphQL客户端时,两个主要的选择似乎是Apollo Client和Relay。
GraphQL的官方网站上还列出了服务器端和其他客户端的选择。

阿波罗客户端

Apollo Client
阿波罗客户端
Rescript Apollo Client
Rescript 阿波罗客户端

转发

转播
重新修改的中继

实施

按照RescriptRelay的说明进行基本操作。

增加接力

为了利用Suspense,将React库设为实验版本。

yarn add react@0.0.0-experimental-4e08fb10c react-dom@0.0.0-experimental-4e08fb10c
yarn add relay-runtime@11.0.0 relay-compiler@11.0.0 react-relay@11.0.0 relay-config@11.0.0
yarn add --dev rescript-relay graphql reason-promise bs-fetch

中继设定

配置

module.exports = {
    src: "./src",
    schema: "./schema.graphql",
    artifactDirectory: "./src/__generated__",
    customScalars: {
        Datetime: "string",
        Color: "Color.t",
    },
};

为了将Next.js的res文件统一放置到一个文件夹中,需要借助RescriptRelay配置文件中src目录的指定方法影响将其移到src文件夹中。

mv pages components bindings src/ 
  "sources": [
-    {
-      "dir": "components",
-      "subdirs": true
-    },
-    {
-      "dir": "pages",
-      "subdirs": true
-    },
-    {
-      "dir": "bindings",
-      "subdirs": true
-    }
+    {
+      "dir": "src",
+      "subdirs": true
+    }
  ],
+  "ppx-flags": [
+    "rescript-relay/ppx"
+  ],
  "bs-dependencies": [
-    "@rescript/react"
+    "@rescript/react",
+    "rescript-relay",
+    "reason-promise",
+    "bs-fetch"
  ],

如果使用了Tailwind,也需要更改目录。

module.exports = {
    purge: [
-        './pages/**/*.res',
-        './components/**/*.res',
+        './src/pages/**/*.res',
+        './src/components/**/*.res',
    ],
    darkMode: false, // or 'media' or 'class'
    theme: {
        extend: {},
    },
    variants: {
        extend: {},
    },
    plugins: [],
}
  "scripts": {
    "dev": "concurrently \"bsb -clean-world -make-world -w\" \"next dev\"",
    "dev:reason": "bsb -clean-world -make-world -w",
    "dev:next": "next dev",
    "build": "bsb -clean-world -make-world && next build",
-    "start": "next start"
+    "start": "next start",
+    "relay": "rescript-relay-compiler",
+    "relay:watch": "rescript-relay-compiler --watch"
  },
const withTM = require('next-transpile-modules')(['bs-platform'])

module.exports = withTM({
-    pageExtensions: ['jsx', 'js', 'bs.js']
+    pageExtensions: ['jsx', 'js', 'bs.js'],
+    experimental: {
+        reactMode: 'concurrent'
+    }
})

开启实验性设置。
如果忘记了,会出现Cannot hydrate Suspense in legacy mode错误,所以要注意。

es6改为commonjs

由于bs-fetch的关系,将bsconfig.json中的package-specs从es6更改为commonjs。

参考にしながら、ReasonMLを使用してNext.jsでページ遷移し、Web APIを呼び出し、レスポンスを表示します。bs-fetchとbs-jsonを追加します。

  "package-specs": {
-    "module": "es6",
+    "module": "commonjs",
    "in-source": true
  },

写配置组件

/* This is just a custom exception to indicate that something went wrong. */
exception Graphql_error(string)

/**
 * A standard fetch that sends our operation and variables to the
 * GraphQL server, and then decodes and returns the response.
 */
let fetchQuery: RescriptRelay.Network.fetchFunctionPromise = (
  operation,
  variables,
  _cacheConfig,
  _uploadables,
) => {
    open Fetch
    fetchWithInit(
        Env.graphQlUrl,
        RequestInit.make(
            ~method_=Post,
            ~body=Js.Dict.fromList(list{
                ("query", Js.Json.string(operation.text)),
                ("variables", variables),
            })
            ->Js.Json.object_
            ->Js.Json.stringify
            ->BodyInit.make,
            ~headers=HeadersInit.make({
                "content-type": "application/json",
                "accept": "application/json",
//                "authorization": "Bearer " ++ Env.graphQlToken,  // 今回は認可(authorization)を要しないのでこの行は不要。graphCMS等用いる場合記述する。
            }),
            (),
        ),
    ) |> Js.Promise.then_(resp =>
        if Response.ok(resp) {
            Response.json(resp)
        } else {
            Js.Promise.reject(Graphql_error("Request failed: " ++ Response.statusText(resp)))
        }
    )
}

let network = RescriptRelay.Network.makePromiseBased(~fetchFunction=fetchQuery, ())

let environment = RescriptRelay.Environment.make(
        ~network,
        ~store=RescriptRelay.Store.make(
        ~source=RescriptRelay.RecordSource.make(),
        ~gcReleaseBufferSize=10, /* This sets the query cache size to 10 */
        (),
    ),
    (),
)
// api
@val external graphQlUrl:string = "process.env.NEXT_PUBLIC_GRAPHQL_URL"
// @val external graphQlToken:string = "process.env.NEXT_PUBLIC_GRAPHQL_TOKEN"  // authorizationが必要な場合

在使用Next.js时,如果要在浏览器中使用环境变量,则需要在开头加上NEXT_PUBLIC_前缀。

NEXT_PUBLIC_GRAPHQL_URL="<YOUR_GRAPHQL_API_URL>"
# NEXT_PUBLIC_GRAPHQL_TOKEN="<YOUR_GRAPHQL_API_TOKEN>"  # authorizationが必要な場合

这次我们将使用SpaceX API。
将以下内容改写为:
这次我们将使用SpaceX的应用程序接口。

NEXT_PUBLIC_GRAPHQL_URL=https://api.spacex.land/graphql/

下载架构

从API下载模式。

yarn add --dev get-graphql-schema
yarn get-graphql-schema https://api.spacex.land/graphql/ > schema.graphql

供应商

@react.component
let make = (~children) => <RescriptRelay.Context.Provider environment=RelayEnv.environment>
    <div>
        children
    </div>
</RescriptRelay.Context.Provider>

let default = make
import '../../styles/main.css'
import App from '../components/App.bs'

const MyApp = ({ Component, pageProps }) => (<App>
        <Component {...pageProps} />
</App>);

export default MyApp;

GraphQL 打击组件

module MyQuery = %relay(
    `query FetchMissionsQuery {
        missions {
             name
             id
             description
        }
    }`
)

@react.component
let make = () => {
    let response = MyQuery.use(~variables=(), ())

    let missions = switch (response.missions) {
       | Some(data) => data
       | _ => []
    }

    <div>
        <div><p className="font-black">{React.string("missions")}</p></div>
        {
            missions
            ->Belt.Array.map(
                mission => switch(mission) {
                    | Some(data) =>  <div key={Belt.Option.getWithDefault(data.id, "XXXXX")}>
                            <div><p className="font-medium">{React.string(Belt.Option.getWithDefault(data.name, ""))}</p></div>
                            <div>{React.string(Belt.Option.getWithDefault(data.description, ""))}</div>
                            <hr/>
                        </div>
                    | _ => <></>
                }
            )->React.array
        }
    </div>
}

let default = make
GraphQL Page

关于响应类型,会在通过下载的schema.graphql创建的.res文件中,使用realy:watch命令进行记录。

/* @generated */
%%raw("/* @generated */")
module Types = {
  @@ocaml.warning("-30")

  type rec response_missions = {
    name: option<string>,
    id: option<string>,
    description: option<string>,
  }
  type response = {
    missions: option<array<option<response_missions>>>,
  }
  type rawResponse = response
  type variables = unit
}

为什么option是id呢?如果使用iteration,每个组件都需要有一个独特的key,但是如果没有id的话应该会被报错…
因为有option存在,去除它会很麻烦,所以希望能有其他的解决办法。

@react.component
let make = () => {
    <div>
        <Header />
        <div className="py-6 md:py-12">
            <div className="container px-4 mx-auto">
                <React.Suspense fallback={<div>{React.string(`Loading...`)}</div>}>
                    <FetchMissions />
                </React.Suspense>
            </div>
        </div>
    </div>
}

let default = make

使用Relay的组件外部使用React.Suspense包裹,并在Suspense标签内指定在出现fallback的情况下(即尚未加载或加载失败时)要显示的组件。
只在外部进行包裹即可,例如在src/components/App.res的

的外部使用Suspense包裹也可以。但是整体会被视为正在加载中的状态,所以基本上不会这样做。

执行dev

yarn relay:watch
yarn dev:rescript

将`yarn dev:reason`命名为其他名称。

yarn dev:next

访问http://localhost:3000/missions。

アクセス結果

印象

对于查询名称的指定有一些特殊要求。
源目录会受限制。
可以使用GraphQL和fetch进行调用。
当存在多个终点时该怎么办?(可能不太常见的情况?)

做了但不成功的事情

重写-阿波罗客户端

一) 背景

二)根据

三) 由于

依赖项是 reason-react。尽管尝试了强制修改并重新构建,但并未成功。

发生了无效的hook错误,无法修复。

交换

提供返回《星球大战》角色名称等相关API集合。最初我打算使用这个。

swapi-qlaphql(GitHub)
swapi-graphql 测试页面

我在使用get-graphql-schema时收到了无效的JSON。为什么会这样?

实际应用

我们正在使用graphCMS以相同的结构。

请查阅参考文献。
或者
请踏入参考文献。

GraphQL
GraphQL代码库、工具和服务
Apollo
Apollo客户端
Rescript Apollo客户端
Relay
RescriptRelay
开始使用RescriptRelay | RescriptRelay
使用悬挂功能获取数据(实验性功能)
使用ReasonML在Next.js中进行页面转换并显示Web API的响应 – 添加bs-fetch、bs-json
swapi-qlaphql(GitHub)
swapi-graphql测试页面
SpaceX API
Next.js – 基本功能:环境变量 – 将环境变量暴露给浏览器
graphCMS

广告
将在 10 秒后关闭
bannerAds