Upgrade to Pro — share decks privately, control downloads, hide ads and more …

jQuery + Sass な SPA Rails アプリを React + CSS in JS にリプレースした話 @kei-p @gotchane #railsdm2019

jQuery + Sass な SPA Rails アプリを React + CSS in JS にリプレースした話 @kei-p @gotchane #railsdm2019

株式会社ソニックガーデン 石澤 佳祐( @kei-p ), 後藤 雅之( @gotchane )

Railsアプリケーションの開発において、React/Vue.js 等のモダンなフレームワークが台頭し、これまで主流だった jQuery、Sass で運用されているサービスをリプレースする事例が増えてきています。その一方で、リプレースを検討しつつも、開発規模が大きすぎたり、リソースの確保ができなかったりという理由で、リプレースが進まないケースも多く存在するかと思います。

本セッションでは、そんな既存サービスのリプレースを検討しているエンジニアにとって、SonicGarden の価値観の一つである小口化という考え方を元に、限られたリソースの中でも着々とリプレースを進めていった知見についてお話ししたいと思います。

Recently, modern frontend frameworks such as React and Vue are getting more and more popular and Rails apps built with jQuery and Sass are getting replaced by those new frameworks. On the other hand, some replaces cannot be done due to the hugeness of the project or lack of resources.

In this talk, I'd like to talk about the case where we replaced with limited resources based on "making it small", that's one of the core values of SonicGarden.

Rails Developers Meetup 2019
https://railsdm.github.io/

Masayuki Goto

March 22, 2019
Tweet

Other Decks in Programming

Transcript

  1. jQuery + Sass な SPA Rails アプリを React + CSS

    in JS にリプレースした話 Rails Developer meetup 2019 - Keisuke Ishizawa, Masayuki Goto
  2. 2 今日話すこと • リプレースをうまく進めていくコツ、小口化 ◦ 機能:画面をパーツごとに ◦ 網羅性:保守性を上げる目的で及第点を目指す ◦ 影響度:限定公開することで

    • Rails における React を活かした UI/UX への刷新事例 ◦ Sass → CSS in JS への置き換え ◦ コンポーネントの振る舞いを jQuery → React に ◦ jQuery・React それぞれの spec を並行稼動 ◦ Rails における React i18n 化
  3. 4 自己紹介 • 氏名:石澤 佳祐 • GitHub, Qiita: @kei-p •

    株式会社 SonicGarden 3 年目 • Rails 歴 4 年 • 今日は @jnchito に送り込まれてきました ◦ 発表初めて!
  4. 6 Remotty について • SonicGarden のバーチャルオフィス / お客さんにも提供 ◦ https://www.remotty.net/

    • 技術的な要素 ◦ SPA ◦ チャット、掲示板 ◦ カメラ撮影 ◦ リアルタイム
  5. 8 • 状態管理が煩雑 ◦ data-xxx 、selector がいっぱい ◦ change イベント拾って、ごにょごにょ

    • イベントが分散しがち ◦ 実際に、セレクターが複数の箇所で bind されてたことも。。。 • 技術的なトレンドから遅れてる感(2017年時点) jQuery 辛い
  6. 15 • 機能的に疎結合なパーツに分けて、段階的に置き換え • 順番としては、要素(機能)が少なそうなパーツから着手 ◦ ボタン、イベントの数 ◦ 閲覧 /

    編集 • 順番 ◦ ① アクティビティログ ◦ ② ビデオキャプチャ ◦ ③ チャット ◦ ④ 掲示板一覧 ◦ ⑤ 掲示板 機能の小口化 ① ② ③ / ⑤ ④
  7. 16 jQuery から React へ部分的に置き換え rooms/show.html.haml .room .board-menu - #

    掲示板一覧 .board - # 掲示板 components/room.jsx export default class Room extends React.Component { render() { return ( <div> <ParticipationContainer /> <Board /> <Logs /> </div> ) } }
  8. 18 網羅性の小口化 • すべてのコンポーネントを完全に React には置き換えない ◦ ⇒ イベントハンドリングが複雑、state の管理が大変なものを優先

    • tooltip や popover など複雑な処理がないものは、jQuery を流用 ◦ React への置き換えは後半で説明します
  9. 19 componentDidUpdate() { this.updateParticipationTooltip(); } updateParticipationTooltip() { const $target =

    $(this.tooltipRef); const participation = this.props.participation; const content = `<img src=${participation.iconUrl} class="popover-icon">`; $target.popover('destroy').data('content', content); } render() { return ( <div ref={ ref => this.tooltipRef = ref }> <div className='participation-icon'> /* 省略 */ </div> </div> ) } React と jQuery をミックス props の内容が変わったときに、 popover の中身を更新
  10. 22 class RoomsController < ApplicationController before_action :set_react, only: %i[show] def

    show if react? render 'react' end end def set_react return unless @room.sonicgarden? && params[:react].present? session[:react] = params[:react].to_i end def react? session[:react] == 1 end end フィーチャートグル 限定公開 params で切り替え
  11. 23 def react? session[:react] == 1 || ENV['FORCE_REACT'] end •

    テスト実行時に強制的に react 状態にさせる React のテスト • feature spec は jQuery 版、React 版で共通化 - run: bundle exec rspec - run: FORCE_REACT=1 bundle exec rspec
  12. 24 まとめ • リプレースの小口化 ◦ 機能 / 網羅性 / 影響度

    • React のリプレースは、だいたい 8ヶ月 くらい ▪ 2017/07 ~ 2018/02 • 小口化の効用 ◦ サービスの改善とReact 化を柔軟に対応 ◦ 考える範囲や不安が減って、堅実に安心して進められた
  13. 27 自己紹介 • 氏名:後藤雅之 • GitHub / Qiita: @gotchane •

    株式会社 SonicGarden もうすぐ2年目 • Rails 歴 約1年弱 • 入社してからインフラ→アプリに転向 • 僕も初発表!
  14. 32 課題に対する取り組み 1. Sass → CSS in JS への置き換え 2.

    コンポーネントの振る舞いを jQuery → React に 3. jQuery 版 / React 版での RSpec 並行稼動 4. Rails における React i18n 化
  15. 35 const Button = styled.a` padding: 0.5rem 0; margin: 0.5rem

    1rem; color: white; ` ReactDOM.render( <div> <Button>styled-components</Button> </div>, document.getElementById('root') ) そこで styled-components • CSS in JS の一種で、シンプルに CSS を記述可能 • Sass でできることはほぼ実現可能 タグ付きテンプレートリテラルで囲んだ形
  16. 37 コード置き換え例 entry-badge.sass =badge display: inline-block .entry-badge +badge background: green

    &.is-mention background: red entry-badge.jsx const badge = () => css` display: inline-block; ` const EntryBadge = styled.div` ${badge()} background: ${ props => props.isMention ? 'red' : 'green' }; ` ReactDOM.render( <div><EntryBadge isMention={false} /></div>, document.getElementById('root') ) entry-badge.html.haml .entry-badge = render 'xxx' mixin の利用 props による切り替え
  17. 40 対応方法 • 振る舞いを個別に切り出し、対応ライブラリで置き換え • 新しい振る舞いも追加しやすくなった 振る舞い例 Before After モーダル

    Bootstrap reactjs/react-modal ポップオーバー Bootstrap littlebits/react-popover キーボードショートカット jQuery による実装 jaywcjlove/hotkeys
  18. 41 コード置き換え例 jump_view.html.haml %span.jump-btn = link_to 'Ctrl+k', '#' .jump-modal.modal =

    render 'modal' jump_view.js.coffee $(document).on 'click','.jump-btn',-> $('.jump-modal').modal('toggle') isShortCut = (event) -> event.key == 'k' && event.ctrlKey $ -> $modal = $('.jump-modal') $(document).on 'keydown',(event)-> if isShortCut(event) $modal.modal('toggle') jump_view_button.jsx class JumpView extends React.Component { constructor () { super() this.state = { showModal: false } } onKeyDown = () => {/* ... */} toggle = () => {/* ... */} render () { return ( <JumpViewWrapper> <Hotkeys keyName='Ctrl+k' onKeyDown={this.onKeyDown} /> <JumpModal isOpen={this.state.showModal} onRequestClose={this.toggle} /> </JumpViewWrapper> )} } 振る舞いが閉じた形に
  19. 44 • デザインに依存しない、spec 用のクラスを適用 • spec の再利用・新規追加がスムーズになった menu_container.html.haml .menu__container.spec-menu__container =

    render 'xxx' 対応方法 menu_container.jsx <MenuContainer className="spec-menu__container" > xxx </MenuContainer> spec 用クラスを付与
  20. 45 menu_container_spec.rb feature '掲示板', js: true do scenario '掲示板の表示' do

    expect(page).to have_selector ".spec-menu__container" end end feature spec コード例 spec 用のクラスを指定し、新旧両方で再利用可能に
  21. 47 課題 • React 上では Ruby の I18n.t メソッドが使えない。。 •

    翻訳データは静的ファイルでなく自社 Web サービスで管理 • Web サービスの翻訳データから i18n 化するには?
  22. 49 • 翻訳データを返す GET メソッドを準備し表示 React i18n 化:Webサービスから直接 react-i18next 翻訳データ管理

    Webサービス react i18next- xhr-backend Rails Controller 翻訳データを返すメソッドを準備し、XHR でリクエスト
  23. 50 コード例:翻訳データ取得 translations_controller.rb class TranslationsController < ApplicationController def index @translations

    = SampleTranslationClient .data render json: @translations end end i18n.js import i18n from "i18next" import { reactI18nextModule } from "react-i18next" import Backend from "i18next-xhr-backend" i18n.use(Backend).use(reactI18nextModule) .init({ backend: { loadPath: "./translations.json" , }, whitelist: ["ja", "en"] }) export default i18n 翻訳データを JSON で返すメソッドを準備 XHR でメソッドを実行する定義を追加
  24. 51 コード例:翻訳データ表示 sample_component.jsx import { t } from "i18next" import

    { withNamespaces } from "react-i18next" class Sample extends React.Component { render () { return ( <SampleComponent> {t("sample.component.text" )} </SampleComponent> ) } } export default withNamespaces( "translation")(Sample) キー指定で翻訳データを呼び出し
  25. 52 まとめ • 新と旧を徐々に疎の状態にしていくことを目指す • 課題を一つ一つ丁寧に潰すことで道が見えてくる ◦ Sass には手を入れず styled-components

    で刷新 ◦ jQuery に依存させず、React に振る舞いを閉じ込める ◦ 新旧デザインに依存しないセレクタで spec を活用 ◦ React i18n 化の仕組みを新たに準備
  26. 53 今日のまとめ • リプレースをうまく進めていくコツ、小口化 ◦ 機能:画面をパーツごとに ◦ 網羅性:保守性を上げる目的で及第点を目指す ◦ 影響度:限定公開することで

    • Rails で React を活かした UI/UX への刷新事例 ◦ Sass → CSS in JS への置き換え ◦ コンポーネントの振る舞いを jQuery → React に ◦ jQuery 版 / React 版での RSpec 並行稼動 ◦ Rails における React i18n 化