使用React+Amplify+AppSync+TypeScript构建实时留言板应用程序

amplify_react_ts.png

我受[“【爆速】React+Amplify+AppSyncでリアルタイム掲示板アプリを15分で作り上げる 〜これが最高のDeveloper Experienceだ〜 – Qiita”]這篇文章的啟發去創作。

由于在Amplify命令中可以选择TypeScript来自动生成代码,我试着看看它是什么样的。
顺便一提,我也在使用React的Hooks。

版本

我使用的环境如下:

$ create-react-app --version
3.0.1
$ node -v
v8.15.1
$ npm -v
6.9.0
$ amplify --version
1.7.0

由于我的环境中甚至没有amplify命令,因此我查看了官方页面并进行了安装。

开始 · 创建React App

创建React应用程序的模板

使用 create-react-app 命令添加参数 –typescript 创建应用,并使用 amplify init 命令进行初始化配置。
请根据需要自行调整配置文件中的 profile 等设置。

$ create-react-app boardapp --typescript
$ cd boardapp
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project boardapp
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use default

添加GraphQL的API。

$ amplify add api
? Please select from one of the below mentioned services GraphQL
? Provide API name: boardapp
? Choose an authorization type for the API API key
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? No
? Provide a custom type name Post

由於可以使用以下架構的示例,因此直接使用。

type Post @model {
    id: ID!
    title: String!
    content: String!
    price: Int
    rating: Float
}

接下来,我们将进行部署并自动生成客户端代码。

$ amplify push
? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target typescript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.ts
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/API.ts

当抵达这里时,GraphQL的API已经部署到了AWS上,本地目录结构如下所示。

$ tree -L 5 -I "node_modules"
.
├── README.md
├── amplify
│   ├── #current-cloud-backend
│   │   ├── amplify-meta.json
│   │   ├── api
│   │   │   └── boardapp
│   │   │       ├── build
│   │   │       ├── parameters.json
│   │   │       ├── resolvers
│   │   │       ├── schema.graphql
│   │   │       └── stacks
│   │   └── backend-config.json
│   ├── backend
│   │   ├── amplify-meta.json
│   │   ├── api
│   │   │   └── boardapp
│   │   │       ├── build
│   │   │       ├── parameters.json
│   │   │       ├── resolvers
│   │   │       ├── schema.graphql
│   │   │       └── stacks
│   │   ├── awscloudformation
│   │   │   └── nested-cloudformation-stack.yml
│   │   └── backend-config.json
│   └── team-provider-info.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
├── src
│   ├── API.ts
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── aws-exports.js
│   ├── graphql
│   │   ├── mutations.ts
│   │   ├── queries.ts
│   │   ├── schema.json
│   │   └── subscriptions.ts
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   └── serviceWorker.ts
├── tsconfig.json
└── yarn.lock

添加amplify的软件包

使用yarn将软件包注册。
似乎还会一同注册TypeScript的类型。

$ yarn add aws-amplify aws-amplify-react

应用程序的升级

首先,我们将修改由create-react-app自动生成的代码。接下来是Amplify的初始化部分。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import Amplify from "aws-amplify"  // 追加
import config from "./aws-exports" // 追加
Amplify.configure(config)          // 追加

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();

接下来是应用程序本身。关键点稍后将会解释。
另外,由于主要目的是理解流程,所以没有进行错误处理。

import React, { useEffect, useState } from "react";
import { API, graphqlOperation } from "aws-amplify";
import { listPosts } from "./graphql/queries";
import { createPost } from "./graphql/mutations";
import { onCreatePost } from "./graphql/subscriptions";
import {
  ListPostsQuery,
  OnCreatePostSubscription,
  CreatePostMutationVariables
} from "./API";

type Post = {
  id: string;
  title: string;
  content: string;
  price: number | null;
  rating: number | null;
};

type FormState = {
  title: string;
  content: string;
};

type PostSubscriptionEvent = { value: { data: OnCreatePostSubscription } };

const usePosts = () => {
  const [posts, setPosts] = useState<Post[]>([]);

  useEffect(() => {
    (async () => {
      // 最初のPost一覧取得
      const result = await API.graphql(graphqlOperation(listPosts));
      if ("data" in result && result.data) {
        const posts = result.data as ListPostsQuery;
        if (posts.listPosts) {
          setPosts(posts.listPosts.items as Post[]);
        }
      }

      // Post追加イベントの購読
      const client = API.graphql(graphqlOperation(onCreatePost));
      if ("subscribe" in client) {
        client.subscribe({
          next: ({ value: { data } }: PostSubscriptionEvent) => {
            if (data.onCreatePost) {
              const post: Post = data.onCreatePost;
              setPosts(prev => [...prev, post]);
            }
          }
        });
      }
    })();
  }, []);

  return posts;
};

const App: React.FC = () => {
  const [input, setInput] = useState<FormState>({
    title: "",
    content: ""
  });
  const posts = usePosts();

  const onFormChange = ({
    target: { name, value }
  }: React.ChangeEvent<HTMLInputElement>) => {
    setInput(prev => ({ ...prev, [name]: value }));
  };

  const onPost = () => {
    if (input.title === "" || input.content === "") return;
    const newPost: CreatePostMutationVariables = {
      input: {
        title: input.title,
        content: input.content
      }
    };
    setInput({ title: "", content: "" });
    API.graphql(graphqlOperation(createPost, newPost));
  };

  return (
    <div className="App">
      <div>
        タイトル
        <input value={input.title} name="title" onChange={onFormChange} />
      </div>
      <div>
        内容
        <input value={input.content} name="content" onChange={onFormChange} />
      </div>
      <button onClick={onPost}>追加</button>
      <div>
        {posts.map(data => {
          return (
            <div key={data.id}>
              <h4>{data.title}</h4>
              <p>{data.content}</p>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default App;

剩下的就只是启动了。

$ yarn start
board.png

当打开多个画面时,它们将同时实时更新。

解释和感想

模特儿

由于在src/API.ts中自动生成了与graphql模式匹配的类型,因此基本上使用在这里定义的类型。

export type ListPostsQuery = {
  listPosts:  {
    __typename: "ModelPostConnection",
    items:  Array< {
      __typename: "Post",
      id: string,
      title: string,
      content: string,
      price: number | null,
      rating: number | null,
    } | null > | null,
    nextToken: string | null,
  } | null,
};

export type OnUpdatePostSubscription = {
  onUpdatePost:  {
    __typename: "Post",
    id: string,
    title: string,
    content: string,
    price: number | null,
    rating: number | null,
  } | null,
};

由于Post中没有仅包含内容的类型,因此我们自行定义如下。

type Post = {
  id: string;
  title: string;
  content: string;
  price: number | null;
  rating: number | null;
};

由于GraphQL的架构是原始的,所以我也希望能自动生成。

注册

这个方法是在点击追加按钮时调用的。

  const onPost = () => {
    if (input.title === "" || input.content === "") return;
    const newPost: CreatePostMutationVariables = {
      input: {
        title: input.title,
        content: input.content
      }
    };
    setInput({ title: "", content: "" });
    API.graphql(graphqlOperation(createPost, newPost));
  };

由于GraphQL自动生成了代表性的查询,您可以通过将其指定为graphqlOperation来切换查询类型。在这里,由于是进行新的注册,所以我们使用createPost。

export const createPost = `mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    content
    price
    rating
  }
}
`;

只需设置要注册的数据并发送查询,因为已自动生成了与追加参数$input: CreatePostInput!对应的类型。

export type CreatePostInput = {
  id?: string | null,
  title: string,
  content: string,
  price?: number | null,
  rating?: number | null,
};

export type CreatePostMutationVariables = {
  input: CreatePostInput,
};

监控一览数据获取和数据注册

通过创建自定义钩子实现注册数据的列表获取和新增数据的监视。

通过在组件挂载时使用 useEffect,在顺序上添加获取帖子列表和创建帖子的订阅,然后使用 useState 返回创建的帖子列表作为返回值,以传达帖子列表的更新。

type PostSubscriptionEvent = { value: { data: OnCreatePostSubscription } };

const usePosts = () => {
  const [posts, setPosts] = useState<Post[]>([]);

  useEffect(() => {
    (async () => {
      // 最初のPost一覧取得
      const result = await API.graphql(graphqlOperation(listPosts));
      if ("data" in result && result.data) {
        const posts = result.data as ListPostsQuery;
        if (posts.listPosts) {
          setPosts(posts.listPosts.items as Post[]);
        }
      }

      // Post追加イベントの購読
      const client = API.graphql(graphqlOperation(onCreatePost));
      if ("subscribe" in client) {
        client.subscribe({
          next: ({ value: { data } }: PostSubscriptionEvent) => {
            if (data.onCreatePost) {
              const post: Post = data.onCreatePost;
              setPosts(prev => [...prev, post]);
            }
          }
        });
      }
    })();
  }, []);

  return posts;
};

为了确保类型的一致性,这有点复杂。

API.graphql的返回类型是Promise | Observable。根据graphqlOperation的参数内容,返回类型会发生变化。由于返回值的数据类型是object,并且无法通过泛型进行指定,因此需要使用if语句在某些地方进行类型缩小,或者使用as进行类型转换。也许还有更好的方法使用它,但我希望它能更加方便一些。

最后

起初我很难抓住整体的概念,但实际使用起来,GraphQL的API比我想象的还要容易建立。除了DynamoDB,它还可以与RDB和Lambda进行连接,似乎可以有许多应用,并且非常方便。

广告
将在 10 秒后关闭
bannerAds