AWS Amplify と Vueで簡単チャット作るぞ！！


AWS Amplify良いですよね。皆さんは使っていますか？
Amplifyを使うことでSPAなWebアプリケーションにAWSの多様な機能を組み込むことができますが、 ここではDynamoDB + GraphQL をデータソースとした簡単なチャットを作ってみます。
amplifyのコマンドを実行することで、AWSのマネジメントコンソール等を直接操作することなくある程度のアプリケーションを作ることができることを体感していただけるといいなーと思います。

ちなみにこれは[AWS Amplify Advent Calendar 2019](https://qiita.com/advent-calendar/2019/aws-amplify)の14日の記事です！



# 開発環境について

```bash
$ vue -V
3.11.0

$ amplify -v
1.7.3
```



# 1. Vueアプリケーションを作る

```bash
vue create vue-chat-app

Vue CLI v4.1.1
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint) 
  Manually select features 
```

とりあえず起動してみます。

```
cd vue-chat-app
yarn serve
```

[http://localhost:8081](http://localhost:8081/)

# 2. Vueアプリケーションを作る

まずはAmplifyを組み込まない状態で、簡単に挙動を確認できるところまでVueアプリケーションを作ります。


## Bootstrapの組み込み

UI周りは`bootstrap-vue`を使いますね。

```bash
$ yarn add bootstrap-vue bootstrap core-js
```

## アプリケーションの実装

メッセージ送信ダイアログ展開
↓
フォームに入力
↓
メッセージ表示

ができるところまでデータベースへの更新処理無しで実装してみます。

**src/main.js**

```js
import Vue from 'vue'
import App from './App.vue'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue)

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
```

**src/App.vue**

```js
<template>
  <div id="app">
    <header>
      <b-navbar variant="info" type="dark">
        <b-navbar-brand href="#">Vue Chat App</b-navbar-brand>

        <b-navbar-nav class="ml-auto">
          <b-nav-form>
            <b-button @click="modalShow = !modalShow" variant="light">Post</b-button>
          </b-nav-form>
        </b-navbar-nav>
      </b-navbar>
    </header>

    <b-container>
      <section class="form">
        <section>
          <b-modal v-model="modalShow" size="lg" title="Post chat message">
            <section v-if="errors.length > 0" class="alerts">
              <div
                v-for="(error, j) in errors"
                v-bind:key="j"
                class="alert alert-danger"
                role="alert"
              >{{ error }}</div>
            </section>

            <b-form>
              <b-form-group label="User:" label-for="user">
                <b-form-input id="user" v-model="form.user" required placeholder="Enter user"></b-form-input>
              </b-form-group>

              <b-form-group label="message" label-for="message">
                <b-form-textarea
                  id="message"
                  v-model="form.message"
                  placeholder="Enter something..."
                  rows="3"
                  max-rows="6"
                ></b-form-textarea>
              </b-form-group>
            </b-form>

            <template v-slot:modal-footer="{ submit, cancel, close }">
              <b-button variant="success" @click="onPostMessage">Submit</b-button>
              <b-button variant="default" @click="onCancel">Cancel</b-button>
            </template>
          </b-modal>
        </section>

        <section>
          <b-card
            v-for="(chat, i) in chats"
            v-bind:key="i"
            v-bind:footer="chat.user + ' - ' + chat.created_at"
            footer-tag="footer"
          >
            <b-media>
              <template v-slot:aside>
                <b-img width="64" src="https://picsum.photos/200"></b-img>
              </template>
              {{ chat.message }}
            </b-media>
          </b-card>
        </section>
      </section>
    </b-container>
  </div>
</template>

<script>
export default {
  user: "app",
  data() {
    return {
      form: {
        user: "",
        message: ""
      },
      chats: [],
      modalShow: false,
      errors: []
    };
  },
  computed: {
    formIsInValid() {
      return this.errors.length > 0;
    },
    chatParams() {
      return {
        ...this.form,
        created_at: new Date().toLocaleString()
      };
    }
  },
  methods: {
    onPostMessage() {
      this.checkMessageParams();
      if (this.formIsInValid) {
        return;
      }

      this.postMessage();

      this.modalShow = false;
      this.form.user = "";
      this.form.message = "";
    },
    postMessage() {
      this.chats.push(
        this.chatParams
      )
    },
    onCancel() {
      this.modalShow = false;
    },
    checkMessageParams() {
      this.errors = [];

      if (this.form.user === "") {
        this.errors.push('"User" is required.');
      }
      if (this.form.message === "") {
        this.errors.push('"message" is required.');
      }
    }
  }
};
</script>

<style>
.card {
  margin: 1em 0;
}
</style>
```

![vue-chat-app.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8607/eecdbbfa-e571-9756-e0f3-52b77bcf8b4e.jpeg "vue-chat-app.jpg")

![vue-chat-app.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8607/c39c3a2f-1ac2-55d3-1b81-cd8145dd8d94.jpeg "vue-chat-app.jpg")


![vue-chat-app.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8607/bd662d4a-38e2-a16a-8a7a-f95317837ad6.jpeg "vue-chat-app.jpg")

![vue-chat-app.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8607/b07357cf-4ab3-89d3-0b7c-5a3ae9eb12a8.jpeg "vue-chat-app.jpg")


# 3. Amplifyを組み込む

### 3.1 初期化

おもむろに以下のコマンドを叩きます。

```bash
$ amplify init


Note: It is recommended to run this command from the root of your app directory

? Enter a name for the project 
vue-chat-app

? Enter a name for the environment
develop

? 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
vue

? Source Directory Path:
src

? Distribution Directory Path:
dist

? Build Command:
yarn build

? Start Command:
yarn serve
```

## 3.2 S3へのホスティング

順番はどちらでもいいのですが、今回はDB周りの設定をする前に現在の状態のアプリケーションをS3へデプロイします。

```bash
$ amplify add hosting

? Select the environment setup: (S3 only with HTTP)
DEV

? hosting bucket name
vue-chat-app-20191214083130-hostingbucket

? index doc for the website
index.html

? error doc for the website
index.html

You can now publish your app using the following command:
Command: amplify publish
```


```bash
$ amplify publish

Current Environment: develop

| Category | Resource name   | Operation | Provider plugin   |
| -------- | --------------- | --------- | ----------------- |
| Hosting  | S3AndCloudFront | Create    | awscloudformation |

? Are you sure you want to continue?
Yes

~

✨  Done in 35.82s.
frontend build command exited with code 0
✔ Uploaded files successfully.
Your app is published successfully.
http://vue-chat-app-20191214083130-hostingbucket-develop.s3-website-ap-northeast-1.amazonaws.com
```

`publish`を実行すると`amplify init`でコマンドラインで回答したビルドコマンド(`yarn build`) が実行され、S3へデプロイされます。
デプロイが完了するとS3のエンドポイントのアドレスが表示されるのであくせすしてみます。

[http://vue-chat-app-20191214083130-hostingbucket-develop.s3-website-ap-northeast-1.amazonaws.com](http://vue-chat-app-20191214083130-hostingbucket-develop.s3-website-ap-northeast-1.amazonaws.com)

ローカルで確認した通りのアプリケーションが表示されればOKですね。


## 3.3 APIの追加

```bash
$ amplify add api

? Please select from one of the below mentioned services
GraphQL

? Provide API name:
vuechatapp

? Choose an authorization type for the API
API key

? Do you have an annotated GraphQL schema?
No

? Do you want a guided schema creation?
Yes

? What best describes your project: (e.g., “Todo” with ID, name, description)
Single object with fields

? Do you want to edit the schema now?
Yes

Please edit the file in your editor: /Users/Work/aws_amplify/vue-chat-app/amplify/backend/api/vuechatapp/schema.graphql

s? Press enter to continue
```


`yes`を選択するとエディタが起動し、以下のようなgraphqlスキーマファイルのテンプレートが表示されます。

**schema.graphql**

```
type Todo @model {
  id: ID!
  name: String!
  description: String
}
```

以下のように変更します。

```
type Chat @model {
  id: ID!
  user: String!
  message: String
  created_at: String
}
```


`amplify status`で見てみます。

```
$ amplify status

Current Environment: develop

| Category | Resource name   | Operation | Provider plugin   |
| -------- | --------------- | --------- | ----------------- |
| Api      | vuechatapp      | Create    | awscloudformation |
| Hosting  | S3AndCloudFront | No Change | awscloudformation |

Hosting endpoint: http://vue-chat-app-20191214083130-hostingbucket-develop.s3-website-ap-northeast-1.amazonaws.com

```

で、再びデプロイします。
API側のリソース作成なので、今回は`push`します。
`push`はローカルで変更されたamplifyの変更をAWSへ適用する操作、
`publish`はamplifyの変更に加えてアプリケーションのビルド&デプロイをする操作ですね。

```bash
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
```

`amplify push`すると幾つか質問が表示されます。
以下は全てデフォルト値のままです。

```bash
GraphQL schema compiled successfully.
Edit your schema at /Users/yuto-ogi/Work/aws_amplify/vue-chat-app/amplify/backend/api/vuechatapp/schema.graphql or place .graphql files in a directory at /Users/yuto-ogi/Work/aws_amplify/vue-chat-app/amplify/backend/api/vuechatapp/schema

? Do you want to generate code for your newly created GraphQL API
Yes

? Choose the code generation language target 
javascript

? Enter the file name pattern of graphql queries, mutations and subscriptions
src/graphql/**/*.js

? 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

⠏ Updating resources in the cloud. This may take a few minutes...
```


AWSでのGrapyQLリソース作成が完了すると以下のようなファイルがソースディレクトリに作成されます。

```bash
  modified:   .graphqlconfig.yml
  modified:   src/graphql/mutations.js
  modified:   src/graphql/queries.js
  modified:   src/graphql/schema.json
  modified:   src/graphql/subscriptions.js
```

さらに、`src/`配下に`aws-exports.js`が作成されます（認証情報が含まれるのでgitignore対象のファイルです）
これをVueアプリケーションで読み込みます。

```yml
const awsmobile = {
    "aws_project_region": "ap-northeast-1",
    "aws_content_delivery_bucket": "*********************************",
    "aws_content_delivery_bucket_region": "ap-northeast-1",
    "aws_content_delivery_url": "*********************************",
    "aws_appsync_graphqlEndpoint": "*********************************",
    "aws_appsync_region": "ap-northeast-1",
    "aws_appsync_authenticationType": "API_KEY",
    "aws_appsync_apiKey": "*********************************"
};


export default awsmobile;
```

## 3.4 VueアプリケーションへのAppSyncの組み込み

```bash
$ yarn add aws-amplify aws-amplify-vue
```


以下を追記します。

**main.js**

```
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)
Vue.use(AmplifyPlugin, AmplifyModules)

Vue.config.productionTip = false
```

**App.vue**

`amplify push`後に作成されたGraphQL関連のファイルをimportし、アプリケーションに組み込みます。

```js
import { API, graphqlOperation } from "aws-amplify";
import { createChat } from "@/graphql/mutations";
import { listChats } from "@/graphql/queries";
import { onCreateChat } from "@/graphql/subscriptions";
```

`created`アクション内では既存のレコードを読み込み、またデータベースへの変更を購読する、という2つの処理を行っています。

```js
async created() {
  // query
  const queryResponse = await API.graphql(graphqlOperation(listChats));
  this.chats = queryResponse.data.listChats.items;

  // subscribe
  API.graphql(
    graphqlOperation(onCreateChat)
  ).subscribe({
    next: chat => {
      this.chats.push(chat.value.data.onCreateChat);
    }
  });
},
```

`postMessage`ではGrapyQLのmutationにてDynamoDBのレコードを作成しています。

```js
async postMessage() {
  await API.graphql(
    graphqlOperation(createChat, {
      input: this.chatParams
    })
  ).catch(error => {
    console.error(error);
  });
},
```

# 4. 公開

ローカルで一通りの動作確認ができたらS3へデプロイします。

```
$ amplify publish
```

以下のような感じになりました。
ユーザーアイコンの表示には(Lorem Picsum](https://picsum.photos/)を使用していますが、とりあえずリンクを指定しているだけなのでリロードする度に画像変わるし全てのユーザーで同じ画像が表示されます^^;

<iframe width="560" height="315" src="https://www.youtube.com/embed/AELvafMiRvk" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 最後に

このアプリケーションは[Hamamatsu.js #8](https://connpass.com/event/148554/)のハンズオンにて使用したものです。
残念ながら当日はAmplifyの組み込みまで至らなかったので、このアドベントカレンダーが供養になればいいなーと思います。

最終的なソースコードはここにありますので良かったら見ていってください。

[jacoyutorius/vue-chat-app](https://github.com/jacoyutorius/vue-chat-app)