Nuxt.js + TypeScript + CompositionAPI + PWAの構成をAWS Amplifyにデプロイする

## はじめに

PWAのアプリを、Nuxt.jsとTypeScriptで構築し、
コンポーネントをCompositionAPIを使った構成を、
AWS Amplifyにデプロイしたいと思います。

## 実行環境のバージョン
node.js : 12.13.1
create-nuxt-app : 2.12.0

## 実装
### プロジェクトの作成
Nuxt.jsのプロジェクトを作成する。

```bash
$ npm install -g create-nuxt-app
$ create-nuxt-app nuxt-pwa-for-composition　　# nuxt-pwa-for-compositionはプロジェクト名
```

対話形式で作成するNuxt.jsの構成を入力します。
この記事では、構成は以下のようにしました。

```
? Project name _ # デフォルトのままEnter 
? Project description _ # デフォルトのままEnter 
? Author name _ # デフォルトのままEnter 
? Choose the package manager Npm
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support, DotEnv
? Choose linting tools ESLint, Prettier, StyleLint
? Choose test framework Jest
? Choose rendering mode Single Page App
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to i
nvert selection)
```

最低でも、`Choose Nuxt.js modules`に`Progressive Web App (PWA) Support`を入れてください。

``` bash
$ cd nuxt-pwa-for-composition
$ npm run dev
```

```
ERROR in No rules found within configuration. Have you provided a "rules" property?
```

このようなエラーが出ました。
`stylelint.config.js`に`rules`が記述されていないとのことなので追加します。

```js:stylelint.config.js
module.exports = {
  rules: {
    'at-rule-no-unknown': [
      true,
      {
        ignoreAtRules: ['extends', 'ignores']
      }
    ],
    indentation: 2,
    'number-leading-zero': null,
    'unit-whitelist': ['px', 'deg', 'em', 'rem', 's']
  }
}

```

再び起動

``` bash
$ npm run dev
```

今回は問題なく、実行できたかと思います。
ポートが使われていないのであれば`http://localhost:3000`でページが表示されるはずです。

### TypeScriptの反映
このガイドの内容を中心に構築します。
https://typescript.nuxtjs.org/guide/

必要なライブラリをインストールします。

```
$ npm install --save-dev @nuxt/typescript-build @nuxtjs/eslint-config-typescript
$ npm install @nuxt/typescript-runtime
```

次のファイルを更新もしくは追加します。

```diff:nuxt.config.js
    buildModules: [
      // Doc: https://github.com/nuxt-community/eslint-module
      '@nuxtjs/eslint-module',
      // Doc: https://github.com/nuxt-community/stylelint-module
      '@nuxtjs/stylelint-module',
-     '@nuxtjs/vuetify'
+     '@nuxtjs/vuetify',
+     '@nuxt/typescript-build'
    ],

  ・・・

    build: {
      /*
       ** You can extend webpack config here
       */
-     extend(config, ctx) {}
+     extend() {}
    }
  }
```

```diff:package.json
  "scripts": {
-   "dev": "nuxt",
+   "dev": "nuxt-ts",
-   "build": "nuxt build",
+   "build": "nuxt-ts build",
-   "start": "nuxt start",
+   "start": "nuxt-ts start",
-   "generate": "nuxt generate",
+   "generate": "nuxt-ts generate",
-   "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
+   "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
    "test": "jest"
  },
```

```diff:.eslintrc.js
  parserOptions: {
-   parser: 'babel-eslint'
  },

  ・・・

  extends: [
    '@nuxtjs',
+   '@nuxtjs/eslint-config-typescript',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended'
  ],
```

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types",
      "vuetify"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}
```

### Composition APIの適用

コンポーネントには、Vue 3で採用される予定のComposition APIを採用しようと思います。
Composition APIの説明はこの記事では行いません。
ドキュメントは[こちら](https://vue-composition-api-rfc.netlify.com)
投稿時点では、Class APIの情報の方が多い気がするので、
Class APIを採用しても良いとは思います。

```bash
$ npm install --save @vue/composition-api
```

```js:plugins/composition-api.js
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)
```

generateされた`.vue`ファイルがいくつかあるので、
そのファイルをComposition APIの記述に変えていきます。
※バージョンアップによりtemplateが変更になるのであれば、
こちらの記述では動作しなくなる可能性があります。

```vue:components/Logo.vue(scriptタグのみ抜粋)
<script lang="ts">
import { createComponent } from '@vue/composition-api'

export default createComponent({})
</script>
```

```vue:components/VuetifyLogo.vue(scriptタグのみ抜粋)
<script lang="ts">
import { createComponent } from '@vue/composition-api'

export default createComponent({})
</script>
```

```vue:layouts/default.vue(scriptタグのみ抜粋)
<script lang="ts">
import { createComponent, ref } from '@vue/composition-api'

interface Item {
  icon: string
  title: string
  to: string
}

const useMenus = () => {
  const clipped = ref<boolean>(false)
  const drawer = ref<boolean>(false)
  const fixed = ref<boolean>(false)
  const items = ref<Item[]>([
    {
      icon: 'mdi-apps',
      title: 'Welcome',
      to: '/'
    },
    {
      icon: 'mdi-chart-bubble',
      title: 'Inspire',
      to: '/inspire'
    }
  ])
  const miniVariant = ref<boolean>(false)
  const right = ref<boolean>(false)
  const rightDrawer = ref<boolean>(false)

  return {
    clipped,
    drawer,
    fixed,
    items,
    miniVariant,
    right,
    rightDrawer
  }
}

export default createComponent({
  setup() {
    const menus = useMenus()
    const title = ref<string>('Vuetify.js')

    return {
      ...menus,
      title
    }
  }
})
</script>
```

```vue:layouts/error.vue(scriptタグのみ抜粋)
<script lang="ts">
import { createComponent, ref } from '@vue/composition-api'

export default createComponent({
  layout: 'empty',
  props: {
    error: {
      type: Object,
      default: null
    }
  },
  head() {
    const title =
      (this as any).error.statusCode === 404
        ? (this as any).pageNotFound
        : (this as any).otherError
    return {
      title
    }
  },
  setup() {
    const pageNotFound = ref<string>('404 Not Found')
    const otherError = ref<string>('An error occurred')
    return {
      pageNotFound,
      otherError
    }
  }
})
</script>
```

```vue:pages/index.vue(scriptタグのみ抜粋)
<script lang="ts">
import { createComponent } from '@vue/composition-api'

import Logo from '~/components/Logo.vue'
import VuetifyLogo from '~/components/VuetifyLogo.vue'

export default createComponent({
  components: {
    Logo,
    VuetifyLogo
  }
})
</script>
```

### jsファイルをtsファイルにする

```diff:nuxt.config.js->nuxt.config.ts
+ import { Configuration } from '@nuxt/types'
  import colors from 'vuetify/es5/util/colors'


- export default {
+ const nuxtConfig: Configuration = {
    mode: 'spa',

    ・・・

    build: {
      /*
       ** You can extend webpack config here
       */
      extend() {}
    }
  }
+ export default nuxtConfig
```

```ts:plugins/composition-api.js->plugins/composition-api.ts
```

### PWAの修正

アプリアイコンとファビコンを変更する。

- static/favicon.ico
- static/icon.png

余談ですが、とあるカンファレンスのWebページがPWA対応になっていましたが、
アプリアイコンは変えていないようで、Nuxtのアイコンが表示されていました。

### GitHubにコミット
以上の内容をGitHubにコミットします。
リポジトリはprivateリポジトリでも問題ありません。

### AWS Amplifyの準備
AWSコンソールからAWS Amplifyを選択します。
<img width="1440" alt="スクリーンショット 2019-12-02 0.24.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/4200a607-b3f6-2dc8-4cd4-0d5e4cff8aba.png">

「Deploy With the Amplify Console」の「GET STARTED」を選択します。
<img width="1440" alt="スクリーンショット 2019-12-08 17.13.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/307efeaa-35ab-805e-bc45-f64a612015c4.png">

GitHub、BitBucket、GitLab、AWS CodeCommitのGitリポジトリを使うことができるようです。
今回はGitHubにpushしましたので、GitHubを選択します。
<img width="1440" alt="スクリーンショット 2019-12-08 17.34.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/6619f971-b0cf-8da7-8030-a71b034b683e.png">

GitHubから承認を求められるので許可します。
<img width="1440" alt="スクリーンショット 2019-12-08 17.38.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/1bb89a37-369f-1257-2c4e-83fc1276d66b.png">

リポジトリとブランチを選択します。
<img width="1440" alt="スクリーンショット 2019-12-08 17.56.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/4de9bc0a-a08b-7a98-2cf0-aceb16a14aaf.png">

ビルド設定の構成で、アプリ名を変更してください。
また、frontend.artifacts.baseDirectoryの項目が`/`になっているようなら、`/dist/`に変更してください。
<img width="1440" alt="スクリーンショット 2019-12-09 15.08.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/7dcfe9da-48d8-f38c-0146-de411c0367a3.png">

特に問題内容でしたら、「保存してデプロイ」ボタンを押します。
<img width="1440" alt="スクリーンショット 2019-12-08 19.08.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/12e008a7-7248-cae6-6352-0e80357a3ed0.png">

デプロイが完了するまでしばらく時間がかかると思います。
<img width="1440" alt="スクリーンショット 2019-12-08 19.09.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/e27b3697-ccd5-8d93-4908-71370b75f0d6.png">

「検証」までチェックが入ったら完了です。
URLのリンクをクリックし、ちゃんとデプロイできているかを確認します。
<img width="1440" alt="スクリーンショット 2019-12-09 15.15.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/e8cfea51-8299-3ae6-4194-60a7afc4c501.png">

<img width="1440" alt="スクリーンショット 2019-12-09 15.15.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/1b92476d-eccd-b584-ebf9-88ae3ca0b906.png">

Vuetify + Nuxt.jsのtemplateが表示されれば成功です。
<img width="1440" alt="スクリーンショット 2019-12-09 15.16.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/2e0a4792-1111-64de-9e19-803289c6fdd4.png">

Android Chrome等でデプロイされたURLを開きます。
<img width="50%" alt="Screenshot_20191209-153620.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/b01eaf2c-0a4c-f4e0-1a11-48aacc8a425c.png">

ホームに追加をタップする。

<img width="50%" alt="Screenshot_20191209-154050.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/e7b2bfd2-2249-2592-b5ea-2e041b305616.png">

アプリとして表示できれば成功です。

## まとめ
今回は、PWAアプリをNuxt.js + TypeScriptで構築する方法と
作成したアプリをAmplify Consoleでデプロイする方法を紹介しました。
SPAをホスティングサービスは多々ありますが、
Amplify ConsoleをCI/CDの管理を気にせずにデプロイすることができるようになるのが大きな利点だと思います。
ただ、今回のサンプルのようにただのホスティングサービスとしてしか使わないと、Amplifyをいかせてはいないとは思います。
また、AWSの資源（カスタムドメイン設定など）を活用しやすいのも利点としてあげれるかと思います。
デプロイ先の一つの候補としてAmplifyを考えてみてはいかがでしょうか。

今回の完成形のリポジトリは[こちら](https://github.com/fiotto/nuxt-pwa-for-composition)