LoginSignup
7

JSON Schema を作る Presenter で Rails アプリケーション (Qiita Team) とフロントエンドの連携をする

Last updated at Posted at 2023-02-27
1 / 22

話者

  • @tomoasleep
    • https://www.wantedly.com/id/tomoasleep
    • Qiita 社のエンジニアです
      • 新卒で Qiita 社にいた → Wantedly で SRE → 最近 Qiita 社に戻ってきた
      • 今は各アプリケーションの開発 & 開発者体験向上 etc... に取り組んでいます
    • Ruby でを作るのが趣味です
    • 最近の趣味: 散財


話すこと

  • Rails アプリケーション (Qiita Team / Qiita Jobs) とフロントエンド (React) の間の連携を、
  • JSON Schema を吐き出す Ruby の Presenter を作って使うことで、
  • 両者をうまく連携して幸せな開発体験を提供したという話

で話す/した内容です。


Qiita Team の紹介

image.png

  • 企業等向け SaaS
    • 簡単に言うと「社内向け Qiita」
  • 今年で 10 歳 :birthday:
  • Rails で開発
  • Qiita との立ち位置
    • Qiita, Qiita Team は同じ Rails アプリケーションとして開発
      • コア機能 (エディタ, Markdown 処理) を流用
      • その他の機能は徐々に分離を行っている
        • 最初は本当に「Private Qiita」だったが、サービスの成長とともに違いが大きくなり実装の分離を行っていった
    • Qiita 開発で得た経験、資産を取り入れる形で開発


Qiita Team の最近のフロントエンドのアップデート

Before After
home-before.png home-after.png
  • グループ選択、タグ検索のリストなどをサイドバーから選択可能に
    • チャンネルを見る感覚で記事を探すことがしやすい

デザインリニューアルに合わせて、 View 実装の設計を変更

  • View の組み立てを Rails (Slim) → TypeScript (React) に寄せる
    • これまで: View を基本 Rails の View (Slim) で + 部分的に React 化
    • これから: View の一部 (head 要素 etc) 以外は React 側で View を組み立て
      • Qiita Team はクライアントサイドレンダリングを利用
  • Why?
    • View を React に寄せることでの開発体験の向上
      • Slim や React を行ったり来たりしなくて良い
      • デザインを CSS-in-JS (Emotion) に寄せられる
      • etc
    • Qiita のフロントエンド資産の流用
  • Rails 側から React を利用する方法

rails-mvc-2.drawio.png

rails-react-mvc-2.drawio.png


コードの雰囲気例

  • React Component を Rails View (Slim) にマウントしているように書ける
html
  head
    title
      = content_for(:title)
    = javascript_include_tag
  body
    = react_component("HomeFeed", props: { username: "Mike", articles: @articles.map(&:to_h) })
export function HomeFeed({ username, articles }) {
  return (
    {articles.map(article => (
       <div css={itemStyle} key={article.id}>
          <ItemHeader article={article} />
          <ItemBody article={article} />
       </div>
    ))}
  )
}

Rails バックエンド & フロントエンド連携の難しさ

  • テストをするとき大掛かりになる
    • SSR しない場合は、ブラウザテスト (System Test 等) が必要
  • フロントエンドの型が信用できない
    • バックエンドのデータと照合する保証がない。。。
  • フロントエンドに渡すための Serialize どこでやる?
    • erb か Slim に Hash リテラル直書きはしんどい
    • React on Rails に渡す props を組み立てる場所が定まっておらず、散らかっていた

→ 間を取り持つのが React on Rails だけだと心許ない

rails-react-react-on-rails.drawio.png


この関係図を見ていて思うこと

:thinking:

rails-react-react-on-rails.drawio.png


View とそれ以外の間を取り持つもの → Presenter 層

  • Presenter 層を置く (ViewModel, Serializer, Decorator などとも呼ばれる)

rails-react-presenter (1).drawio.png


異なる2つのシステムを取り持つもの → スキーマ

  • 個別にテストしにくい、型が信用できない → スキーマを作る
    • Web API だと例えば OpenAPI, gRPC, GraphQL などがある

schema-driven.drawio.png


この2つをかけ合わせると…?

:eyes:

rails-react-presenter (1).drawio.png

X

schema-driven.drawio.png


JSON Schema を生成する Presenter を間に挟む

rails-react-presenter.drawio.png


作ったもの


コード例 (Component 呼び出し)

= react_component("HomeFeed", props: { username: "Mike", articles: @articles })

= render HomeFeed.new(props: { username: "Mike", articles: @articles })

コード例 (生成される JSON Schema)

{
  "title": "HomeFeedProps",
  "properties": {
    "articles": {
      "type": "array",
      "items": { ... },
    },
    "username": {
       "type": "string",
    },
    "required": [
      "articles",
      "username"
    ],
    "additionalProperties": false
}

コード例 (Component 定義)

class HomeFeed < JsonSchemaView::Component
  renderer_class(:react_on_rails)

  props_class do
    property(:username, type: String)
    property(:articles, type: Array, items: { type: HomeFeed::Article })

    attr_reader :username

    def initialize(username:, articles:)
      @username = username
      @articles = articles
    end

    def articles
      @article.map { |article| HomeFeed::Article.new(article) }
    end
  end
end
  • JsonWorld を View で render 出来るように & 最近の JSON Schema に対応できるように拡張して利用

コード例 (TypeScript)

json-schema-to-typescript を使って TypeScript の型を吐き出せる

import { HomeFeedProps, Article } from "./generated-types/home-feed"

export function HomeFeed({ username, articles }: HomeFeedProps) {
  return (
    {articles.map((article: Article) => (
       <div css={itemStyle} key={article.id}>
          <ItemHeader article={article} />
          <ItemBody article={article} />
       </div>
    ))}
  )
}

:v: 実際に組み込んだ :v:

  • 導入
    • いくつかのプロトタイプを作り
    • コンセプト的に行けそうと判断したら社内ライブラリ化して Qiita Team に導入
    • (便利に使ってたら他の開発メンバーが Qiita Jobs に組み込んでくれたり)
    • その後 OSS 化
  • 効果
    • 信頼性向上 → 安心して開発出来るように
      • TypeScript の型が信用できるものになり、
      • Rails 側の動作検証も Request Spec などで出来るようになる
    • 秩序が生まれた
      • Presenter 層が整ってなかった部分がついでに整備された
      • 必要なデータの問い合わせが Presenter 層に集約されたことで、クエリの効率化
  • 課題
    • クライアントが叩く API の部分は未対応 (信用しにくい型定義のまま)
      • 非同期で API を叩く必要がある箇所は実装済みのため後回しに
    • Presenter が DRY になるための整備


おまけ: render 出来るオブジェクトを作ることが出来る

  • ViewComponent で採用されていた手法
    • Rails 側のアップデートで ViewComponent 以外でも出来るように
= render HomeFeed.new(props: { username: "Mike", articles: @articles })

Ref:


おまけ: Action ごとのテンプレート (Slim) を捨てる

  1. View の React 化が進むことで Rails 側のテンプレート (Slim) が限りなく薄くなる
  2. 使う React Component を宣言するだけのオブジェクト (ReactPage) を作る
  3. Controller だけで完結し、 Action 毎のテンプレート (Slim) が消える
class HomeController < ApplicationController
  def index
    render ReactPage.new(
       HomeFeed.new(props: { username: current_user.name, articles: Article.page(1) })
    )
  end
end

設計面の FAQ

  • Why not OpenAPI/GraphQL/gRPC ?
    • View に必要なデータは、API 経由ではなく、直接渡したい
    • GraphQL は小さくない初期投資をする必要がある
      • GraphQL は Qiita 側で運用されているが、他のサービスでは導入はまだ
      • 土台が出来てくると便利だが、そこまでのコストが大きい
      • GraphQL でモデルの関係をどう表現したり、も慣れが必要
  • Why not Rails API mode?
    • 書き換えが大きくなる
      • やりたいことはフロントエンドを React に寄せることだったので、やりたいことに対して労力が大きすぎると判断した
  • Why JSON Schema?
  • Code First vs Schema First → Code First
    • 社内で採用している技術が Code First なので (GraphQL-Ruby, Qiita API) 馴染む
    • (個人的な考えとして) Ruby は型をチェックする仕組みがまだ弱いので、型定義と実装は近いところにある方が置いたほうが何かと良い
  • API 等のスキーマはどうしている?
    • やれてない (今回はスコープから外した)
    • 「JSON Schema を吐き出せる Presenter」 が作れたので、 API と紐付ければ OpenAPI のスキーマを吐き出すことも将来的に可能 (かもしれない)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7