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

React / Hypernova + Rails で SSR 速習会

3846masa
November 14, 2017

React / Hypernova + Rails で SSR 速習会

3846masa

November 14, 2017
Tweet

Other Decks in Technology

Transcript

  1. ハンズオンセットアップ Requirements Node.js >= v8 Ruby >= 2.3.0 Postgresql https://github.com/wantedly/rails-react-

    demo/pull/5 git clone --depth 1 -b study-start https://github.com/wantedly/rails-react-demo bin/setup && npm i or bin/setup && yarn ./db/seeds.sh
  2. New Wantedly frontends Server-side Rendering Hypernova, CSS in JS etc.

    RESTful API + BFF-like API ActiveModelSerializer Dynamic import webpack, Routing etc. TypeScript, and so on...
  3. FMP の改善 あらかじめ HTML をレンダリングして返す JavaScript を読み込む前に表示できる Critical Rendering Path

    を最適化する クライアントのレンダリングに必要な情報を 極力少ないリクエストで返すようにする
  4. Critical Rendering Path <html> <head><link href="style.css" rel="stylesheet"></head> <body><p>Hello World!</p></body> </html>

    https://developers.google.com/web/fundamentals/performanc e/critical-rendering-path/analyzing-crp
  5. CSS は レンダリングブロック リソー ス 外部 CSS を呼び出すと、 ラウンドトリップが発生 ラウンドトリップ中はレンダリングが止まる

    https://developers.google.com/web/fundamentals/performanc e/critical-rendering-path/render-blocking-css ブラウザは、DOM と CSSOM の両方が揃うまで レンダリングをブロックします。
  6. Server-side CSS Rendering SSR でリクエストされたペー ジに必要な分だけ CSS を生成して一緒に配信する インライン CSS

    で リクエスト( ラウンドトリップ) を少なく あらかじめ HTML + CSS を送ることで JavaScript が読み込まれる前でも閲覧できる
  7. Hypernova // サー バ側とクライアント側の関数を作る export default hypernova({ server: () =>

    (state) => { const html = ReactDOMServer.renderToString(<App/>); return hypernova.serialize("App", html, state); }, client: () => { const [{ node, data }] = hypernova.load('App'); ReactDOM.hydrate(<App/>, node); } }); <%= render_react_component "App", @react_state %> <%= javascript_include_tag 'main' %>
  8. Rails + Hypernova Fail-safe な設計 Node.js サー バが落ちても Client JS

    だけで動く 非インタラクティブなペー ジは Rails のみにできる 単純なペー ジは Rails + erb でサクッと書ける
  9. ハンズオンを起動しよう 3 つのスクリプトを動かす bundle exec rails s yarn start webpack

    による Client JS のビルド yarn run hypernova Hypernova による SSR サー バ 今回は nodemon + babel-node で動かす https://localhost:3000/projects にアクセス
  10. First meaningful paint を体感 Chrome の DevTools で帯域幅を絞ってみる Network ->

    Online を Slow 3G に変更 段階的に表示されていくはず ヘッダー(Rails) Top Page 見出し(Client JS) プロジェクト一覧(API Request) SSR 後と比較するために HTML ソー スを見ておく
  11. SSR と CSS React の SSR は JavaScript で実行される CSS

    を JavaScript として読む必要がある CSS を JS として読み込む方法 CSS in JS を使う CSS Modules でトランスパイルする 今回は CSS in JS を使っていきます
  12. CSS in JS JavaScript のオブジェクトとして CSS を書く const styles =

    { base: { width: 640, margin: 'auto', fontSize: '1.5rem' } }; const Component = () => (<div style={styles.base} />); React の style はこの形式 トランスパイルせずに Isomorphic な コンポー ネントを作れる
  13. react-with-styles ThemedStyleSheet.registerTheme({ color: { black: '#212121' } }); const App

    = ({ styles }) => ( <h1 {...css(styles.header)}>Hello World!</h1> ); export default withStyles(({ colors }) => ({ header: { fontWeight: 600, color: colors.black } }))(App); https://github.com/airbnb/react-with-styles
  14. CSS in JS も Hypernova https://github.com/wantedly/rails-react- demo/commit/39f9f238aaa2df1a0e3626c747bda 36cb59aba8e frontend/src/index.jsx //

    return markup; return `${style}\n${markup}\n${classNames}`; 再読込するとどうなるか スタイルも適応した表示が最初に出る (Rails + Hypernova + CSS in JS)
  15. 余談 | CSS Modules 普段の CSS 記法で書いたものを import する import

    style from './style.css'; CSS 自体を Webpack などでトランスパイルする Server-side でもトランスパイルする必要がある isomorphic にするのが難しい SSR + CSS Modules したいときは? kriasoft/isomorphic-style-loader Webpack の loader で isomorphic に変換
  16. API 設計 API を RESTful に保ちながら SSR を実現したい Ref. https://wantedly.connpass.com/event/55138/

    Web / ネイティブアプリで共通化 SPA-like な React 設計にすれば共通化できる SSR でペー ジをレンダリングするためには 複数のデー タを一括で呼び出したい Backends for Frontends のような考えかた RESTful API & BfF-like API を提供したい
  17. ActiveModelSerializers ActiveModel から JSON API を作成できる app/serializers/project_serializer.rb より抜粋 class ProjectSerializer

    < ApplicationSerializer attributes :id, :title, :published_at attribute(:canonical_path) { "/projects/#{object.id}" } belongs_to :company has_one :image end 詳しくは以前の速習会 https://wantedly.connpass.com/event/55138/ を参照
  18. API 設計 クエリパラメタで eld や association を設定 http://localhost:3000/api/v2/projects? fields=title,description http://localhost:3000/api/v2/projects?

    fields=canonical_path 必要なデー タだけを取ってくる View に依存しない API 設計 https://wantedly.connpass.com/event/55138/
  19. React の initialState を作る SSR するときの初期デー タをどう用意するか Hypernova は初期デー タを渡せる

    ( @react_state ) ActiveModelSerializers を流用したい controller / action ごとに必要な elds を定義して おく app/views/projects/index.yml ActiveModuleSerializers 経由で JSON を作る ReactStateRenderer を作る lib/react_state_renderer.rb
  20. 現在の Redux state { projects: { index: { projects: [

    { id: 123, company: {...}, description: '...' }, { id: 456, company: {...}, description: '...' }, ], filter: { ... } } } }
  21. デー タを再利用する ペー ジを遷移時に API ロー ドに時間がかかる Slow 3G にしてペー

    ジ遷移してみる ロー ドが終わるまで何も表示されない... orz 一覧ペー ジ → 詳細ペー ジならデー タが既にある タイトルやサムネイル情報は 一覧ペー ジでも詳細ペー ジでも使う
  22. normalizr API などからのデー タを Entity にわける 例) post と post.author

    を post + author に Entity の形式は同じになるため、 デー タが重複しない 例) posts[0].author と posts[2].author が同じ デー タが形式立って一箇所にまとまるので 管理しやすい 追加のデー タを merge するのとかが楽 https://github.com/paularmstrong/normalizr
  23. normalize { id: 10, title: 'Post Title', tags: [ {

    id: 8, name: 'awesome' } ] } { result: 10, entities: { posts: { 10: { id: 10, title: 'Post Title', author: 8 } }, tags: { 8: { id: 8, name: 'awesome' } } } }
  24. ActiveModel と normalizr ActiveModel と normalizr scheme を対応させて 自動で normalize

    したい 再帰的に normalize をかける 今回の API では _entity_type に ActiveModel のモデル名を入れて対応させる See frontend/src/common/utils/normalizer.js
  25. normalizr を設定しよう commit/4b147052052ae340b5a451b37e534d9c 0f5e18b0 frontend/src/common/utils/generateStateFromAPIData.js import { normalize } from

    "./normalizer"; const { result: currentState, entities } = normalize(initial.body); const state = Object.assign({}, initial.global, { entities }); /* ... */ state[initial.controller][initial.action] = currentState;
  26. reselect frontend/src/projects/selectors/projectsSelector.js export const projectSelector = createSelector( // 必要なデー タを抽出

    [ (_state, ownProps) => ownProps.match.params.id, state => state.entities ], // 抽出デー タから denormalize (projectId, entities) => denormalize(projectId, ProjectSchema, entities) );
  27. 最終的な Redux state { entities: { projects: [ { id:

    1, author: 10, content: '...' } ], user: [ { id: 10, name: 'John Smith' } ], ... }, projects: { index: { projects: [ 1, 8, 10, ... ], filter: { ... } } } }
  28. デー タが再利用できているか 一旦一覧ペー ジでリロー ド( デー タ初期化) 一覧ペー ジ →

    詳細ペー ジを表示してみる Slow 3G で表示してみる タイトルやサムネイルは最初から表示されている API 読み込みが終わると全部表示される
  29. Immutable.js 変更すると新しいオブジェクトを返す // ESnext const newState = { ...prevState, title:

    'New' }; // Immutable.js const newState = prevState.set('title', 'New'); mergeDeep で深い階層まで新しいオブジェクトに const normalized = normalize(apiData, apiSchema); const newState = prevState.mergeDeep(normalized); https://facebook.github.io/immutable-js/
  30. Immutable.Record Record 型でクラス化できる モデル構造( 型情報) を持てる メソッドを持てる class Post extends

    Immutable.Record({ title: '', tags: Immutable.List(), }) { public readonly title: string; public readonly tags: Immutable.List; addTag(tag) { return this.set('tags', this.tags.push(tag)); } }
  31. normalizr + Immutable.js const normalized = normalize(apiData, apiSchema); const entities

    = fromJS(normalized.entities); // Convert object to Immutable.Record entities.map((entitiesMap, key) => { return entitiesMap.map(val => new Records[key](val)); }); const newState = prevState.deepMerge({ entities }); // Get post record whose id is 10 const post = denormalize(10, Post, state.entities); post.title; // typeof string post.tags.get(0); // instanceof Tag const newPost = post.addTag('awesome');
  32. Routing 必要なコンポー ネントだけを読み込みたい Dynamic import import('./App').then(({ default: App }) =>

    { ... }); Dynamic import と React Component を合わせる const route = { loader: () => import('./ProjectContainer') }; <AsyncComponent route={route} />
  33. AsyncComponent class AsyncComponent extends React.Component { componentDidMount() { const {

    route } = this.props; route.loader().then(({ default: App }) => { this.view = App; this.forceUpdate(); }); } render() { const { route, ...rest } = this.props; const Component = this.view || 'div'; return <Component {...rest} />; } }
  34. Dynamic import + SSR ReactDOMServer.renderToString は非同期を待たない import() が待機されない あらかじめ import()

    をすべて読みこんでおく Hypernova の getComponent は非同期 async getComponent() { await Promise.all(routes.map((r) => r.loader())); return Component; }
  35. Dynamic import してみる https://github.com/wantedly/rails-react- demo/commit/96073c316992d68b373af8fc5f2b3 a5a7f6b1556 frontend/src/router.jsx import ... from

    を消して import(...) にする new AsyncRoute({ path: "/projects", loader: () => import("./projects/router"), }),
  36. Code splitting via webpack Code splitting で ペー ジごとにスクリプトを分割する webpack

    は import() 構文から 自動で Code splitting する機能がある 静的解析なため、 import(scriptName) のような 変数をつかった読み込みには対応していない https://webpack.js.org/guides/code-splitting/#dynamic- imports
  37. React + webpack 環境 create-react-app をベー スに環境構築 Code splitting などは標準で付いている

    eject で webpack の設定を書き換えられる HMR や SSR 、CSS Modules は入っていない 必要なものだけ別途設定する 今回 SSR は webpack を使わない HMR は gaearon/react-hot-loader を利用
  38. TypeScript の導入 静的型をつけることでコー ド補完を充実させる DEMO CSS in JS Redux の

    Action も補完が効くように switch - case 文で型推論される DEMO Action の補完 Immutable.Record を使うと Entity のプロパティに補完が効くようになる DEMO Immutable.Record のコー ド補完
  39. まとめ Hypernova で React + Rails の SSR CSS in

    JS で First Meaningful Paint の改善 State は ActiveModelSerializers で生成 ActiveModel から RESTful API と BfF-like API normalizr と Immutable.js で Entity 管理 Dynamic import で Component を遅延読み込み webpack による Code splitting TypeScript で型のある幸せな世界へ