18
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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 のスキーマを吐き出すことも将来的に可能 (かもしれない)
18
8
0

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
  3. You can use dark theme
What you can do with signing up
18
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?