Help us understand the problem. What is going on with this article?

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

この記事は Nuxt.js Advent Calendar 2019 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

移行後の構成

  • リニューアルするページ: 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

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

after

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 を使うと良いでしょう。

// 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というライブラリを使っているため、ほぼオーバーヘッドはありません。
例えば、Pageコンポーネントは以下のように書くだけです。

<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の紹介

ディレクトリ構成

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のネストした動的ルーティングで困ったので調べてみた

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メソッドを使うことで設定できます。

また、GoogleAnalyticsやGoogleTagManagerなどは、既にモジュールがあるので、RoRよりもむしろ簡単に導入できるかと思います。
Google アナリティクスを使うには?

エラーハンドリング

もともとbugsnagを使っていたので、nuxt-bugsnagを入れました。
これにより、サーバー/クライアント両方でのエラー通知が可能になります。

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

ページネーション

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

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

# 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だと難しい部分だと個人的に考えています。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした