Nuxt.js + typescriptの新規開発案件で得た知見〜アーキテクチャ（Atomic Designとか）～

# コンポーネント設計の知見


# デフォルトのcreate-nuxt-appで作成されるフォルダ構成と特に違いが出る箇所
- `mock`
- `components`
- `containers`
- `store`
- `types`

# まずは、ローカルだけで動作確認できるように`mock`を作る
- こちらはローカルで動作確認を行うためのモックの置き場。詳しくは[Nuxt.js + Expressでローカルサーバ構築](https://qiita.com/sukezane/items/5622924a7523ddb710c6)

# `components`(コンポーネントの設計)
- まずコンポーネント責務によって`Presentational Components`と`Container Components`へ分ける

### <font color="red">`Presentational Components` 
- 見た目に対する部分

###  `Container Components`
- 処理に対する部分


### <font color="bluck">表で表現すると</font>

|                    |Presentational Components           |Container Components              |
|--------------------|------------------------------------|----------------------------------|
|目的                |見た目（マークアップ, スタイル）    |動作（データの取得、状態の更新）  |
|Store へのアクセス  |できない                            |できる                            |
|データの読込        |Props                               |Vuex の State の Getter を呼ぶ    |
|データの変更        |Props から取得した Callback を呼ぶ  |Vuex の Action を Dispatch する   |

### コンポーネントの粒度

|Category　　　　　　   |Directory           |State 　　　　　      |Storeへのアクセス     |責務  &nbsp;&nbsp;      |
|-----------|------------------------|----------|----------|-------------|
|Atoms      |src/components/atoms    |持たない     |NG        | Presentational 　Component|
|Molecules  |src/components/molecules|持つ        |NG        |Presentational 　Component|
|Organisms  |src/components/organisms|持つ        |NG        |Presentational 　Component|
|Templates  |src/layouts             |持つ        |OK        |Container <br>Component     |
|Pages      |src/pages               |持つ        |OK        |Container <br>Component     |


- ここで使用している設計技術として<font color="red">Atomic Design</font>というものがある。
- `Atomic Design`...責務を明確にしてレベルに分けてデザインする。
  - **Lv1**：`Atoms`（原子）←最小の単位
  - **Lv2**：`Molecules`（分子）
  - **Lv3**：`Organisms`（生体）
  - **Lv4**：`Templates`テンプレート
  - **Lv5**：`Pages`ページ

- 参考：[Atomic Design を分かったつもりになる](https://design.dena.com/design/atomic-design-%E3%82%92%E5%88%86%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%A4%E3%82%82%E3%82%8A%E3%81%AB%E3%81%AA%E3%82%8B/)

# 振る舞いの共通化
- `Presentational Components`内でAPI呼び出しのためのstoreへのアクセスをしてしまうと`Container Component`の役割も担うことになってしまいメンテナンスがしにくい。
- そんな場合は`対象のAPI呼び出し専用のContainer Component`を作成し、それを親コンポーネントとして配置することでイベントとプロパティの受け渡しが可能になる。

- 例）検索用ダイアログで動的に検索処理を走らせたい場面
  - ダイアログとしての見た目（Presentational）と検索処理（Container）を分ける

- こんなことがしたい時があると思います
![検索ダイアログ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303093/ea328e3f-c499-a544-30df-19c46b41aa27.gif)

- こんな時は**<font color="Salmon">画面の描画部分とAPIコール(stateへのset）部分を分ける。</font>**


### APIコール部分のContainer コンポーネントを用意
```vuejs:src/containers/search-container.vue

<template>
  <div>
    <slot :search="search" :result="result"></slot> 　//①
  </div>
</template>

<script lang="ts">
import { Component, Getter, Vue } from 'nuxt-property-decorator'
import { ClientFilter } from '~/store/domain/clients/actions'
import { Client, PagedResources, PageRequest } from '~/types'

@Component({})
export default class Search extends Vue {
  @Getter('domain/clients/search') result: PagedResources<Client[]> //②

  async search(filter: ClientFilter, pageRequest: PageRequest) {
    await this.$store.dispatch('domain/clients/search', { //③
      filter,
      pageRequest
    })
  }
}
</script>

```

- ①.<slot> 要素を使用して子コンポーネントにイベントとプロパティを受け渡しができるようにする
- ②.検索結果を取得するgetter
- ③.検索 API を呼び出すためのアクションをディスパッチする

### ダイアログコンポーネント（Presentational）
- こちらは見た目の部分なので検索イベントを Emit するだけ、結果は Prop から受け取るようにする。

```vue:src/components/organisms/dialog.vue
<template>
  <el-dialog
    title="得意先選択"
    :visible.sync="visible"
    width="50%"
    :before-close="handleClose"
  >
    <client-search-form
      :query="query"
      @search="search"
    ></client-search-form>
    <client-list
      v-if="resources"
      :clients="resources._embedded.results"
      :page-metadata="resources.page"
      :selectable="true"
      @select="handleSelect"
      @current-change="handlePageChange"
      @size-change="handleSizeChange"
    />
    <span slot="footer" class="dialog-footer">
      <el-button @click="hide">閉じる</el-button>
    </span>
  </el-dialog>
</template>

<script lang="ts">
import { Component, Prop, Vue, Watch } from 'nuxt-property-decorator'
import { ComponentOptions } from 'vue'
import ClientSearchForm from '~/components/organisms/client-search-form/client-search-form.vue'
import ClientList from '~/components/organisms/client-list/client-list.vue'
import { ClientFilter } from '~/store/domain/clients/actions'
import { Client, PagedResources, PageRequest } from '~/types'

@Component({
  components: {
    ClientSearchForm,
    ClientList
  }
} as ComponentOptions<Vue>)
export default class ClientSelectDialog extends Vue {
  /**
   * 表示フラグ
   */
  @Prop()
  visible

  /**
   * 検索結果
   */
  @Prop()
  result: PagedResources<Client[]> //①

  resources = this.result

  query = {
    keyword: ''
  } as ClientFilter

  page = 0
  size = 10

  search(form: ClientFilter) {
    /**
     * 検索イベント
     * @type {object}
     */
    // eslint-disable-next-line
    this.$emit('search', form, { //②
      page: 0
    } as PageRequest)
  }

  hide() {
    this.query = {
      keyword: ''
    } as ClientFilter
    /**
     * 表示切り替えイベント
     * @type {object}
     */
    this.$emit('update:visible', false)
  }

  handleSelect(client: Client) {
    this.hide()
    /**
     * 選択イベント
     * @type {object}
     */
    this.$emit('select', client)
  }

  handlePageChange(page: number) {
    this.page = page - 1
    this.$emit('search', this.query, {
      page: this.page,
      size: this.size
    } as PageRequest)
  }

  handleSizeChange(size: number) {
    this.size = size
    this.$emit('search', this.query, {
      page: this.page,
      size: this.size
    } as PageRequest)
  }

  @Watch('visible')
  handleVisible() {
    this.resources = null
  }

  @Watch('result')
  handleResultChange() {
    this.resources = this.result
  }

  handleClose() {
    this.hide()
  }
}
</script>
```

- ①.検索結果がここに入ってくる
- ②.Containerへ検索APIのコールをしている（正しくは`お父さん、検索APIをコールしてね`というメソッド）

# 使いたい場所へ配置する

- 検索ダイアログを配置するとしたら何かのフォーム部品とかになるかと思うのでそんなイメージ
- コンテナでラップする
- v-slot で定義した名前を利用して親コンポーネントからスロットプパティを受け取る
 
```javascript:src/components/organisms/なんとかform.vue
<search-container v-slot="scope">
  
  <dialog
    :visible.sync="clientSelectDialogVisible"
    :result="scope.result"
    @search="scope.search"
    @select="handleClientSelect"
  ></dialog>
</search-container>
```

# storeの構成

- 今回はバックエンドが`静的型付け言語`だったのでtypescriptを使用している
- storeにフォーカスしてフォルダ構成を載せてみる

```
├─store
│  │  ├─各store//それぞれの責務でファイルを分ける
│  │  │    └─action.ts
│  │  │    └─mutation.ts
│  │  │    └─index.ts
│  │  │    └─getter.ts
│  │  ├─modules
│  │  └─index.ts //まとめてexport

```

- このように`action`、`mutation`、`getter`をそれぞれ分割してindex.tsでまとめて`export`している
- 中身はそれぞれ以下のようにしている（ソースコード自体はVueやNuxtの経験者の方ならご理解頂けると思うので解説はしていません）
- 登場する`RootState`,`PageRequest`はtypes部分で紹介

```javascript:各モジュール/actions.ts
import { ActionTree } from 'vuex'
import { hogeTransfer } from '~/types/hoge'
import { hogeState } from '~/types/hoge/state'
import { RootState } from '~/types/state'
import '@nuxtjs/axios'
import { PageRequest } from '~/types/util'

//初期表示情報
const actions: ActionTree<hogeState, RootState> = {
  async describe({ commit }) {
    const response = await this.$axios.$post<hogeTransfer>(
      `/hoge/describe-myorg`
    )
    commit('myorg', { myorg: response })
  }
}

export default actions

```

```javascript:各モジュール/mutations.ts
import { MutationTree } from 'vuex'
import { hogeState } from '~/types/hoge/state'

const mutations: MutationTree<hogeState> = {
  myorg(state, { myorg }) {
    state.myorg = myorg
  }
}

export default mutations

```

```javascript:各モジュール/getters.ts
import { GetterTree } from 'vuex'
import { hogeState } from '~/types/hoge/state'
import { RootState } from '~/types/state'

const getters: GetterTree<hogeState, RootState> = {
  myorg: state => state.myorg,
}

export default getters

```

```javascript:各モジュール/index.ts
import { Module } from 'vuex'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
import { hogeState } from '~/types/hoge/state'
import { RootState } from '~/types/state'

const state: hogeState = {
  myorg: null,
}

const hoge: Module<hogeState, RootState> = {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

export default hoge

```

- あとは大元のindex.tsでmodulesに設定

```javascript:index.ts
import Vuex, { StoreOptions } from 'vuex'
import { RootState } from '~/types/state'
import hoge from '~/store/hoge'

const storeOptions: StoreOptions<RootState> = {
  modules: {
    hoge
  }
}

const store = () => new Vuex.Store<RootState>(storeOptions)
export default store

```
- こうすることで`hoge.$store`でstoreにアクセス可能になる

# types

- storeの時と同じようにフォルダ構成をおさらい

```
│  └─types
│      ├─各types
│      └─util
```

- `util`内は以下のようになってます

```
.
├── AggregateRoot.ts
├── PageMetadata.ts
├── PageRequest.ts
├── PagedResources.ts
└── index.ts

```

- こちらはバージョンはページ情報など主にメタデータを管理しています。
- こちらのソースコードは先頭にも掲載していますがgithub:[nuxt-typscript]()で記載

- あとは`types`直下へ機能（コンポーネント）ごとにフォルダを切って扱うオブジェクト単位で型を定義。
- 例としてマイページで使用する自分の所属する組織情報の型を`MyOrgType`として定義してみると。。。

```javascript:types/mypage/MyOrgType.ts
import { AggregateRoot } from '~/types/util'

export interface MyOrgType extends AggregateRoot {
  header: string
  name: string
  id: string
  position: string
  items: string[]
}

```

- index.tsを作成します。

```javascript:types/mypage/index.ts
import { MyOrgType } from './MyOrgType'

export {
  MyOrgTransfer,
  //扱うオブジェクトが増えるたびに書き足していく
}

```

- state.tsを作成します

```javascript:types/mypage/state.ts
import {
  MyOrgType,
} from '~/types/mypage'

export interface DomainData {
  mypage: MypageState
}

export interface MypageState {
  myorg: MyOrgTransfer
  //何かの履歴を表示するコンポーネント用の型とかが入ってくる想定
}

```

- こうすることでstoreの`getter`や`mutations`でstateを取得したり書き換えたりすることができます。

### 一旦以上とします。(2019/10/19）
- まだ参画して3週間なのでその都度更新していきます。
- アーキテクチャ編と書いたものの、他の`編`を書くかは検討中ww