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

Turbolinks が dynamic import になるまで / Code splitting in Rails Frontend

2f374df92882247e5985413dad8b39b9?s=47 fsubal
September 05, 2018

Turbolinks が dynamic import になるまで / Code splitting in Rails Frontend

第70回 HTML5とか勉強会「開発環境」にて発表した資料です https://html5j.connpass.com/event/96895/

2f374df92882247e5985413dad8b39b9?s=128

fsubal

September 05, 2018
Tweet

Transcript

  1. Code splitting in Rails Frontend Turbolinks が dynamic import になるまで

    pixiv Inc. f_subal 2018/09/04
  2. 2 誰 • 2016年新卒入社 • pixiv投稿画面リニューアル (1月 ~ 4月) •

    pixivFACTORY フロントエンド (5月~) • TypeScript, React, Fluxible, Vue 他 • 巨大フォームの設計ばかりやってる @f_subal
  3. 3

  4. 4

  5. 最近やったこと 5

  6. 6

  7. 今日はその話ではなく 7

  8. Code Splitting + Dynamic Import 8

  9. Code splitting • JS などの バンドルファイルを分割すること • webpack などモジュールバンドラの機能として提供される •

    dynamic import による遅延読み込みを伴う ◦ 今回は native ES module の話はしません 9
  10. Dynamic Import • import() 関数を用いたモジュールの遅延読み込み • 「必要なタイミングで」モジュールをロードすることが可能になる • 実体は <script>

    要素を挿入して、利用可能になったら resolve される Promise 10
  11.   // static import   import myUtil from './myUtil'   // dynamic import

    (with webpack)   import('./myUtil').then(util => ... ) 11
  12. • SPA のページ遷移を表現する(遷移時、あるいはその手前で import ) • 重いモジュールだけを遅延で読み込む • 1枚の *.bundle.js

    に全部入り をとにかくやめたい場合に使う ◦ SPA じゃなくても有用 12 主なユースケース
  13. 1枚の *.bundle.js をやめる 13

  14. 14

  15. 15

  16. • トップレベルの JS ファイルで全部 import はありがち • バンドルサイズが不要に大きくなる • モジュール境界が不明になり、影響範囲も読みづらくなる

    16 1枚の *.bundle.js をやめる
  17. • Rails + Turbolinks + Webpack • Turbolinks は全部 1

    枚にバンドルされてる状態のほうが都合が良いらしい ◦ pjax による擬似 SPA をすべく、JS はそのままで HTML だけ差し替える設計 • そんなわけでバンドルファイルは泥団子になる • 変更の影響範囲も読むのが大変 17 pixivFACTORY の場合
  18.   // こういう感じ   document.addEventListener('turbolinks:load', () => { if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname)) { return

    } // ページによって jQuery だったり React だったりする 11
  19.   // こういう感じ   document.addEventListener('turbolinks:load', () => { if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname)) { return

    } // ページによって jQuery だったり React だったりする 11 turbolinks 独自の遷 移イベント 今いるページの location.pathname を正規表現で判定 これのせいで何度か事 故っている
  20. 20

  21. 21

  22. • 各ページの js ファイルが閉じたモジュールになってほしい • ページの判定を正規表現でやらないで欲しい • 必要ないページでは余分な js ファイルを読まないで欲しい

    22 どうすると良いか ?
  23. • 各ページの js ファイルが閉じたモジュールになってほしい ◦ 関数モジュールにリファクタリングする • ページの判定を正規表現でやらないで欲しい ◦ ルーターを入れる

    • 必要ないページでは余分な js ファイルを読まないで欲しい ◦ ルーティング解決時に dynamic import する 23 こうすると良い
  24. • 関数モジュールにリファクタリングする ◦ turbolinks を捨てる + 関数 export に変更 •

    ルーターを入れる ◦ universal-router を導入 • ルーティング解決時に dynamic import する ◦ Webpack の設定をいろいろ変更 + webpacker を捨てる 24 必要なこと
  25. ✂ というわけで ✂ 25

  26. • Turbolinks が邪魔なので消えてもらう • 一旦 turbolinks:load をただの DOMContentLoaded に変えればいい ◦

    旧ブラウザ対応考えるなら ded/domready とか使う • やると遷移は遅くなるが、一旦許容して進む 26 関数モジュールにリファクタリングする
  27. 27 関数モジュールにリファクタリングする   // こういう感じ   document.addEventListener('turbolinks:load', () => { if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname))

    { return } // ページによって jQuery だったり React だったりする これが邪魔
  28. 28 Turbolinks をやめる      document.addEventListener('DOMContentLoaded', () => { if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname))

    { return } // ページによって jQuery だったり React だったりする まずは単に DOMContentLoaded にする
  29. が 29

  30. 30 本当はこうしたいはず      export const setup = () => {

    if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname)) { return } // ページによって jQuery だったり React だったりする 関数を export する
  31. そのためには 31

  32. • 各ページを関数モジュールをした場合、それを受ける層が必要 • ここで router が必要になる • 各ページを関数モジュールに変更し、ルーターがそれを受け取る設計にする • つまり、関数モジュール化を完遂するにはルーターに載せる必要がある

    32 ルーターに載せる
  33. • 何か react-router とか vue-router とか、特定のビュー実装にくっつきすぎじゃね? • 「全ページ React に載せないと改善できないように見える」問題

    • 「LPとかFAQとか react 化してもなぁ…(いいんだけど優先度が」 • jQuery のページ、React のページ、いろいろあるけど全部同じ土俵に乗らないんですか? ◦ pixivFACTORY には jQuery のページ、Backboneのページ、React のページ、 Redux のページ、fluxible のページがある。死ぬ。 33 クライアントサイドルーターの悩み
  34. kriasoft/universal-router 34

  35. • 特定のビュー実装に依存しないルーター • Universal JavaScript ガチ勢といった作り • 中は path-to-regexp なので、だいたい

    express • ルーティング解決、ミドルウェア、 URL生成 ぐらいしか機能がない ◦ ブラウザバックでスクロール位置の復元とか、そういうものは一切ない 35 kriasoft/universal-router
  36.   import UniversalRouter from 'universal-router'   const router = new UniversalRouter([   

    {    path: 'books/orders/:id',    async action ({ params }) =>    await import('./pages/books/orders).then(page => page.setup(params.id))    }   ])   document.addEventListener('DOMContentLoaded', async () =>    const handler = await router.resolve(location.pathname)    handler() 11
  37.   import UniversalRouter from 'universal-router'   const router = new UniversalRouter([   

    {    path: 'books/orders/:id',    async action ({ params }) =>    await import('./pages/books/orders).then(page => page.setup(params.id))    }   ])   document.addEventListener('DOMContentLoaded', async () =>    const handler = await router.resolve(location.pathname)    handler() 11 path-to-regexp 記 法で解決 ここで dynamic-import location.pathname を渡して解決
  38. • universal-router は action() の返り値が undefined | null だと、Not found

    と見なす • しょうがないので、setup() の型は params => void ではなく、params => () => void にして いる ◦ params を受け取ってできた関数を router.resolve 後に実行 ◦ 関数じゃなくて JSX.Element を返すようにすれば、今後このルーターを使って SPAに することも可能ではある(大変だけど 38 注意する点
  39. 39   export const setup = (id: string) => () =>

    { // この中にページ固有の処理を記述   }
  40. 40   export const setup = (id: string) => () =>

    { // この中にページ固有の処理を記述   } 各ページはコレさえexport していれば中は何でもいい 中が ReactDOM.render だろうが、$(...).on だろうが、 new SomeWidget() だろうが関係ない 外からの interface を保ったまま React 化を進められる
  41. 41

  42. • 導入はいたって簡単 ◦ import() が 構文エラーにならないような設定 をする ◦ ビルド済みの ファイルに名前がつくように

    する ◦ import() するコードを書く ◦ おわり :) 42 dynamic import を入れるには
  43. • Babel の場合 ◦ yarn add babel-plugin-syntax-dynamic-import ◦ babel-loader あたりに

    ↑ を読ませる • TypeScript の場合 ◦ tsconfig.json の module: を esnext にする ◦ es2015 とかになってるとできない 43 Syntax support for import()
  44. • モジュール名に名前をつけるマジックコメント • import() 関数の場合、文字列が好きに渡せるので、読まれるファイル名が自明ではなくな る • 何も指定しないと、雑に 0.bundle.js のようなファイルが解決順に作られる

    • マジックコメントを指定することで、たとえば my-util.bundle.js にできる 44 /* webpackChunkName: */
  45.   import UniversalRouter from 'universal-router'   const router = new UniversalRouter([   

    {    path: 'books/orders/:id',    async action ({ params }) =>    await import(/* webpackChunkName: “order” */ './pages/books/orders).then(..)    }   ])   document.addEventListener('DOMContentLoaded', async () =>    const handler = await router.resolve(location.pathname)    handler() 11 これで名前を指定する
  46. • webpack のバージョンが古すぎて(2.2)、webpackChunkName が使えなかった • バージョンを上げるには webpacker が邪魔だった • webpacker

    を剥がした 46 pixivFACTORY の場合
  47. 47

  48. • Q. 細かくバンドル切りまくるとモジュール読み込み回数増えない? • A. もちろん増える。HTTP/2 でない環境だと辛いかもしれない ◦ pixivFACTORY はすでに

    HTTP/2 だった 48 一応留意する点
  49. • 全ページルーターに載せきったわけじゃないので今後もやっていく • 各バンドルを軽くする(webpack 設定に改善の余地が...) • ページごとの不統一とかもなんとか( LP だけ jQuery

    で他 Redux を目指す… ) • とはいえ戦える基盤が整った ◦ 俺たちの戦いはこれからだ 49 今後
  50. • モジュールがでかいと大変なので切ると良い • dynamic import は簡単 • ルーターを入れるほうがプロジェクトによっては困難 ◦ とにかく

    universal に寄せることで上手く着地できる 50 まとめ
  51. Code splitting in Rails Frontend Turbolinks が dynamic import になるまで

    pixiv Inc. f_subal 2018/09/04