この記事は 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にのせる感じ。なんかイマイチ納得いってないので、知見あれば教えてください
# 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だと難しい部分だと個人的に考えています。
移行に迷っている方の参考になれば幸いです!同様の知見やご意見などお気軽にコメントください。