Nuxt で IT イベント検索アプリを作成した

## はじめに

Nuxt を勉強中です
一応[前回](https://qiita.com/kurosame/items/80437d5387cb253f2c1e)の続きですが、前回はほぼ環境構築で終わってしまったので、今回は簡単なアプリを作成しました

[ホスティング先]
https://event-search.netlify.com

[GitHub]
https://github.com/kurosame/event-search

簡単に説明すると、IT 系のイベントサイトからイベント一覧を取得する API を叩いて、取得したものをテーブルに表示しているだけのアプリです
まだあまり使い物にならないですが、イベントサイトを増やしたり、検索機能などを使いやすくしたり、CI で定期的に取得して Slack に通知するなどしたら普通に使えそうなので、もう少しちゃんと作ろうと思います

現在は、以下の縛りがあります

- connpass と ATND のみ対応
- 定員が 30 人以上のイベントに限定
- 本日から来月末までの期間のイベントを取得

開発してて、いくつか気になった点をピックアップして書こうと思います

## layouts, pages, components の違いについて

コンポーネントを作る際にディレクトリ構成を眺めると、Vue ファイルを置くディレクトリが 3 つある
pages, components, layouts のこれらの概念を先に理解する必要がある

pages はルーティングで呼ばれるコンポーネントを配置する
components は pages コンポーネントから呼ばれるコンポーネントを配置する

layouts については、[この図](https://ja.nuxtjs.org/guide/views#layouts)が分かりやすい

layouts は pages よりも 1 つ上のレイヤーで外観（ヘッダーやフッターなど）が異なるページが必要な時、それぞれの外観をコンポーネントとして作成し、layouts 配下に置いておけば、pages コンポーネントから layout オプションで好きな外観コンポーネントを指定できる

layouts はとても便利な機能だと思う
Nuxt を使ってない時はログイン画面などヘッダーやフッターが必要ない画面は`App.vue`で`v-if`してた

以下は簡単な layouts コンポーネントを作成した

```vue:layouts/default.vue
<template>
  <v-app dark>
    <v-container>
      <nuxt />
    </v-container>
  </v-app>
</template>
```

`<nuxt />`は layouts コンポーネントでのみ許される機能で pages コンポーネントを呼び出すことができる
ちゃんと調べてないけど`<router-view />`みたいなものだと思う

また、`default.vue`という名前で作成しているので、pages コンポーネントで layout オプションを指定していなければ、この layouts コンポーネントが呼ばれる

任意の layouts コンポーネントを使う場合は、default 以外の名前で Vue ファイルを作成し、pages コンポーネントで layout オプションを指定すれば良い

```vue:layouts/sample.vue
<template>
  <v-app>
    <v-container>
      <nuxt />
    </v-container>
  </v-app>
</template>
```

```vue:pages/index.vue
<script>
export default {
  layout: 'sample'
}
</script>
```

## plugins の使い方

Nuxt を`create-nuxt-app`でインストールする際に、UI フレームワークで Vuetify を選択したので、サンプルコードのコンポーネントが Vuetify で作られた
そのコンポーネントを見ると`Vue.use(Vuetify)`をしていないので、どこかでグローバルレベルで使用できるようにしているはずである
それを行っているのが plugins である

Vue で外部モジュールを使う場合、`new Vue(...)`する前に`Vue.use(...)`する必要がある
plugins ディレクトリ内に JS ファイルを入れておくと、Vue のインスタンスが作られる前に実行されるので、その JS で`Vue.use(...)`しておけば良い

また、`nuxt.config.js`に以下の設定が必要
（`create-nuxt-app`で Vuetify をインストールした場合は、既に設定済み）

```js:nuxt.config.js
module.exports = {
  plugins: ['@/plugins/vuetify']
}
```

`@`は webpack などでよく使われるエイリアスでデフォルトは`process.cwd()`が指定されている

## axios の使い方

`create-nuxt-app`で Axios を選択していれば、axios を拡張した[axios-module](https://github.com/nuxt-community/axios-module)が利用可能になっている
Axios を選択していなくても、以下で使えるようになると思う

```
yarn add -D @nuxtjs/axios
```

```js:nuxt.config.js
module.exports = {
  modules: ['@nuxtjs/axios']
}
```

Vuex での簡単な使い方

```js
export const actions = {
  async getApi({ commit }) {
    const res = await this.$axios.$get('http://...')
  }
}
```

### 別ドメインからリソースを取得する場合の CORS 対策について

以下のように別ドメインの API を叩くようなケースがあった場合、CORS ポリシーによりブロックされる

```js
const res = await this.$axios.$get('https://connpass.com/api/v1/event', {
  params: {
    keyword: 'python'
  }
})
```

今回は API 側にオリジンを許可させることはできない
[Nuxt での解決策](https://ja.nuxtjs.org/faq/http-proxy)としては、axios の proxy 機能を使うのが良いらしい

```
yarn add -D @nuxtjs/proxy
```

```js:nuxt.config.js
module.exports = {
  modules: ['@nuxtjs/axios', '@nuxtjs/proxy'],
  proxy: {
    '/connpass': {
      target: 'https://connpass.com/api/v1/event',
      pathRewrite: {
        '^/connpass': '/'
      }
    }
  }
}
```

リクエストをプロキシして API と同一ドメインで叩けるようにしている

### 静的サイトの場合、proxy が機能しない

`nuxt generate`で Netlify などにホスティングした場合、proxy が機能しない
ちゃんと GitHub の[README](https://github.com/nuxt-community/proxy-module#features)に「Does not work in generated/static mode!」って書いてあった

今回は Netlify Functions を利用して、この問題を回避した
[Netlify Functions を使って CORS エラーを回避する](https://qiita.com/kurosame/items/7b5fd3cbf1f688b47e88)

## Vuex を型安全にしたい

Nuxt に限ったことではないが、例えば Vue 内の`this.$store`は型定義が`Store<any>`なので、中身の State は any 型である
型安全にしたいと思い、「vuex typescript」などでググるといろんなやり方が出てくる
正直スタンダードなやり方は分からなかった

今回作るアプリは小規模ということもあり、一旦 Vue 内にヘルパー関数を実装し、Mixin させることにした
（後でいいやり方が見つかれば、簡単に捨てれるので）

目標としては`Store<state1 | state2>`のような型を付けて、パラメータで渡したモジュール名の State を返してくれるヘルパー関数を作成する

以下のように各 State をまとめた RootState を定義
events と condition というモジュールの State があるとする

```js:store/index.ts
import { IEventsState } from '@/store/events'
import { IConditionState } from '@/store/condition'

export interface IState {
  events: IEventsState
  condition: IConditionState
}
```

ヘルパー関数を実装した StoreHelper クラスを作成する

```vue:components/mixins/StoreHelper.vue
<script lang="ts">
import { IState } from '@/store/index'
import { Component, Vue } from 'vue-property-decorator'

@Component
class StoreHelper extends Vue {
  getState<K extends keyof IState>(key: K): IState[K] {
    return this.$store.state[key]
  }
}

export default StoreHelper
</script>
```

State を参照したい Vue で StoreHelper を Mixin して、`getState('モジュール名')`を呼び出す

```vue
<script lang="ts">
import StoreHelper from '@/components/mixins/StoreHelper.vue'
import { Component, Mixins } from 'vue-property-decorator'

@Component
class Summary extends Mixins(StoreHelper) {
  get items(): [] {
    return this.getState('events').events
  }
}

export default Summary
</script>
```

VSCode 等で試すといい感じに補完してくれると思う

## SCSS を使う

```
yarn add -D node-sass sass-loader
```

```vue:Sample.vue
<style lang="scss">
...
</style>
```

## ホスティングする

Netlify でやるとかなり簡単
ローカルで`yarn generate`(`nuxt generate`)が正常に動いていれば、Netlify 側で以下の設定をすれば良い

<img width="565" alt=" 2019-06-14 17.49.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168197/eea6a52b-1233-3297-c64b-9d16eb1c061b.png">

## さいごに

まだ触れてない`nuxt/middleware`とかサーバー周りとか PWA とかはまた次回何か作ろうと思います