84
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Vue.js #3Advent Calendar 2018

Day 14

Vue.js×GraphQL(AppSync)×AmplifyでTODOリストを作る

Last updated at Posted at 2018-12-14

この記事は Vue.js #3 Advent Calendar 2018 の14日目の記事です。

はじめに

ある日、「Vue.jsとGraphQLでなんか簡単な画面を作るハンズオン的なのできないか?」という要請が来たので、この記事を書きました。
ついでにAmplifyCLIを使えば、より簡単にAWSのサービスを構築することができてアプリ側からの連携も楽になるので、Amplifyも採用しました。

この記事では、Vue.jsでタイトルと内容TODOリスト(みたいなもの)を作成/削除するアプリケーションを、AWSにバックエンドのサービスをデプロイしながら開発していくことを体験することを目的に書いています。お時間のある方はぜひ試してください。

前提条件

下記がインストール済みであること。

  • Node.js / npm
  • AWSCLI

アーキテクチャ

フロントはVue.js(以下、Vue)で書きます。ログイン機能をつけるのでAmazonCognito(以下、Cognito)を採用。
GraphQLはAWSAppSync(以下、AppSync)を使います。TODOリストのデータはDynamoDBに格納します。

todo_list.png

完成品

todo_list.gif

手順1. Vue-CLIのインストール

Vueプロジェクトをビルドするためのツールをインストールします。ここではVue-CLI3を使っていきます。

$ npm install -g @vue/cli

インストールが完了したら、バージョンを確認します。

$ vue -V

手順2. Vueプロジェクト作成

プロジェクトを作成します。

$ vue create vue-todo-list

詳細設定をしていきます。

スクリーンショット 2018-12-12 12.38.50.png

最後まで設定すると自動的にビルド環境がローカルに用意されます。成功すると下のようなメッセージが表示されます。

スクリーンショット 2018-12-11 17.03.26.png

cd vue-todo-list して yarn serve とすると、このようなデフォルト画面が表示されます。

スクリーンショット 2018-12-11 17.04.49.png

今回TemplateにPugを利用するので、Pugをビルドするためのパッケージをインストールします。

$ vue add pug

これでVueプロジェクトの作成は完了です。

手順3. AmplifyCLIでAWSサービスを準備する

そもそもAmplifyとは?

AmplifyはAWSリソースとの接続を支援するオープンソースのライブラリです。
強力なCLIツールが提供されていて、AWSリソースの作成をコマンド上で実行し、ライブラリに反映することができます。 現在はCognitoによる認証や、AppSyncを用いたGraphQLの利用などがサポートされています。
参考)AWSAmplifyの公式ドキュメント

AmplifyCLIのインストール

Vue-CLIと同様にnpmでインストールします。

$ npm i -g @aws-amplify/cli

インストールできているか確認します。

$ amplify

スクリーンショット 2018-12-11 18.01.40.png

Amplify用のユーザー作成

AmplifyCLIで利用するユーザーを作成します。
ここではAdminユーザーを作成して、 amplify というProfile名でAWSCLIに保存します。
リージョンは ap-northeast-1 (東京リージョン)を、ユーザー名はデフォルトのまま作成してください。
作成したIAMユーザのアクセスキーとシークレットアクセスキーを入力する必要があるので、CSVをダウンロードするなりして保管しておいてください。
最後にAWSCLIのプロファイルとしてユーザーの情報を保存させるので、好きな名前を付けて保存してください。下の画像は amplify というプロファイル名で保存しています。

$ amplify configure

スクリーンショット 2018-12-11 18.03.30.png

Amplifyの初期化

IDEや言語、どういったFrameworkを使っているのかを設定していきます。
途中でどのIAMユーザーを利用するか?と聞かれるのでAdmin権限を持っているユーザーを選択します。
※青くなっている文字が選択した内容です。

$ amplify init

スクリーンショット 2018-12-11 18.04.08.png

Successfully created ... というメッセージが表示されたので、AmplifyCLIを利用する準備ができました。

手順4. Cognitoをデプロイする

ログインのバックグラウンドとしてCognitoを利用するので、AmplifyCLIでデプロイしていきましょう。

そもそもCognitoって?

ログインなどの認証機能を実装する際に便利なマネージドサービスです。Serverlessにログイン認証を提供してくれます。
ユーザー情報などをCognitoで持ってくれるので、別にDBなどを用意する必要がありません。
参考)Cognitoの公式サイト

デプロイ準備

まずどのような設定でCognitoをデプロイするかを決めます。
今回はデフォルトのままで問題ないです。

$ amplify auth add

デプロイ実行

実際にデプロイしましょう

$ amplify auth push

✔ All resources are updated in the cloud というメッセージが出れば成功です。
コンソール画面でCognitoが作成されているの確認することができます。

手順5. AppSyncをデプロイする

GraphQLを利用するためにAppSyncを用意します。

そもそもAppSyncって?

AWSのサービスに対して、GraphQLで操作するためのサービスです。
GraphQLを利用して、DynamoDBのデータを操作したり、Lambdaをinvokeしたりすることができます。
参考)AppSyncの公式サイト

デプロイ準備

下の画像の青くなっている文字が選択した内容です。

  • Provide API name には任意のAPI名をつけてください。
  • APIをCognito認証で利用するので、 Amazon Cognito User Pool を選択。
  • TODOリストを作成するためのSchemaが用意されているので、今回はすでに用意されているSchemaを利用します。
$ amplify api add

スクリーンショット 2018-12-12 9.57.08.png

デプロイ

準備ができたのでデプロイしていきましょう。
途中で「GraphQLAPIのコードを作るか?」と聞かれますが、一旦ここでは作らずに進めます。

$ amplify api push

✔ All resources are updated in the cloud というメッセージが出れば成功です。
このタイミングでAppSyncのAPIとDynamoDBのテーブルが作成されています。

これでAWSの構築が完了しました。
この状態で ./src 以下に aws-exports.js が生成されているので、このファイルをコピーして aws-exports.ts を作成しておいてください。

手順6. 画面の作成

AWS側の準備が整ったので、Vueのアプリを作成していきましょう。

プラグインのインストール

VueのUIコンポーネントである Element UI と AWSAmplify@types/node インストールします。

$ npm i --save-dev element-ui aws-amplify @types/node

インストールできたらVueに読み込ませます。

main.ts
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

// モジュールの読み込み
import ElementUI from "element-ui";  
import "../node_modules/element-ui/lib/theme-chalk/index.css";
import Amplify from "aws-amplify";
import appSyncConfig from "./aws-exports";

Amplify.configure(appSyncConfig); // 追加

Vue.config.productionTip = false;
Vue.use(ElementUI); //追加

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app");

また @types/node を有効にするために、インストールしたあと tsconfignode を追加します。

tsconfig.json
...
    "types": [
      "webpack-env",
      "node" // 追加
    ],
...

エラーが発生する場合は…

Could not find a declaration file for module 'graphql/language/ast'...Could not find a declaration file for module '@aws-amplify/ui'... というエラーメッセージが出る。

noImplicitAny オプションを無効にします。

tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": false,
...
  }
...
}

親Component(App.vue)の修正

上部にあるメニューが必要ないので、削除しましょう。

App.vue
<template lang="pug">
  #app
    router-view
</template>

<style lang="scss">
body {
  background-color: #2c3e50;
  margin: 0;
}

#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #ffffff;
}

.title {
  font-size: 4rem;
  margin-top: 10vh
}
</style>

SignInページのVueファイル

src/views/ 以下に SignIn.vue を作成し、内容は下記です。

src/views/SignIn.vue
<template lang="pug">
.signin-page
  .title SignIn
  form.signin-form(@submit.prevent="signIn")
    el-row.user-info.input-area
      el-col(:span="6")
        .text ID
      el-col(:span="12")
        el-input(v-model="userID")
    el-row.password-info.input-area
      el-col(:span="6")
        .text Password
      el-col(:span="12")
        el-input(type="password" v-model="password")
    el-button.auth-button(type="primary" native-type="submit") サインイン
    el-button.auth-button(type="info" @click="linkSignUp") サインアップする
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Auth } from "aws-amplify";
import router from "@/router";

@Component({})
export default class SignIn extends Vue {
  password: string = "";
  userID: string = "";

  public signIn() {
    const self = this;
    Auth.signIn(self.userID, self.password)
      .then(user => {
        return router.push("/");
      }).catch(err => {
        console.error(err);
      });
  }

  public linkSignUp() {
    return router.push("/signUp");
  }
}
</script>

<style scoped lang="scss">
.signin-form {
  margin: 40px auto 0;
  width: 40vw;

  .input-area {
    line-height: 60px;
    margin: 10px 0;
  }

  .auth-button {
    font-weight: bold;
    margin-top: 20px;
    width: 40%;
  }
}
</style>

SignUpページのVueファイル

src/views/ 以下に SignUp.vue を作成し、内容は下記です。

src/views/SignUp.vue
<template lang="pug">
.signup-page
  .title SignUp
  form.signup-form(@submit.prevent="signUp" v-show="signupForm")
    el-row.email-info.input-area
      el-col(:span="6")
        .text EmailAddress
      el-col(:span="12")
        el-input(v-model="email")
    el-row.user-info.input-area
      el-col(:span="6")
        .text ID
      el-col(:span="12")
        el-input(v-model="userID")
    el-row.password-info.input-area
      el-col(:span="6")
        .text Password
      el-col(:span="12")
        el-input(type="password" v-model="password")
    el-button.auth-button(type="primary" native-type="submit") サインアップ
  form.signup-form(@submit.prevent="userVerify" v-show="!signupForm")
    el-row.email-info.input-area
      el-col(:span="6")
        .text VerifyCode
      el-col(:span="12")
        el-input(v-model="verifyCode")
    el-button.auth-button(type="primary" native-type="submit") ユーザーの有効化
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Auth } from "aws-amplify";
import router from "@/router";

@Component({})
export default class SignUp extends Vue {
  email: string = "";
  password: string = "";
  signupForm: boolean = true;
  userID: string = "";
  verifyCode: string = "";

  public signUp() {
    const self = this;
    Auth.signUp(self.userID, self.password, self.email)
      .then(user => {
        self.signupForm = false;
      }).catch(err => {
        console.error(err);
      });
  }

  public userVerify() {
    console.log("verify!");
    const self = this;
    Auth.confirmSignUp(self.userID, self.verifyCode)
      .then(data => {
        alert("登録完了しました");
        return router.push("/signIn");
      }).catch(err => {
        console.error(err);
      });
  }
}
</script>

<style scoped lang="scss">
.signup-form {
  margin: 40px auto 0;
  width: 40vw;

  .input-area {
    line-height: 60px;
    margin: 10px 0;
  }

  .auth-button {
    font-weight: bold;
    margin-top: 20px;
    width: 40%;
  }
}
</style>

TODOリストページのVueファイル

src/views/ 以下に TODO.vue を作成し、内容は下記です。

src/views/TODO.vue
<template lang="pug">
.todo-list-page
  .title TODO List
  .list-area
    el-button.signout(@click="signOut") SignOut
    el-col.todo-card(:span="8")
      el-card.box-card
        .card-header(slot="header")
          el-input(v-model="cardTitle")
        .card-body
          el-input(type="textarea" :rows="10" v-model="cardBody")
        el-button.card-button.card-create-button(type="primary" @click="create") 作成
    el-col.todo-card(:span="8" v-for="item in listItems" :key="item.name")
      el-card.box-card
        .card-header(slot="header")
          .text {{ item.name }}
        .card-body
          .text {{ item.description }}
        el-button.card-button.card-delete-button(type="danger" @click="remove(item.id)") 削除
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { Auth, API, graphqlOperation } from "aws-amplify";
import router from "@/router";

type listItemType = {
  id: string,
  name: string,
  description: string
};

@Component({})
export default class TODO extends Vue {
  cardBody: string = "";
  cardTitle: string = "";
  listItems: listItemType[] = [];

  async created() {
    await this.getListItems();
  }

  // サインアウト処理
  public signOut() {
    Auth.signOut().then(data => {
      return router.push("/signIn");
    }).catch(err => {
      console.error(err);
    });
  }

  // TODOリストの作成
  public async create() {
    console.log(this);
    const gqlBody = `
      mutation create {
        createTodo(input: {
          name: "${this.cardTitle}"
          description: "${this.cardBody}"
        }) {
          id
          name
          description
        }
      }
    `;
    const result: any = await API.graphql(graphqlOperation(gqlBody));
    this.listItems.unshift(result.data.createTodo);
  }

  // TODOリストの削除
  public async remove(id: string) {
    console.log(id);
    const gqlBody = `
      mutation delete {
        deleteTodo(input: {
          id: "${id}"
        }) {
          id
        }
      }
    `;
    const result: any = await API.graphql(graphqlOperation(gqlBody));
    const newListItems: listItemType[] = [];
    this.listItems.filter(item => {
      console.log(item);
      if (result.data.deleteTodo.id !== item.id) {
        newListItems.push(item);
      }
    });
    this.listItems = newListItems;
  }

  // TODOリスト取得
  public async getListItems() {
    const gqlBody = `
      query list {
        listTodos(limit: 10) {
          items {
            id
            name
            description
          }
        }
      }
    `;
    const result: any = await API.graphql(graphqlOperation(gqlBody));
    this.listItems = result.data.listTodos.items;

    console.log(this.listItems);
  }
}
</script>

<style scoped lang="scss">
.list-area {
  margin: 28px auto 0;
  width: 90vw;

  .signout {
    font-weight: bold;
    position: absolute;
    right: 40px;
    top: 40px;
  }

  .todo-card {
    padding: 15px;

    .box-card {
      height: 380px;
      position: relative;

      .card-body {
        text-align: left;
      }

      .el-button {
        font-weight: bold;

        &.card-button {
          bottom: 10px;
          left: 0;
          margin: auto;
          position: absolute;
          right: 0;
          width: 80px;
        }
      }
    }
  }
}
</style>

Routeの設定

router.ts の内容を下記のように書き換えます。

router.ts
import Vue from "vue";
import Router from "vue-router";
import SignIn from "./views/SignIn.vue";
import SignUp from "./views/SignUp.vue";
import TODO from "./views/TODO.vue";

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "TODO",
      component: TODO,
    },
    {
      path: "/signIn",
      name: "signIn",
      component: SignIn,
    },
    {
      path: "/signUp",
      name: "signUp",
      component: SignUp,
    },
  ],
});

これでアプリ側の準備が整いました。

手順7. 動作の確認

サインアップ

「サインアップする」をクリック

vue-todo-list 2018-12-13 13-06-45.png

メールアドレス(EmailAddress) / ユーザー名(ID) / パスワード(Password) を入力して「サインアップ」をクリック

vue-todo-list 2018-12-13 13-07-52.png

検証コードがメールアドレスに送信されるので、そのコードを入力して「ユーザーの有効化」をクリック

vue-todo-list 2018-12-13 13-09-33.png

これでログインユーザーが作成されました。

サインイン

先ほど作成したユーザーの情報を入力して「サインイン」をクリック

vue-todo-list 2018-12-13 13-14-21.png

ログインできるとTODOリストページに遷移します。

スクリーンショット 2018-12-14 9.49.18.png

TODOリストの作成/削除

作成

TODOリストを作成します。
タイトルと内容を入力して「作成」をクリック。

vue-todo-list 2018-12-13 13-15-40.png

追加されました。実際にDynamoDBに書き込まれています。

vue-todo-list 2018-12-13 13-17-03.png スクリーンショット 2018-12-14 9.57.37.png

削除

TODOリストを削除します。
作成済みのリストにある「削除」をクリック。

vue-todo-list 2018-12-13 13-17-45.png

削除されました。DynamoDBからも削除されています。

vue-todo-list 2018-12-13 13-18-58.png スクリーンショット 2018-12-14 9.59.02.png

現状できることはこんな感じです。

さいごに

ここまでできた内容からさらに発展させていきましょう。
例えば、TODOリストの作成と削除まですることができるようになっていますが、更新ができるようにVueファイルを編集してみたり、TODOリストに残せる内容を増やしてみたりなどなど…

またこのアプリを公開するためにデプロイする必要がありますね。
ビルドしてS3にアップロードしてWebサイトホスティングするもよし、AmplifyConsoleを利用してデプロイしても良しです!

アプリをアップデートして更にVueやGraphQL(AppSync)の知識を深めましょう!
ではまた!!!

84
70
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
84
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?