Nuxt.js + Vuetify.jsで回文を作成して共有できるサービス「回文メーカー」を作った

Nuxt.js + Vuetify.jsで、回文を作成して共有できるサービス「**[回文メーカー](https://kaibunmaker.com/)**」を作りました。

## 特徴
<img src="https://qiita-image-store.s3.amazonaws.com/0/132608/982bf1a0-33e9-87ff-3fd4-076ae4e9a90d.png" width="300">

回文メーカーでは、入力内容に応じて回文を作成することができます。
また、作成した回文をTwitterに共有することもできます。

### レスポンシブデザイン
![6b3195a1-9c90-b8ba-31f0-c1b1a908099a.gif](https://qiita-image-store.s3.amazonaws.com/0/132608/6b3195a1-9c90-b8ba-31f0-c1b1a908099a.gif)

PCやスマートフォンなど、あらゆるデバイスで違和感のないレスポンシブデザインです。

### PWA対応
<img src="https://qiita-image-store.s3.amazonaws.com/0/132608/eb96af9c-ffd2-156c-28a0-345e476d60de.png" width="300"><img src="https://qiita-image-store.s3.amazonaws.com/0/132608/850b771c-b696-7367-a4c7-1cf541766dd4.png" width="300">

PWA対応なので、iPhoneやAndroidではアプリとしても使えます。

## Nuxt.js + Vuetify.jsという選択
<img src="https://qiita-image-store.s3.amazonaws.com/0/132608/3b63e63d-fad4-3b85-c7a1-dc473fcecefb.png" width="50%"><img src="https://qiita-image-store.s3.amazonaws.com/0/132608/94077787-b9f5-6bcc-12af-aa0628497077.png" width="50%">

今回は、[Nuxt.js](https://ja.nuxtjs.org/)と[Vuetify.js](https://vuetifyjs.com/en/)という二つのフレームワークを使用しました。

### Nuxt.jsという選択
Nuxt.jsは、ウェブサイトのインターフェースを構築するためのJavaScriptフレームワークである[Vue.js](https://jp.vuejs.org/)に、いくつかのライブラリを組み込んだフレームワークです。
Vue.js自体も魅力的なフレームワークですが、Nuxt.jsにはより強力な機能が備わっています。
回文メーカーはページが一つしかない小規模なアプリケーションですが、Nuxt.jsの恩恵をおおいに受けています。

### Vuetify.jsという選択
Vuetify.jsは、マテリアルデザインに準拠したVue.js用のUIフレームワークです。
Vue.js用のUIフレームワークは他にもたくさんありますが、僕はタグ名がイケてる（`<v-btn>`のように`v-`が接頭辞になっている）のでVuetify.jsが気に入っています。
Vuetify.jsはウェブ上に日本語の情報があまりなかったのですが、最近は導入事例や紹介記事も増えてきて人気の高まりを感じています。[^1]

## やったこと
### ドメインを取得
[munieru.jp](https://munieru.jp)ドメインのホスティングでもお世話になっている[スタードメイン](https://www.star-domain.jp/)で、`kaibunmaker.com`ドメインを取得しました。
これにより、将来的にホスティングサイトを切り替えた場合も同じドメインで運用し続けることが可能です。
~~早くもGitHub Pagesのデプロイフローにつらみを感じているので、早々にNetlifyに移行するかもしれません。~~
その後、Netlifyに移行しました。

### Twitterのアカウントを取得
念のため、Twitterの[@kaibunmaker](http://twitter.com/kaibunmaker)アカウントを取得しました。
なりすまし防止の観点で一応押さえているだけなので、特にツイートはしていません。

### アプリケーションの環境構築
アプリケーションを実装するための基盤となる環境作りです。

#### アプリケーションの雛形を生成
はじめに、[create-nuxt-app](https://github.com/nuxt/create-nuxt-app)を用いてアプリケーションの雛形を生成しました。[^2]

```sh
$ npx create-nuxt-app kaibunmaker
```

プロジェクトの設定は以下の通りです。

* Project name：`kaibunmaker`
* Project description：`Palindrome maker`
* Use a custom server framework：`none`
* Use a custom UI framework：`vuetify`
* Choose rendering mode：`Single Page App`
* Use axios module：`no`
* Use eslint：`yes`
* Use prettier：`no`
* Author name：`Munieru`
* Choose a package manager：`npm`

次に、不要なページやコンポーネントを削除しました。[^3]
この時点で、`layouts/default.vue`と`pages/index.vue`は以下のような状態です。

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

```vue:pages/index.vue
<template>
  <v-layout>
    <v-flex>
      <span>Hello World</span>
    </v-flex>
  </v-layout>
</template>
```

こうすることで、その後ページを作っていく際にコミットの差分が綺麗になります（本当は`<span>Hello World</span>`すらないほうがいいです）。

参考：[Nuxt.jsのプロジェクトにVuetify.jsを導入する方法 - Qiita](https://qiita.com/munieru_jp/items/882727bb493c8502aea1)

#### JavaScript Standard Styleを導入
僕は[JavaScript Standard Styleが好き](https://qiita.com/munieru_jp/items/ca16cbfa859468137d2e)なので、[JavaScript Standard Style](https://standardjs.com/)を導入しました。[^4]

参考：[Nuxt.jsのプロジェクトにESLint + JavaScript Standard Styleを導入する方法 - Qiita](https://qiita.com/munieru_jp/items/22ad3a5d0919e423b447)

#### Sassを導入
素のCSSでスタイルを書くのはつらいので、CSSのメタ言語である[Sass](https://sass-lang.com/)を導入しました。[^5]
使用する言語ぐらいは、create-nuxt-appのオプションで選べるようにしてほしいところです。

参考：[Nuxt.jsのプロジェクトにSassを導入する方法 - Qiita](https://qiita.com/munieru_jp/items/ff603eaa0a27656e7182)

#### stylelintを導入
CSSやSassの構文をチェックするために、[stylelint](https://stylelint.io/)を導入しました。[^6]
こちらも、ESLintやPrettierと同様にcreate-nuxt-appのオプションで選べるようにしてほしいところです。

参考：[Nuxt.jsのプロジェクトにstylelintを導入する方法 - Qiita](https://qiita.com/munieru_jp/items/ea41284f473ad3669616)

#### nuxt-i18nを導入
アプリケーションで使用するテキストをソースコードから分離するために、[nuxt-i18n](https://nuxt-community.github.io/nuxt-i18n/)を導入しました。[^7]
nuxt-i18nはその名の通りi18nのためのライブラリですが、一つの言語にしか対応しない場合も有用です。
たとえば言語ファイルにアプリ名を定義しておけば、後からアプリ名を変更したい場合にソースコードのあちこちにあるアプリ名を書き換えなくとも、言語ファイルを更新するだけで済みます。

#### フォントを追加
アプリケーションで使用するフォントを追加しました。[^8][^9][^10]

* [Noto Sans JP](https://fonts.google.com/specimen/Noto+Sans+JP)
* [Noto Serif JP](https://fonts.google.com/specimen/Noto+Serif+JP)
* [Font Awesome](https://fontawesome.com/)

タイトルロゴは明朝体（Noto Serif）、それ以外はゴシック体（Noto Sans）を使用しています。
游書体にしようか悩みましたが、マテリアルデザインとの親和性を考えてNotoにしました。
Font Awesomeは、ツイートボタンでTwitterのアイコンを使用するためだけに追加しています。

### アプリケーションを実装
リリース日まで時間もなかったので、最悪途中の状態でリリースできるように重要度の高いものから実装していきました。

#### 回文を作成する機能を実装
最初は回文を共有する機能を抜きにして、回文を作成する機能のみを実装しました。
フォームの値を元に回文を生成する処理は、以下のような関数によって実現しています。

```js:createPalindrome.js
import reverseString from '~/modules/reverseString'

export default function (text, repeat) {
  const reversedText = reverseString(text)
  return repeat ? `${text}${reversedText}` : `${text.substring(0, text.length - 1)}${reversedText}`
}
```

「最後の文字を繰り返す」がオフの場合は、ダブらないように最後の文字を削っています。

#### 回文を共有する機能を実装
回文メーカーは静的サイトであり、データベースを持っていません（「DBレスアーキテクチャ」とか言っておきます）。
作成した回文を共有するための仕組みとして、URLのパラメーターに`status=とまと`のように回文を含めることで、フォームに値が入力された状態で表示するようにしています。
URLのパラメーターを元にフォームの値を生成する処理は、Nuxt.jsの`asyncData`メソッド内と、vue-routerの`beforeRouteUpdate`メソッド内で行なっています。

```js
export default {
  // (省略)
  async asyncData ({ query }) {
    return createFormData(query.status)
  },
  beforeRouteUpdate (to, from, next) {
    const { text, repeat } = createFormData(to.query.status)
    this.text = text
    this.repeat = repeat
    next()
  }
}
```

こうすることで、初回アクセス時とページ遷移時の両方でフォームの値を更新することができます。

#### レスポンシブデザインの最適化
ブロック要素や画像はVuetify.jsのコンポーネントを使用すればいい感じにレスポンシブにしてくれるのですが、テキストについてはその限りではありません。
タイトルロゴのフォントサイズを以下のように指定することで、ビューポートの横幅に応じて文字サイズが適切に変化するようにしました。

```scss
$relative-max-width: 600px;
$relative-font-size: 14vw;
$absolute-font-size: 84px;

.app-title {
  font-family: "Noto Serif JP", serif;
  font-size: $absolute-font-size;

  a {
    color: inherit;
    text-decoration: inherit;
  }
}

@media (max-width: $relative-max-width) {
  .app-title {
    font-size: $relative-font-size;
  }
}
```

`14vw`というのは「ビューポートの横幅の14%」という意味で、横幅が`600px`未満の場合は1文字をビューポートの横幅の14%で表示し、横幅が`600px`以上の場合は`84px`で表示するようにしています。

#### アイコンを作成
最近流行りの[Sketch](https://www.sketchapp.com/)で、アプリケーションのアイコンを作成しました。

![132608/2e8c16c8-68c4-a58a-8d82-98097e74a339.png](https://qiita-image-store.s3.amazonaws.com/0/132608/2e8c16c8-68c4-a58a-8d82-98097e74a339.png)

色とフォントを実際のアプリケーションに合わせつつ、円形にトリミングしても文字部分が欠けないようにサイズを調整しました。

#### OGP対応
TwitterやFacebookなどのサイト上でリッチな表示をするために、[OGP](http://ogp.me/)に対応しました。[^11]
OGPタグではURLを絶対URLで記述する必要があるため、アプリケーション独自の`config.json`ファイルでデプロイ先のオリジンやベースディレクトリを指定するようにしています。

```json:config.json
{
  "BASE_DIR": "/",
  "TWITTER_ID": "@munieru_jp",
  "OG_IMAGE_PATH": "favicon-256x256.png",
  "ORIGIN": "http://localhost:3000/"
}
```

こうすることで、以下のように環境に応じてベースURLを変えることができます（環境変数を使用していないのは、毎回指定するのが面倒だからです）。

|環境|ベースURL|
|---|---|
|ローカル|`http://localhost:3000/`|
|GitHub Pages|`https://munierujp.github.io/kaibunmaker/`|
|独自ドメイン|`https://kaibunmaker.com/`|

#### PWA対応
せっっかくなので、[PWA](https://developers.google.com/web/progressive-web-apps/)にも対応しました。[^12]
Nuxt.jsでPWAに対応するには、公式の[PWAモジュール](https://pwa.nuxtjs.org/)を使うのが便利です。
注意点としては、デフォルトではアプリケーション名や作者名などのメタ情報は`package.json`に記載されているものが使われるので、変更したい場合は`nuxt.config.js`ファイルに項目を追加する必要があります。


```js:nuxt.config.js
// (省略)
module.exports = {
  // (省略)
  manifest: {
    name: APP_NAME,
    short_name: APP_NAME,
    description: APP_DESCRIPTION,
    lang: DEFAULT_LOCALE
  },
  meta: {
    author: TWITTER_ID
  }
}
```

#### Googleアナリティクスを導入
アクセス解析をするために、Googleアナリティクスを導入しました。[^13]
Nuxt.jsでGoogleアナリティクスを使用するには、公式の[Googleアナリティクスモジュール](https://github.com/nuxt-community/analytics-module)を使うのが便利です。

参考：[Google アナリティクスの統合 - Nuxt.js](https://ja.nuxtjs.org/faq/google-analytics/)

## 所感
### 締め切り駆動開発はつらい
今回、リリース日を回文の日である12月21日に決めてしまったので、プライベート開発にも関わらず締め切りが明確に定められているのがきつかったです。
偶然にも12月18日に仕事でサービスリリースのための深夜作業があったので、その前後はあまり開発に時間を費やすことができず、リリース前日はネットカフェで終電ギリギリまでデバッグ作業をしていました。

### Nuxt.jsはいいぞ
たいていのことはモジュールを使用すれば実現できるので、本質的な部分の開発に注力することができます。
僕自身も[nuxt-onsenui-module](https://www.npmjs.com/package/nuxt-onsenui-module)というモジュールを作って公開しているので、次は[Onsen UI](https://ja.onsen.io/)を使って何かを作ってみたいですね。

## ソースコード
回文メーカーのソースコードは、GitHubで公開しています。

* [munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker)

## 関連記事
* [なぜ12月21日が回文の日なのかを考えてみた - Qiita](https://qiita.com/kgoto/items/d63f6b0f91d632b49709)

[^1]: [Vueでもっと幸せになりたいあなたへ。VueのUIコンポーネントライブラリVuetifyのススメ - KAYAC engineers' blog](https://techblog.kayac.com/2018/12/12/080000)
[^2]: [Create project by create-nuxt-app · munierujp/kaibunmaker@3ac3935](https://github.com/munierujp/kaibunmaker/commit/3ac3935fb37e1710c6c1a4765b3153147f5997a9)
[^3]: [Clean pages · munierujp/kaibunmaker@fb8ddc5](https://github.com/munierujp/kaibunmaker/commit/fb8ddc53c47cd3e2e224439f62fb02da97f3c2fa)
[^4]: [Use JavaScript Standard Style by munierujp · Pull Request #2 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/2)
[^5]: [Use Sass by munierujp · Pull Request #5 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/5)
[^6]: [Use stylelint by munierujp · Pull Request #6 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/6)
[^7]: [Use nuxt-i18n by munierujp · Pull Request #8 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/8)
[^8]: [Add Noto Sans JP font by munierujp · Pull Request #12 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/12)
[^9]: [Add Noto Serif JP font by munierujp · Pull Request #13 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/13)
[^10]: [Add Font Awesome 5 by munierujp · Pull Request #14 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/14)
[^11]: [Add OGP/Twitter Cards by munierujp · Pull Request #16 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/16)
[^12]: [Apply PWA by munierujp · Pull Request #19 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/19)
[^13]: [Add Google Analytics by munierujp · Pull Request #18 · munierujp/kaibunmaker](https://github.com/munierujp/kaibunmaker/pull/18)