💎

「zaicoのフロントエンドはなぜRailsエンジニアにやさしいのか」というタイトルで社内LTをしました

に公開
2

この記事は2025 ZAICO アドベントカレンダーの1日目の記事です!

ZAICOでは月に1回、社内LT会が開催されています。
この記事はLTで発表した内容を改めてまとめたものになります。

はじめに

zaico は在庫管理SaaSとして10年以上の歴史を持つサービスです。バックエンドには Rails を使用しており、当初は ERB + jQueryでしたが、現在はほとんど全ての画面が Rails + Vue(3系)に移行されています。

私は前職では ERB + jQuery でフロントエンドのライブラリを Webpacker でバンドリングするような開発環境でした。

jQuery は簡単な装飾程度なら問題ない一方で、フロントエンドに関して特にルールがない中でコードが増えていくことによる見通しの悪さや、リッチなUIでDOM操作、状態管理がある仕様(ダイアログ開いてフォーム送信等)の実装が複雑になりがちなどの問題が発生しました。

そこで、Railsにモダンフロントエンドを導入するみたいな観点で色々と調べた記憶があります。
その中で SPA という構成を知りました。SPA とは Rails をAPIサーバーとして使い、分離されたフロントエンドで動的に画面を書き換えます。

しかし、Rails に慣れているとだいぶ違った開発体験になりそうだと思いました。結局「jQuery は辛いけど SPA も辛そう」で止まってしまっていました。

その後2024年4月に ZAICO に入社して当時すでに Vue が導入されていたんですが「なんか ERB と似ていて開発体験が良いな」と感じました。

そして内部の実装をあれこれ調べていくうちに、さまざまな工夫がされていることに気づき感動したので、今回その内容でLTしてみようと思いました。

zaicoのアプローチ

zaico では2つの Rails 標準の仕組みと同じものを自前で実装することによって、Vue を ERB のような感覚で使えるようになっています。

1. 自動レンダリング機能

従来の Rails では、コントローラーのアクションに対応する ERB ファイルが自動的にレンダリングされます。

例えば以下のように ItemsController#show アクションを定義すると、自動的に app/views/items/show.html.erb がレンダリングされます。

# app/controllers/items_controller.rb
class ItemsController < ApplicationController
  def show
  end
end
# => app/views/items/show.html.erb が自動的にレンダリングされる

これと同じように zaico では WebVuetifyController を継承した上で、ItemsController#show アクションを定義すると、 app/frontend/javascripts/pages/items/show.vue が自動的にレンダリングされるようになっています。

# app/controllers/items_controller.rb
class ItemsController < WebVuetifyController
  def show
  end
end
# app/frontend/javascripts/pages/items/show.vue が自動的にレンダリングされる

実装の仕組み

  1. WebVuetifyController を継承したコントローラーは、Vue 用のレイアウトファイルが適用される
# app/controllers/concerns/web_vuetify.rb
module WebVuetify
  extend ActiveSupport::Concern

  included do
    layout 'vuetify_main'
  end

  def default_render(*args)
    respond_to do |format|
      format.html {
        render(inline: '', layout: layout_name)
      }
    end
  end
end
  1. レイアウトファイル内で、コントローラー名とアクション名から自動的に Vue コンポーネントのパスを生成
<% # app/views/layouts/vuetify_main.html.erb %>
<body data-component="<%= vue_page_component_path(controller) %>">
  <% # vue_page_component_path => "items/show" %>
  <page-component
    <% if defined?(@vue_data) %>
      :server-data="<%= @vue_data.to_json %>"
    <% end %>
  ></page-component>
</body>
  1. JavaScript側で data-component 属性を読み取り、対応する Vue コンポーネントを動的にロード:
// app/frontend/javascripts/app.ts
const getPageComponent = (modules: any[]) => {
  const pageComponentName = document.body.dataset.component
const pageComponentPath = `../javascripts/pages/${pageComponentName}.vue`
return modules[pageComponentPath].default
}

// data-component="items/show" の場合
// pages/items/show.vue がロードされる

このように一度 Vue 用の ERB レイアウトファイルを経由することで、コントローラーのアクション名に対応したページコンポーネントの描画を行っています。

2. インスタンス変数の共有

Rails の ERB では、コントローラーで定義したインスタンス変数をビューで参照することができます。

# app/controllers/items_controller.rb
class ItemsController < WebVuetifyController
  def show
    @item_title = "メロンパン"
  end
end
# app/views/items/show.html.erb
<p><%= @item_title %></p>

zaico では、@vue_data という特別なインスタンス変数を定義することで、対応するページコンポーネント側で serverData という名前の props として受け取れるようになっています。

例えば @vue_data の中に itemTitle というプロパティで変数を定義すると、show.vue では props.serverData.itemTitle として受け取ることができます。

# app/controllers/items_controller.rb
class ItemsController < WebVuetifyController
  def show
    @vue_data = {
      itemTitle: "メロンパン"
    }
  end
end
// app/frontend/javascripts/pages/items/show.vue
<template>
  <p>{{ props.serverData.itemTitle }}</p>
</template>

<script setup lang="ts">
const props = defineProps<{
  serverData: {
    itemTitle: string
  }
}>()
</script>

このような構成は、ページ読み込み時にフロントにデータを渡すことができて、ERB の時の開発体験とにている点が魅力的だと思います。

この仕組みについてもVueの画面で共通で呼ばれる共通の ERB レイアウトファイルで実装されています

<% # app/views/layouts/vuetify_main.html.erb %>
<page-component
  <% if defined?(@vue_data) %>
    :server-data="<%= @vue_data.to_json %>"
  <% end %>
></page-component>

共通のレイアウトファイルで @vue_data を受け取り、serverData としてページコンポーネントに props を渡しています。

注意点

ご紹介した zaico のフロントエンドは Vue を導入しつつも構成としては MPA になります。そのため画面内でリアクティブにデータを取得したい場合は、APIを用意する必要があります。

つまり、同じ画面で2つの Controller が必要になります。

  • 画面表示用 - app/controllers/items_controller.rb
  • 内部API用 - app/controllers/api/items_controller.rb

はじめは紛らわしいですが、これらを適切に使い分けることによって MPA でありながらリアクティブにデータ取得する画面を実装することができます

まとめ

ここまでで zaico がどのようにして Rails に Vue を導入したかについてご紹介しました。

導入した当時はフロントエンド専門のエンジニアが不在で、SPA を諦めたそうです。

Rails + Vue の MPA にした上で ERB と同じ感覚で Vue が使えるようにさまざまな工夫がされていました。

こうすることで、Rails に慣れたエンジニアにとって非常に開発体験がよいです。

また Vue( + TypeScript )を導入することでより複雑な仕様の画面を安全に見通しよく開発できるので、Rails とモダンフロントエンドのいいとこ取りのような構成になっています。

Rails にモダンフロントエンドを導入検討しているチームの参考になれば幸いです。

明日はmaaatsudaさんによる、「Slackチャットデータで共起ネットワーク分析を試す」です。お楽しみに!

ZAICO Developers Blog

Discussion

NaofumiNaofumi

ありがとうございます!とても参考になりました。
もしかしたらなのですが、app/frontend/javascripts/pages/items/show.vueのところにタイポがあるかもしれないと思いました。

<template>
  <p>{{ props.serverData.inventoryTitle }}</p>
</template>

上記のprops.serverData.inventoryTitleはもしかしてprops.serverData.itemTitleですか?

NishiNishi

ご報告いただきありがとうございます!
タイポでした。先ほど本文を修正しました。