【Vuetify on Nuxt.js】実際に本番運用したコンポーネントとそのカスタマイズ

# 概要

本スライドは[Vuetify Meetup #1](https://vuetify-meetup.connpass.com/event/141677/)での発表資料です。面白そうと思った方はぜひ次回以降参加してください！LTも募集中です。

----

# 自己紹介

----

### 業務内容
Twitter [@Meijin_garden](https://twitter.com/Meijin_garden)
名人って呼ばれてます。

株式会社[NoSchool](https://noschool.asia)のCTO。
中高生のための勉強質問サイトを作ってます。
自分しかフルタイムのエンジニアがいないのでAWSもFirebaseもLaravelもNuxt.jsもデザインも採用も機械学習もやってます。

----

### 経歴
奈良高専情報工学科
↓
株式会社LIFULL
↓
株式会社NoSchool

----

# Vuetify on Nuxt.jsを採用した理由

----

### 背景
2017年からNoSchoolのサイト自体はあるが、社長がWordPress on SakuraServerで起業したのでエンジニアから見たら酷いものだった。
シードの資金調達を終えて僕がジョインした2019年3月から5月にかけて、フルリプレイスを行うこととなった。

----

### リプレイス後の技術構成

インフラ： AWS（CloudFront, ALB, EC2, S3...）
サーバーサイド： Laravel
フロントエンド: Nuxt.js, jQuery

*※2ヶ月じゃフロントまで移行やりきることができず、現在もページ単位でNuxtに移行中*

----

### Nuxt.jsの採用理由

- 僕自身がVue.jsの経験が1年強あった（前職の新規事業で1人でフルスクラッチした）
- jQueryと比較して開発速度が段違い
- ReactとVueだとあまり違いも採用力が衰えるということもなさそうなのでCTOの僕が経験があって開発速度がとにかく速いものを選べばいいやという考え

----

### Vuetifyの採用理由

- デザイナー不在でそこそこの品質で開発をするためにUIフレームワークは必須
- Bulma、Bootstrap Vueなども見たが、Class属性ではなくコンポーネント自体を用意してくれている方が使い勝手がいいと思った
- ちなみにElementは前職で使っていたけどレスポンシブや細かいところでイライラした

----

# 実際にVuetifyで頑張ったページたち(2019/8時点)

----

### 家庭教師一覧
<img width="1425" alt="スクリーンショット 2019-08-29 10.41.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/72dfd175-d7d1-0e2d-3347-a557c218e7fc.png">

----

### 家庭教師詳細
<img width="379" alt="スクリーンショット 2019-08-29 10.47.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/069e51a5-b4d5-cc80-e3ee-2a285a78df87.png">

----

### 質問作成
![FireShot Capture 001 - NoSchool - NoSchool 無料で勉強の質問、塾や家庭教師の検索 - noschool.asia.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/303696e4-e25f-40f2-3475-32368e80912f.png)

----

# Vuetifyでまずこれだけ覚えておけば的なコンポーネントリスト

----

- ページ（`v-app`, `v-content`等）
- レイアウト（`v-layout`, `v-flex`）
- 余白（`.py-2`, `.mx-4`等）
- 装飾系（`v-card`, `v-chip`, `v-icon`）
- 画像系（`v-img`, `v-avatar`）
- フォーム（`v-btn`, `v-select`, `v-text-field`等）
- あとはドキュメントを読み込む。カスタムSlotも便利。

----

# Vuetifyの便利コンポーネントをいくつか紹介

----

### ページ構成（default.vue）

```html
<template lang="pug">
v-app
  my-header
  v-content(app)
    my-breadcrumbs(:items="breadcrumbs")
    v-container(app fluid)
      nuxt
  my-footer
</template>
```

*※ちょっと改変はしてます*

`v-app`や`v-content`などは公式ドキュメントの通り。

----

### パンくず(Breadcrumbs.vue)

パンくずはv-breadcrumbsがベースで、カスタマイズのために`v-slot:item`を利用。

```html
<template lang="pug">
  v-breadcrumbs(:items="items").px-2.py-0.grey.lighten-3
    template(v-slot:item="props")
      v-breadcrumbs-item(
        :to="props.item.to"
        :disabled="props.item.disabled"
        v-if="props.item.to !== '/'"
      )
        span.grey--text {{ props.item.text }}
      span(v-else)
        a(href="/").pa-0
          v-icon.body-1.primary--text mdi-{{ props.item.icon }}
    template(v-slot:divider)
      span >
</template>
```

↓こうなります
<img width="375" alt="スクリーンショット 2019-08-29 10.58.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/88c57c42-d668-1f39-896b-cc876ed3c784.png">

----

### ページネーション
```html
<template lang="pug">
  v-pagination(
    v-model="pageModel"
    @input="onPaginationClicked"
    @next="onPaginationClicked"
    @previous="onPaginationClicked"
    :length="last_page"
    :total-visible="7"
    style="overflow-x: scroll;"
  ).my-4
</template>
```

scriptを抜粋するとこんな感じ。

```javascript
<script>
export default {
...
  methods: {
    onPaginationClicked(event) {
      this.$emit("query-updated", {
        page: this.pageModel
      });
    }
  }
...
};
</script>
```

要は、ページネーションのページ番号が変わったときに呼び出し元コンポーネントで再度API叩くなりする必要があるので、親にemitする設計にしている。

----

### ページネーションの注意
total-visibleが偶数だと、中央あたりのページ番号で表示が変になるので注意。
例えば6にしたとき、全ページ数が10などのときに5ページ目付近で表示がおかしくなり、隣り合ったページが表示されない。
これは...も含めたvisibleのcountだから、偶数だと...の数が合わなくて隣り合ったページが出ない。
*(気になるなら直せやOSSだろという話ですよねすみません)*

----

# Vuetifyをベースにカスタマイズしているコンポーネント

----

### 見出し系

Vuetifyの見出しはフォントサイズが大胆なのと、サイト全体で統一感を出したいので共通コンポーネント化。

```javascript:plugin/globalComponent.js
import Vue from 'vue'

import Body from "@/components/layouts/Body";
import PageTitle from "@/components/parts/heading/pageTitle";
import SubTitle from "@/components/parts/heading/subTitle";
import SubTitle2 from "@/components/parts/heading/subTitle2";

Vue.component('sec-main', Body)
Vue.component('page-title', PageTitle)
Vue.component('subtitle', SubTitle)
Vue.component('subtitle2', SubTitle2)
```

悩ましいのは、マージンまでコンポーネントに入れるかどうか。入れるとしたらどれくらい大きいマージンを入れるか（デザインの話になってくるけど）。

----

### ボタン
サイトを作っていく中で、`outlined`な`v-btn`をよく使うので共通化。

```html
<template lang="pug">
v-btn(outlined large :color="color" :to="to" @click="clicked" :block="block").py-2.mt-2
  v-icon(v-if="icon" :class="iconColor + '--text'").title.mr-2 mdi-{{ icon }}
  .subtitle-1
    slot
</template>
以下略
```

----

### 必須ラベル
最近作ったatoms的なのでかなり気に入ってる。

```html
<template lang="pug">
  v-chip(small label color="accent").px-2.font-weight-bold 必須
</template>
```

こうなる。
<img width="188" alt="スクリーンショット 2019-08-29 11.17.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/eabb0373-c469-aead-518e-6a00fa51d450.png">

----

# 駆け足でしたがLTなのでこれくらいにします

----

### 最後に

NoSchoolではフロントエンドが得意で、Laravel等のサーバーサイドもゴリゴリやっていきたいWebエンジニアを募集中です！（**iOSエンジニアも！**）
回答最適化のために機械学習も取り入れていたり、勉強意欲の強い人が自由に暴れられると思いますので是非お声がけください〜。