nuxt.js+Amplify+AppSync+cognitoを使ってチャットサービスを作る

結論をいうと

yarn create nuxt-app ***
amplify init
amplify add api
amplify add auth
amplify push

終わりです、amplifyに全てを委ねてください

amplifyの準備

yarn global add @aws-amplify/cli
amplify configure

これを実行するとブラウザ立ち上がり、AWSのログイン画面になります。 ブラウザでログインを行い、ターミナルへ戻ります。下記の状態になっているので表示通りenterキーを押します。

Follow these steps to set up access to your AWS account:

Sign in to your AWS administrator account:
<https://console.aws.******.com/>
Press Enter to continue

するとIAMのユーザーを作成するための設定を対話形式で行っていく形になります。 途中またブラウザに戻りAWSのIAMのコンソールでユーザー作成する画面に遷移します。そこで、画面にそってユーザーを作成します。最終的に発行したユーザーのaccessKeyIdとsecretAccessKeyを対話型コマンドラインに入力します。

Enter the access key of the newly created user:
? accessKeyId: **************
? secretAccessKey: **************
...
Successfully set up the new user.

これで、Amplifyを使う際のアカウント設定が完了します。

nuxt HelloWorld!!

yarn create nuxt-app chat-aws-nuxt

? Project name chat-aws-nuxt
? Project description My peachy Nuxt.js project
? Use a custom server framework none
? Choose features to install Progressive Web App (PWA) Support, Linter / Formatter, Prettier, Axios
? Use a custom UI framework none
? Use a custom test framework jest
? Choose rendering mode Universal
? Author name castleobj
? Choose a package manager yarn

nuxt新しいのがいいので入れ直します

cd chat-aws-nuxt
yarn remove nuxt
yarn add nuxt

yarn dev

ちょっとすみません、fix設定します

package.json

"scripts": {
	"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
	"lintfix": "eslint --fix --ext .js,.vue --ignore-path .gitignore .",
	...
},

あ、scssで書きたいです

yarn add node-sass sass-loader -D

個人的な設定を

.eslintrc

rules: {
	"no-console": "off"
}

.prettierrc

{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all", // 追加
}

amplify init

amplify init

? Enter a name for the project chat-aws-nuxt
? Enter a name for the environment master
? Choose your default editor: Vim (via Terminal, Mac OS only)
? 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 start
Using default provider awscloudformation

For more information on AWS Profiles, see:
https://docs.aws.******.com/cli/latest/userguide/cli-multiple-profiles.html

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use default

profileは~/.aws/credentialsに記載されている

amplify add api

amplify add api

? Please select from one of the below mentioned services GraphQL
? Provide API name: chatAwsNuxt
? 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
Creating a base schema for you...

GraphQL schema compiled successfully.
Edit your schema at /Users/castleobj/repository/chat-aws-nuxt/amplify/backend/api/chat-aws-nuxt/schema.graphql or place .graphql files in a directory at /Users/castleobj/repository/chat-aws-nuxt/amplify/backend/api/chat-aws-nuxt/schema
Successfully added resource chat-aws-nuxt locally

Some next steps:
"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

amplify push

Current Environment: master

| Category | Resource name | Operation | Provider plugin   |
| -------- | ------------- | --------- | ----------------- |
| Api      | chat-aws-nuxt | Create    | awscloudformation |

? Are you sure you want to continue? Yes

GraphQL schema compiled successfully.
Edit your schema at /Users/castleobj/repository/chat-aws-nuxt/amplify/backend/api/chat-aws-nuxt/schema.graphql or place .graphql files in a directory at /Users/castleobj/repository/chat-aws-nuxt/amplify/backend/api/chat-aws-nuxt/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

...

GraphQL endpoint: https://※※※.appsync-api.us-east-1.******aws.com/graphql
GraphQL API KEY: ※※※

この情報は src/aws-exports.jsに自動的に挿入されます

src/aws-exports.js

const awsmobile = {
		"aws_project_region": "us-east-1",
		"aws_appsync_graphqlEndpoint": "https://※※※.appsync-api.us-east-1.******aws.com/graphql",
		"aws_appsync_region": "us-east-1",
		"aws_appsync_authenticationType": "API_KEY",
		"aws_appsync_apiKey": "※※※"
};

export default awsmobile;

これを下に修正(lint)

const awsmobile = {
	aws_project_region: 'us-east-1',
	aws_appsync_graphqlEndpoint:
	 'https://※※※.appsync-api.us-east-1.******aws.com/graphql',
	aws_appsync_region: 'us-east-1',
	aws_appsync_authenticationType: 'API_KEY',
	aws_appsync_apiKey: '※※※'
}

export default awsmobile

更新の度に修正は面倒なので.eslintignoreを作成して除外

/.eslintignore

aws-exports.js

yarn add aws-amplify aws-amplify-vue

nuxt.config.js

plugins: [{ src: '~/plugins/amplify.js', ssr: false }],

plugins/amplify.js

import Vue from 'vue'
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin, components } from 'aws-amplify-vue'
import awsconfig from '@/src/aws-exports'

Amplify.configure(awsconfig)

Vue.use(AmplifyPlugin, AmplifyModules)
Vue.component(components)

pages/index.vue

<template>
	<div class="container">
		<div>
			<h1 class="title">
				chat-aws-nuxt
			</h1>
			<h2 class="subtitle">
		    My magnificent Nuxt.js project
		  </h2>

		  <div className="App">
		    <div>
		      タイトル
		      <input v-model="state.title" name="title" @change="onChange" />
		    </div>
		    <div>
		      内容
		      <input v-model="state.content" name="content" @change="onChange" />
		    </div>
		    <button @click="createPost">追加</button>
		    <div v-for="(list, index) in mapPosts" :key="index">
		      <div>title: {{ list.title }}</div>
		      <div>content: {{ list.content }}</div>
		    </div>
		  </div>
		</div>
	</div>
</template>

<script>
import { API, graphqlOperation } from 'aws-amplify'
import { listPosts } from '@/src/graphql/queries'
import { createPost } from '@/src/graphql/mutations'
import { onCreatePost } from '@/src/graphql/subscriptions'

export default {
	data() {
		return {
			state: {
				posts: [],
				title: '',
				content: ''
			}
		}
	},

	computed: {
		mapPosts() {
			return this.state.posts.map((post, idx) => {
				return post
			})
		}
	},

	async mounted(e) {
		try {
			const posts = await API.graphql(graphqlOperation(listPosts))
			console.log('posts: ', posts)
			this.state.posts = posts.data.listPosts.items
		} catch (e) {
			console.log(e)
		}

		API.graphql(graphqlOperation(onCreatePost)).subscribe({
		  next: eventData => {
		    console.log('eventData: ', eventData)
		    const post = eventData.value.data.onCreatePost
		    const posts = [
		      ...this.state.posts.filter(content => {
		        return content.title !== post.title
		      }),
		      post
		    ]
		    this.state.posts = posts
		  }
		})
	},

	methods: {
		async createPost() {
			// バリデーションチェック
			if (this.state.title === '' || this.state.content === '') return
			
			// 新規登録 mutation
		  const createPostInput = {
		    title: this.state.title,
		    content: this.state.content
		  }

		  // 登録処理
		  try {
		    const posts = [...this.state.posts, createPostInput]
		    this.state.posts = posts
		    this.state.title = ''
		    this.state.content = ''
		    await API.graphql(
		      graphqlOperation(createPost, { input: createPostInput })
		    )
		    console.log('createPostInput: ', createPostInput)
		  } catch (e) {
		    console.log(e)
		  }
		},

		onChange(e) {
		  console.log(e)
		  this['e.target.name'] = e.target.value
		}
	}
}
</script>

<style lang="scss" scoped>
...
</style>

yarn dev

、、、

styleさわります、、!!、scssで書きたいっていいましたがcss自体書きたくないのでvuetify使います

yarn add @nuxtjs/vuetify

nuxt.config.js

...
	modules: [
	  '@nuxtjs/axios',
	  '@nuxtjs/pwa',
	  '@nuxtjs/eslint-module',
	  '@nuxtjs/vuetify'
	],
...

default.vue

<template>
  <v-app>
    <v-navigation-drawer
      v-model="drawer"
      :mini-variant="miniVariant"
      :clipped="clipped"
      fixed
      app
    >
      <v-list>
        <v-list-tile
          v-for="(item, i) in items"
          :key="i"
          :to="item.to"
          router
          exact
        >
          <v-list-tile-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            <v-list-tile-title v-text="item.title" />
          </v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>
    <v-toolbar color="primary" :clipped-left="clipped" fixed app dark>
      <v-toolbar-side-icon @click="drawer = !drawer" />
      <v-btn icon @click.stop="miniVariant = !miniVariant">
        <v-icon>{{ `chevron_${miniVariant ? 'right' : 'left'}` }}</v-icon>
      </v-btn>
      <v-btn icon @click.stop="clipped = !clipped">
        <v-icon>web</v-icon>
      </v-btn>
      <v-btn icon @click.stop="fixed = !fixed">
        <v-icon>remove</v-icon>
      </v-btn>
      <v-toolbar-title v-text="title" />
      <v-spacer />
      <v-btn icon @click.stop="rightDrawer = !rightDrawer">
        <v-icon>menu</v-icon>
      </v-btn>
    </v-toolbar>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
    <v-navigation-drawer v-model="rightDrawer" :right="right" temporary fixed>
      <v-list>
        <v-list-tile @click.native="right = !right">
          <v-list-tile-action>
            <v-icon light>compare_arrows</v-icon>
          </v-list-tile-action>
          <v-list-tile-title>Switch drawer (click me)</v-list-tile-title>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>
    <v-footer :fixed="fixed" app>
      <span>© 2019</span>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      items: [
        {
          icon: 'apps',
          title: 'Chat',
          to: '/'
        }
      ],
      miniVariant: false,
      right: true,
      rightDrawer: false,
      title: 'Chat'
    }
  }
}
</script>

index.vue

<template>
  <v-layout row wrap>
    <v-flex xs12 class="mb-5">
      <v-card>
        <v-card-title class="headline">Welcome to Chat</v-card-title>
        <div v-for="(list, index) in mapPosts" :key="index">
          <v-card-title class="font-weight-bold pb-0">
            {{ list.title }}
          </v-card-title>
          <v-card-text class="font-weight-thin pt-0">{{
            list.content
          }}</v-card-text>
        </div>
      </v-card>
    </v-flex>

    <v-flex xs12>
      <v-text-field
        v-model="state.title"
        label="title"
        @change.native="onChange"
      ></v-text-field>
    </v-flex>

    <v-flex xs12>
      <v-text-field
        v-model="state.content"
        label="content"
        @change.native="onChange"
      ></v-text-field>
    </v-flex>

    <v-flex xs12>
      <v-btn color="primary" @click="createPost">Add</v-btn>
    </v-flex>
  </v-layout>
</template>

<script>
import { API, graphqlOperation } from 'aws-amplify'
import { listPosts } from '@/src/graphql/queries'
import { createPost } from '@/src/graphql/mutations'
import { onCreatePost } from '@/src/graphql/subscriptions'

export default {
  data() {
    return {
      state: {
        posts: [],
        title: '',
        content: ''
      }
    }
  },

  computed: {
    mapPosts() {
      return this.state.posts.map((post, idx) => {
        return post
      })
    }
  },

  async mounted(e) {
    try {
      const posts = await API.graphql(graphqlOperation(listPosts))
      console.log('posts: ', posts)
      this.state.posts = posts.data.listPosts.items
    } catch (e) {
      console.log(e)
    }

    API.graphql(graphqlOperation(onCreatePost)).subscribe({
      next: eventData => {
        console.log('eventData: ', eventData)
        const post = eventData.value.data.onCreatePost
        const posts = [
          ...this.state.posts.filter(content => {
            return content.title !== post.title
          }),
          post
        ]
        this.state.posts = posts
      }
    })
  },

  methods: {
    // createPost = async () => {
    async createPost() {
      // バリデーションチェック
      if (this.state.title === '' || this.state.content === '') return

      // 新規登録 mutation
      const createPostInput = {
        title: this.state.title,
        content: this.state.content
      }

      // 登録処理
      try {
        const posts = [...this.state.posts, createPostInput]
        this.state.posts = posts
        this.state.title = ''
        this.state.content = ''
        await API.graphql(
          graphqlOperation(createPost, { input: createPostInput })
        )
        console.log('createPostInput: ', createPostInput)
      } catch (e) {
        console.log(e)
      }
    },

    onChange(e) {
      console.log(e)
      this['e.target.name'] = e.target.value
    }
  }
}
</script>

<style lang="scss" scoped></style>

lint系のerrorは

yarn lintfix

で修正しました

universalだとssrの関係で[window is not defined]なのでspaにしました。

Deploy

Amplify Console の手順を説明します。

まずは「AWS Amplify」を選択します。

「GET STARTED」を選択します。

今回は「GitHub」を例にします。

・リポジトリの選択
・ブランチの選択

・ブランチの選択
・ロールの作成

baseDirectoryを「/dist」に修正します。

「保存してデプロイ」

デプロイが終わるまで待ちます。
「検証」までチェックがつけば終わりです。
エラーが出た際はエラーログを確認しましょう。

cognitoの追加

amplify add auth
amplify push

src/aws-exports.jsが書き換わる

コード編集(ログインフォーム、認証処理諸々)

git add -A
git commit -m 'cognito'
git push

awsのコンソールでこちょこちょやらずに、ターミナルからamplifyに全て任せたほうがうまくいった。 amplifyすごい。

カラム追加、テーブル追加

amplify/backend/api/boardappAwsNuxt/schema.graphql

編集

amplify api gql-compile

amplify push

/signinへのリダイレクト追加、nuxtのmiddlewareに書きます。

middleware/auth.js追加
signin.vue追加
pages/index.vue修正
default.vue修正
unsplash-logoJESHOOTS.COM

Photo by JESHOOTS.COM on Unsplash