Nuxt.js + TypeScript + Composition APIで作るSPA

これまでVue.jsを使うことが何度かあったが、Vue.jsのフレームワークとしてよく聞く「Nuxt.js」を使ったことがなかったため、入門として触ってみた話をまとめる。

# 今回作るもの
 `create-nuxt-app` で作成されるサンプルをベースに、とても単純なテキストフォームアプリを作成する。
 
 https://github.com/nuxt/create-nuxt-app
 
言語はTypescriptにし、また、Vue.jsでは主流となりつつあるComposition APIも合わせて使ってみる。

レンダリングについては、静的ホスティングを想定してSPAモードを選択する。
https://nuxtjs.org/ja/docs/concepts/static-site-generation


# セットアップ
## バージョン
 - Node.js v14.17.1
 - Yarn: v1.22.17

## プロジェクトの作成
ドキュメントのインストール方法にある通りに `create-nuxt-app` を使ってプロジェクトを作成する。

ドキュメント: https://nuxtjs.org/ja/docs/get-started/installation

今回プロジェクト名は `nuxt-ts-practice`とする。

```bash
yarn create nuxt-app nuxt-ts-practice
```

作成時の各質問に対する回答。

```text
? Project name: nuxt-ts-practice
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: Vuetify.js
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
? What is your GitHub username? shintaro yamasaki
? Version control system: Git
```

今回はTypeScriptを使い、また、静的ホスティングすることを想定してSPAとする。

## TypeScriptの導入
 `create-nuxt-app`による作成時にTypeScriptを選択すると、必要なパッケージが一緒にインストールされる。
 
- `@nuxt/types`
- `@nuxt/typescript-build`

参考: https://typescript.nuxtjs.org/ja/guide/setup

また、Nuxt.jsの設定ファイルである `nuxt.config.js` の `buildModules` に `@nuxt/typescript-build` が自動的に追加される。

```js:nuxt.config.js
  buildModules: [
      ...
      '@nuxt/typescript-build'
  ]
```

この時点で各VueファイルではJavaScriptが使われているため、必要であれば適宜書き換える。

## Composition APIの導入
追加で、`@nuxtjs/composition-api` モジュールをインストールする。

```
yarn add @nuxtjs/composition-api
```

インストール後、 `nuxt.config.js`のbuildModulesに記述追加

```js:nuxt.config.js
  buildModules: [
      ...
      '@nuxtjs/composition-api/module'
  ]
```

参考: https://composition-api.nuxtjs.org/getting-started/setup

## layouts/default.vue のTypeScript + Composition API化
試しに `layouts/default.vue` のscript部を、TypeScript + Composition APIで書き換えてみる。

```vue:layouts/default.vue
<template>
....
</template>

<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api'

type Item = {
  icon: string
  title: string
  to: string
}

export default defineComponent({
    setup() {
        const clipped = ref(false)
        const drawer = ref(false)
        const fixed = ref(false)

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

        return {
            clipped,
            drawer,
            fixed,
            items,
            miniVariant,
            right,
            rightDrawer,
            title
        }
    }
})
</script>
```

## ローカルで実行
```bash
yarn dev
```

ポート番号はデフォルトが3000となっているが、これを変更 (例. 5000へ変更) する場合は、`package.json`のscriptsの記述を以下のように変更する

```json:package.json
"dev": "PORT=5000 nuxt",
```


# ページ追加
ページを自分で作成して追加する。

## ページの構成
Nuxt.jsのビューの構成は、以下の図ように一番の大枠としてHTMLファイル (ディレクトリ内にファイルとして存在せずコンパイル時に生成される) があり、その中に各ページがあるような構成となっている。また、各ページはレイアウトという、ページ間で流用可能な見た目に関するテンプレートによって括られる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241369/ac5edffd-d7fa-c629-0fe0-3c2735570eaa.png)

参考: https://nuxtjs.org/ja/docs/concepts/views

## レイアウトの追加
初期状態で用意されているデフォルトのレイアウト `layouts/default.vue` があるが、新しく専用のレイアウトを追加する。

```vue:layouts/note.vue
<template>
    <v-app flat>
        <v-main>
            <v-container>
                <div class="text-h2" >
                    Note
                </div>
                <Nuxt />
            </v-container>
        </v-main>
    </v-app>
</template>
```

シンプルに、タイトルのみが上部に表示されるだけ。

参考: https://nuxtjs.org/ja/docs/concepts/views#%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88 

## コンポーネントの追加
テキストを画面上に表示し、また、ボタンで削除するコンポーネントを作成する。

```vue:components/Note.vue
<template>
    <div>
        <v-card>
            <v-card-text>
                {{ text }}
            </v-card-text>
        </v-card>
        <br />
        <v-btn @click="onClear">
            clear
        </v-btn>
    </div>
</template>
<script lang="ts">
import { defineComponent, ref, SetupContext } from '@nuxtjs/composition-api'

type Props = {
    text: string,
};

export default defineComponent({
    props: {
        text: {
            type: String,
        }
    },

    setup(props: Props, context: SetupContext) {
        const title = ref<string>('Note');

        const onClear = () => {
            context.emit("clear");
        };

        return {
            title,
            onClear,
        };
    },
})
</script>
```

## ページの追加
作成したレイアウトとコンポーネントも使って、専用のページを作成する。

```vue:pages/note.vue
<template>
    <v-container fluid>
        <v-row>
            <v-col>
                <v-textarea
                    v-model="text"
                    hint="Text here"
                    ></v-textarea>
                <br />
                <v-btn @click="onSubmit">
                    Submit
                </v-btn>
            </v-col>
        </v-row>
        <v-row>
            <v-col>
                <Note
                    :text="shown_text"
                    @clear="onClear"
                    />
            </v-col>
        </v-row>
    </v-container>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, useContext} from '@nuxtjs/composition-api'

export default defineComponent({
    setup() {
        const text = ref<string>("")
        const shown_text = ref<string>("")

        const onSubmit = () => {
            console.log("onsubmit")
            shown_text.value = text.value
            text.value = ""
        }

        const onClear = () => {
            shown_text.value = ""
        }

        const { $config } = useContext()

        onMounted(() => {
            console.log('mounted!')
            console.log($config.stage)
        })

        return {
            text,
            shown_text,
            onSubmit,
            onClear,
        }
    },
    layout: 'note',
})
</script>
```

### Vuexの利用
`store` の実装は様々な方法があるが、Nuxtの場合に限らずTypeScriptとの相性の面で複雑化しやすい。

- `vuex-module-decorators` を使ったクラスベースの実装
- `nuxt-typed-vuex` を使った実装

参考: https://typescript.nuxtjs.org/ja/cookbook/store#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E3%82%BF%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0 

今回はVuexによる状態管理の実装は見送ることとする。

また、今回はNuxt2を利用しているが、次バージョンのNuxt3ではStateという機能が追加され、Vuexを使わず状態管理ができるようになる。

参考: https://v3.nuxtjs.org/docs/usage/state/


### パスを追加
`pages` ディレクトリ内のファイルパスがそのままルーティングになるため、別途どこかに記載する必要なし。

参考: https://nuxtjs.org/ja/docs/features/file-system-routing

## 環境変数の読み込み
今回は環境変数を使う場面はなかったが、環境変数の読み込み方についてもまとめる。

`nuxt.config.js` のRuntime configで読み込む。

https://nuxtjs.org/docs/directory-structure/nuxt-config#runtimeconfig

```js:nuxt.config.js
  publicRuntimeConfig: {
    "stage": process.env.STAGE   // STAGE環境変数
  },
```

vueファイルからの参照方法については、SetupContextは `$context` を要素として持っていないようなのでエラーとなる。以下のように`@nuxtjs/composition-api`のuseContextで取得する

```ts
const { $config } = useContext()
console.log($config.stage)
```

https://nuxtjs.org/docs/directory-structure/nuxt-config#using-your-config-values 
https://composition-api.nuxtjs.org/API/useContext

## できた画面
ページを追加して出来上がった画面 

ローカルで実行し、`/note`にアクセス。

![nuxt-ts-practice.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241369/897ce286-82f9-e66d-141c-ab94a1dc804c.gif)


テキストエリアに入力した文字列をSubimitすると下の領域に表示させるだけの簡単なアプリ。

# ソースコード
https://github.com/mmclsntr/nuxt-ts-practice

# 感想
Nuxt.jsが定めているディレクトリ構成とビューやルーティング等との関係性さえ理解すれば、あとは通常通りのVue.jsによる開発と同じ印象。

普段よくディレクトリ構成を悩むことが多いので、純粋にフレームワークを使ったWeb開発として、そのあたりが統一して考えることができて良い。

SSRの文脈でよくNuxt.jsが出てくることが多いが、SPAにも対応しており、フレームワークとしての効果は十分にあるため積極的に使っていきたいと感じた。

また、Composition APIについても初めて触ってみたが、小規模であったこともありあまり恩恵は感じられず。。ある程度触るとすぐ慣れそうなので今後はこっちを使って開発していきたい。

## Nuxt3について
今回はNuxt2を利用したが、執筆時現在Nuxt3がbeta版として公開されている。

https://v3.nuxtjs.org/ 

このバージョン3ではTypeScriptやComposition APIが標準対応しているためセットアップもより楽になりそう。