RailsアプリをNuxt.jsに移行する際のTIPSいろいろ

この記事は [Nuxt.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nuxt-js) 16日目の記事です。

この記事では、Ruby on Railsアプリのview部分をNuxt.jsに移行した話を元に、Nuxt.jsでの開発全般の知見を紹介します。
※Railsの話はほぼ出てきません！


# 移行前の状況
フリーランスとしてジョインしたWebサービスが、以下の状況でした。

* 全体的に6年前くらいの技術スタックのRailsアプリ(Rails4.0, svn, jQuery...など)
* サーバー側はModel, View, Controllerとどこもコードの量が多く煩雑で、リファクタリングが辛い。テストもほぼない。
* フロントエンドも適切にファイル分割されておらず、1つのcssファイルが一万行あったり、jsがRailsのテンプレートにベタ書きされている
* ちょうど一部ページのフルリニューアルの計画がある(!)

# 移行のモチベーション
**「今後の開発効率が上がり、様々な機能を今後開発しやすくなるのがメリット」**
ということを、会社の経営陣などステークホルダーにお伝えして、承認を得ました。
具体的には以下をお伝えしました。

**開発上のメリット**

- Hot Module Replacementによるコーディングの即時反映
- コンポーネント開発が強制され、jsやcssの見通しが良くなる
    - 既存の状態では誤ったcss変更によるレイアウト崩れがしばしば発生していた
    - また、cssを変更できる人が限られていた
- 非同期処理やアニメーションのロジックが簡潔に見通しよく書ける
    - jQueryで頑張るのはもうつらい

**ユーザー側のメリット**

* アプリのようなリッチなUXを提供しやすい。
* 開発効率が上がる分、ユーザーに本当に提供すべきことに開発を集中できる

個人的に、ある程度リッチなUIを作るのであれば、もはやRailsでフロントエンドをやる時代ではないと思っています。
（そして、ある程度リッチなUIはもはや現代のWebサービスでは必須と考えています）

この辺の話は、以下スライドが参考になるかと思います

[私たちはなぜ SPA で開発するのか / Why you choose SPA](https://speakerdeck.com/potato4d/why-you-choose-spa)


# 移行後の構成
* リニューアルするページ: Nuxt.js(SSR) + Ruby on Rails(API)
* 旧ページ: Ruby on Rails

という構成で、もともとのRoRアプリにAPIを生やしつつ、Nuxt.jsを別サーバーとして立ち上げることにします。
旧ページも今後すべてNuxtに移行し、RoRはAPIのみとする予定ですが、全てを一度にリニューアルするのはボリュームが大きすぎるため、一旦一部ページのみとしました。

ちなみに、Nuxt移行の前に、開発環境を整える作業を1ヶ月で完了しました。
（Rails4.0->6.0, svn->git, EC2->GAE, MySQL on EC2 -> Cloud SQL, もろもろのリファクタリングなど）


# 移行のTIPS
## ルーティングについて
今回は一部ページのみrailsで動き続けるため、リクエストを適切にnuxtかrailsに振り分ける必要があります。
今回はこれを「GAEによるdispatch」「Nuxtによるリダイレクト」の2つで移行を実現します。

### GAEのdispatch
GAEは以下のような `disaptch.yml` を書くだけでルーティングを変えることが可能で、非常に楽なのでオススメです。

**before**

```yaml
dispatch:
  - url: "*www.example.com/*"
    service: rails
```

**after**

```yaml
dispatch:
  - url: "*www.example.com/*"
    service: nuxt
  - url: "*www.example.com/admin/*"
    service: rails
  - url: "*www.example.com/api/*"
    service: rails
```

これだけで済めば万歳だったのですが、GAEでは `*` がURLの最初か末尾にしか使えず、複雑な正規表現などは使えないため、これだけでは要件を満たせませんでした。

### Nuxtのリダイレクト
GAEレイヤでのルーティングで対応できない箇所は、Nuxtにきたリクエストをリダイレクトすることにします。

Nuxtでこれを行いたい場合 [@nuxtjs/redirect-module](https://github.com/nuxt-community/redirect-module) を使うと良いでしょう。

```js
// nuxt.config.js
{
  modules: [
    '@nuxtjs/redirect-module'
  ],
  redirect: [
    {
      from: '^/hoge',
      to: 'https://www.external.com/hoge',
    }
  ],
}
```

モジュールの中では、`addServerMiddleware` を使ってリダイレクトの処理を行なってくれます。
外部へのリダイレクトを行いたい場合、 `vue-router` を使うのではなく、 `serverMiddleware` の機構を使ってリダイレクトをすべきであることに注意しましょう。


## APIとのつなぎ込み
RoRからSPA+APIに移行する際のオーバーヘッドとして、APIとのつなぎ込みが頭に浮かぶかもしれません。

ここに関しては[nuxt-resource-based-api](https://github.com/nullnull/nuxt-resource-based-api)というライブラリを使っているため、ほぼオーバーヘッドはありません。
例えば、Pageコンポーネントは以下のように書くだけです。

```html
<script>
import createComponent from '@/lib/create_component'

export default createComponent([
  { resource: 'task', action: 'index' }, // APIのコントローラー、アクションを指定
])
</script>

<template>
<div>
  <div class="task" v-for="task in tasks">
    {{ task.name }}
  </div>
</div>
</template>
```

詳細は以下をご覧ください。
[爆速でnuxtとAPIを繋げるnuxt-resource-based-apiの紹介](https://qiita.com/nullnull/items/d727767392527cd5773a)


## ディレクトリ構成
### `pages` ディレクトリ
Nuxt.jsではpagesディレクトリ配下の構成が、そのままルーティングになります。

Railsでリソースベースでルーティングを行なっていると、 `/users/123/tasks/456` のようなパスを作ることがあると思いますが、
この場合は `pages/users/_id/tasks/_taskId/index.vue` というコンポーネントを作成すると、正しくルーティングされます。
idには `route.params.id` `route.param.taskId` のような形でアクセスできます。
ということで、無事Railsのパスをそのまま使うことができます。


注意として **`_id.vue` ではなく `_id/index.vue` を作ることを推奨します**。
`pages/users/_id.vue` が存在する状態で、 `pages/users/_id/hoge.vue` というコンポーネントを作成し、 `users/123/hoge` にアクセスすると、期待通りの挙動をしません。
この辺は以下を参照にしてください。

[Nuxt.jsのネストした動的ルーティングで困ったので調べてみた](https://tech.actindi.net/2019/07/04/090239)


### componentsディレクトリ
Nuxt.jsは他の同様のフレームワークと比べると、ディレクトリ構成の制約が強いフレームワークですが、RoRに比べると弱いですよね。
特にcomponentsディレクトリ以下について、構成のベストプラクティスは特に定まっていない認識です。

ここについて、少なくともRailsアプリ開発経験者には、Railsと同じくパスベース+ `shared` ディレクトリを使った、以下のような構成が分かりやすいと考えています。

```
（components/以下のディレクトリ構成例）
.
├── users
│   ├── articles
│   │   └── A.vue
│   └── shared
│       ├── B.vue
│       ├── C.vue
├── articles
│   └── D.vue
├── layouts
│   ├── Footer.vue
│   ├── Header.vue
│   └── header
│       ├── Logo.vue
└── shared
    ├── E.vue
    ├── card
    │   ├── ArticleCard.vue
    └── icon
        ├── FacebookIcon.vue
        ├── LineIcon.vue
```

より具体的には、以下のルールを `README.md` に明記しています。

- コンポーネント Foo が、1 つの Page コンポーネントでしか使われない場合 -> pages に対応するディレクトリに格納する
  - (例) `pages/users/index.vue` のみで使うコンポーネントのパスは `components/users/Foo.vue`
- コンポーネント Foo が、複数の Page コンポーネントで使われる場合 -> 共通する名前空間として最大の名前空間となるディレクトリに `shared` を作って格納する
  - (例) `pages/users/a/index.vue` `pages/users/b/index.vue` で使うコンポーネントのパスは `components/users/shared/Foo.vue`
- `shared/` 以下のディレクトリ構成は、できる限り意味のあるまとまりごとに格納する。（共通認識を得るのが難しいため、厳密に管理しない）
  - （例） `shared/icon/TwitterIcon.vue` `shared/card/ArticleCard.vue` など。`shared/bar/` ディレクトリのコンポーネント名は `*Bar.vue` を推奨。
  - コンポーネントの数が増えてきた場合、atomic design などを取り入れつつ、UIのグルーピングの単位をチーム内でしっかりと共通認識を揃えて、ディレクトリを作成する
- `components/layouts/` のみ特殊で、 `layouts/*.vue` で使われるコンポーネントを格納する


## OGPやタグ
SSRしてNuxtを使うのは初めてだったので、OGPがちゃんと生成できるかドキドキでしたが、ちゃんと `fetch()` で取得してきたAPIレスポンスを元にタイトルやOGPを生成することができました！すごい（小並）
OGPは[headメソッド](https://ja.nuxtjs.org/api/pages-head/)を使うことで設定できます。

また、GoogleAnalyticsやGoogleTagManagerなどは、既にモジュールがあるので、RoRよりもむしろ簡単に導入できるかと思います。
[Google アナリティクスを使うには？](https://ja.nuxtjs.org/faq/google-analytics/)


## エラーハンドリング
もともとbugsnagを使っていたので、[nuxt-bugsnag](https://github.com/julianmar/nuxt-bugsnag#readme)を入れました。
これにより、サーバー/クライアント両方でのエラー通知が可能になります。

便利なモジュールがたくさん公開されているのも、Nuxtの良いところですね。

## ページネーション
レコードを全件fetchしてよければ[vuejs-paginate](https://www.npmjs.com/package/vuejs-paginate)のようなライブラリを使うのが良さそうですが、
総レコード数が多い場合は、フロント側だけでページネーションの機構を作るのは難しいです。

そのため、結局ページネーションに必要な情報はサーバーサイドで全て算出して、レスポンスとして渡すことにしました。
RoRを使ってれば、ページネーションはkaminariを使うと思いますが、kaminariのメソッドのレスポンスをそのままAPIにのせる感じ。なんかイマイチ納得いってないので、知見あれば教えてください:pray:

```ruby
# Rubyのコードです
class Api::BaseController < ApplicationController
  private

  def paginate(relation, page, per_page, includes: [])
    paged_relation = relation.page(page).per(per_page)

    {
      page_meta: {
        total_pages: paged_relation.total_pages,
        total_count: paged_relation.total_count,
        current_page: paged_relation.current_page,
        current_cursor_start: (page - 1) * per_page + 1,
        current_cursor_end: [page * per_page, paged_relation.total_count].min
      },
      records: paged_relation.includes(includes)
    }
  end
end
```


# 総括
RailsアプリをNuxtにリニューアルすることで、サーバー側はシンプルなAPIの実装で見通しよく、フロントもコンポーネント化によりjsとcssが非常に見通しよくなりました！
また、マークアップを担当していただいているエンジニアさんにも好評でした。この辺は、Reactだと難しい部分だと個人的に考えています。

移行に迷っている方の参考になれば幸いです！同様の知見やご意見などお気軽にコメントください。