NativeScript-VueとAWS Amplifyで爆速でアプリを実装する

# はじめに

NativeScript-Vueを用いてアプリ開発をしています。
その中で、AWS Amplifyと連携する機会があったので紹介していきたいと思います。

# アプリの完成イメージ

下部の入力エリアで入力した内容が追加ボタンで追加されます。
タスクのトグルボタンを押下することでタスクに取り消し線と文字が薄くなります。

<img width="300" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106459/5c7db63d-f9e1-d95e-9aa9-76340457b4fd.png">


# 今回使用する技術を軽く説明
## NativeScript-Vue
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106459/aee277b2-e387-02db-6ed3-6e1db0cce4e5.png" width="30%">
[NativeScript-Vue](https://nativescript-vue.org/)は、モバイルアプリをJavaScriptで開発するためのOSSです。
モバイルアプリを構築するフレームワークだと、ReactNativeやionicなどReactやAngularを用いることが多いですが、Vue.jsで書けるのがNativeScript-Vueの大きな特徴です。（NativeScriptはAngular）

## AWS Amplify
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106459/5cca4f38-880c-9787-87f9-c099a0ad0223.png" width="30%">
[AWS Amplify](https://aws.amazon.com/jp/amplify/)は、モバイルアプリやWebアプリの作成、実装、設定を容易にしてくれるライブラリです。
認証機能やデータの連携を簡単に構築することができます。

# 環境
動作が確認できた環境です。
また、AWS Amplifyを使用する関係でAWSのアカウントが必要となります。
※amplify mockを使用するため、料金はかかりません。

```bash
MacOS Mojave: 10.14.6
Node.js: 12.13.1
Python: 3.6.8

Java -version
openjdk version "1.8.0_232"
OpenJDK Runtime Environment Corretto-8.232.09.2 (build 1.8.0_232-b09)
OpenJDK 64-Bit Server VM Corretto-8.232.09.2 (build 25.232-b09, mixed mode)
```
※Python3系は、NativeScript-Vueを実行するために必要です。

# アプリの構築
## NativeScript-Vueのセットアップ
vue-cliを利用して、NativeScript-Vueのアプリを構築します。

```bash
npm install -g @vue/cli @vue/cli-init

vue init nativescript-vue/vue-cli-template <app-name>
# ? Select a preset (more coming soon) (Use arrow keys)
# ❯ Simple

cd <app-name>
npm install
```

NativeScriptでは、`tns`というコマンドを使用します。
下記のコマンドで、`iPhone 11 Pro`のシミュレータが起動します。
もちろん、`tns run android`と実行するとAndroidを起動することが可能です。

```bash
tns run ios --debug --device 'iPhone 11 Pro'
```

## AWS Amplifyのセットアップ

```bash
npm install @aws-amplify/cli
npm i @aws-amplify/api aws-amplify

amplify --version
# 4.18.1
```

## AWS Amplifyを使う準備

### webpackの設定を変更する

ここが今回のポイントです。
AWS amplifyをWebのフレームワークで使用した場合には発生しないのですが、Nativescriptで実装した場合、`mjs`ファイルを明示的に読み込めるようにしてあげる必要があります。

```diff:webpack.config.js
（省略）

+aliasFields: ["browser"],
+extensions: [".mjs", ".vue", ".ts", ".js", ".scss", ".css"],
-extensions: [".vue", ".ts", ".js", ".scss", ".css"],

（省略）
```

### main.jsにAmplifyのモジュールを使う設定

下記を追加する。

```javascript:main.js
import Amplify, * as AmplifyModules from 'aws-amplify'
import awsmobild from './aws-exports'

Amplify.configure(awsmobild)
Vue.use(AmplifyModules)
```

## APIを作成する

```bash
# IAMの作成をしていない方は、aws configureを実行する必要があります。
amplify configure

amplify init

amplify add api
```

```graphql:amplify/backend/api/schema.graphql
type Message @model {
  id: ID!
  username: String!
  content: String!
}
```

## APIのモックサーバ起動
今回は、amplify mockを利用します。
amplify mockは、作成したAPIをローカル環境で確認することができます。

```bash
amplify mock api

# amplify pushをすれば、AppSyncを構築し、DynamoDBにテーブルが作成まで行われます。今回は、mockに留めます。
```

## todoアプリのUIを作成する

### ビュー部分

todosという配列をfor文で回して表示しています。
todosが空の時は、メッセージを表示しています。

```vue:<app-name>/app/components/App.vue
<template>
  <Page>
    <ActionBar title="Todo" />
    <GridLayout columns="*" rows="*, auto">
      <ScrollView class="todo-area" row="0">
        <StackLayout v-if="todos.length > 0" class="todos">
          <StackLayout v-for="(todo, i) in todos" :key="i" orientation="horizontal" class="todo">
            <Label class="content" :class="{ done: todo.isDone }" :text="todo.title" />
            <Switch class="check" v-model="todo.isDone" @checkedChange="doneTodo(todo)" />
          </StackLayout>
        </StackLayout>
        <StackLayout v-else id="empty-msg">
          <Label text="タスクがありません" />
        </StackLayout>
      </ScrollView>
      <GridLayout columns="*, auto" row="1">
        <TextField id="comment-input" col="0" v-model="content" hint="タスクを入力" />
        <Button id="submit" col="1" text="追加" @tap="add()" />
      </GridLayout>
    </GridLayout>
  </Page>
</template>
```

### ロジック部分

addでタスクの登録処理、doneTodoでタスクの更新処理、fetchTodosでタスクの一覧を取得しています。
amplifyのmutationsが登録、更新処理。queriesが取得処理です。

```vue:<app-name>/app/components/App.vue
<script>
import API, { graphqlOperation } from '@aws-amplify/api';
import { createTodo, updateTodo } from '@/graphql/mutations';
import { listTodos } from '@/graphql/queries';

export default {
  data() {
    return {
      todos: [],
      content: ''
    }
  },
  created() {
    this.fetchTodos()
  },
  methods: {
    generateId () {
      return this.todos.length
    },
    async add () {
      let todo = {
        id: this.generateId(),
        title: this.content,
        isDone: false
      }
      try {
        await API.graphql(graphqlOperation(createTodo, { input: todo }))
      } catch (error) {
        console.error(error)
      } finally {
        this.resetInput()
        this.fetchTodos()
      }
    },
    async fetchTodos () {
      try {
        const res = await API.graphql(graphqlOperation(listTodos, { limit: 100 }))
        this.todos.push(...res.data.listTodos.items)
      } catch (error) {
        console.error(error)
      }
    },
    async doneTodo (todo) {
      try {
        const res = await API.graphql(graphqlOperation(updateTodo, {
          input: todo
        }))
      } catch (error) {
        console.error(error)
      }
    },
    resetInput () {
      this.content = ''
    }
  }
}
</script>
```


<details><summary>ソースコード全体像</summary>
<div>

```vue:<app-name>/app/components/App.vue
<template>
  <Page>
    <ActionBar title="Todo" />
    <GridLayout columns="*" rows="*, auto">
      <ScrollView class="todo-area" row="0">
        <StackLayout v-if="todos.length > 0" class="todos">
          <StackLayout v-for="(todo, i) in todos" :key="i" orientation="horizontal" class="todo">
            <Label class="content" :class="{ done: todo.isDone }" :text="todo.title" />
            <Switch class="check" v-model="todo.isDone" @checkedChange="doneTodo(todo)" />
          </StackLayout>
        </StackLayout>
        <StackLayout v-else id="empty-msg">
          <Label text="タスクがありません" />
        </StackLayout>
      </ScrollView>
      <GridLayout columns="*, auto" row="1">
        <TextField id="comment-input" col="0" v-model="content" hint="タスクを入力" />
        <Button id="submit" col="1" text="追加" @tap="add()" />
      </GridLayout>
    </GridLayout>
  </Page>
</template>

<script>
import API, { graphqlOperation } from '@aws-amplify/api';
import { createTodo, updateTodo } from '@/graphql/mutations';
import { listTodos } from '@/graphql/queries';

export default {
  data() {
    return {
      todos: [],
      content: ''
    }
  },
  created() {
    this.fetchTodos()
  },
  methods: {
    generateId () {
      return this.todos.length
    },
    async add () {
      let todo = {
        id: this.generateId(),
        title: this.content,
        isDone: false
      }
      try {
        await API.graphql(graphqlOperation(createTodo, { input: todo }))
      } catch (error) {
        console.error(error)
      } finally {
        this.resetInput()
        this.fetchTodos()
      }
    },
    async fetchTodos () {
      try {
        const res = await API.graphql(graphqlOperation(listTodos, { limit: 100 }))
        this.todos.push(...res.data.listTodos.items)
      } catch (error) {
        console.error(error)
      }
    },
    async doneTodo (todo) {
      try {
        const res = await API.graphql(graphqlOperation(updateTodo, {
          input: todo
        }))
      } catch (error) {
        console.error(error)
      }
    },
    resetInput () {
      this.content = ''
    }
  }
}
</script>

<style scoped>
ActionBar {
  color: white;
}

.todo-area {
  background: #E5E5E5;
  width: 100%;
  height: 100%;
}

.todos {
  height: 100%;
  margin: 8px;
  border-right: 1 solid #ddd;
  border-left: 1 solid #ddd;
  background-color: #E5E5E5;
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2) inset;
}

.todo {
  padding: 4px;
}

.content {
  font-size: 20px;
  width: 80%;
}

.done {
  color: gray;
  text-decoration: line-through;
}

.check {
  width: 20%;
}

#empty-msg {
  text-align: center;
  font-size: 16px;
  margin-top: 40px;
}


#submit {
  margin: 0 auto;
}

#comment-input {
  border-width: 1;
  border-color: #2E2E2E;
  border-radius: 8px;
  height: 100px;
}
</style>
```

</div>
</details>

## アプリの起動（シミュレータ）

```bash
tns run ios --debug --device 'iPhone 11 Pro'
```

## おわりに
本来であれば、バックエンドのAPIを構築してデータベースと連携してと実装が、大変なイメージですがAWS Amplifyを使うと簡単にバックエンドが実装できます。

また、アプリもNativeScript-Vueを使えばVue.jsの知識で作れてしまいます。

Vue.jsでアプリ作りたい方、AWS Amplify試してみたい方、NativeScript-Vue + AWS Amplify、是非試してみてください！