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

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

fsubal
September 05, 2018

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

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

fsubal

September 05, 2018
Tweet

More Decks by fsubal

Other Decks in Programming

Transcript

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

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

  3. 4

  4. 6

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

    dynamic import による遅延読み込みを伴う ◦ 今回は native ES module の話はしません 9
  6.   // static import   import myUtil from './myUtil'   // dynamic import

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

    に全部入り をとにかくやめたい場合に使う ◦ SPA じゃなくても有用 12 主なユースケース
  8. 14

  9. 15

  10. • Rails + Turbolinks + Webpack • Turbolinks は全部 1

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

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

  13. 21

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

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

    ルーターを入れる ◦ universal-router を導入 • ルーティング解決時に dynamic import する ◦ Webpack の設定をいろいろ変更 + webpacker を捨てる 24 必要なこと
  16. • Turbolinks が邪魔なので消えてもらう • 一旦 turbolinks:load をただの DOMContentLoaded に変えればいい ◦

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

    { return } // ページによって jQuery だったり React だったりする まずは単に DOMContentLoaded にする
  18. 30 本当はこうしたいはず      export const setup = () => {

    if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname)) { return } // ページによって jQuery だったり React だったりする 関数を export する
  19. • 何か react-router とか vue-router とか、特定のビュー実装にくっつきすぎじゃね? • 「全ページ React に載せないと改善できないように見える」問題

    • 「LPとかFAQとか react 化してもなぁ…(いいんだけど優先度が」 • jQuery のページ、React のページ、いろいろあるけど全部同じ土俵に乗らないんですか? ◦ pixivFACTORY には jQuery のページ、Backboneのページ、React のページ、 Redux のページ、fluxible のページがある。死ぬ。 33 クライアントサイドルーターの悩み
  20. • 特定のビュー実装に依存しないルーター • Universal JavaScript ガチ勢といった作り • 中は path-to-regexp なので、だいたい

    express • ルーティング解決、ミドルウェア、 URL生成 ぐらいしか機能がない ◦ ブラウザバックでスクロール位置の復元とか、そういうものは一切ない 35 kriasoft/universal-router
  21.   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
  22.   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 を渡して解決
  23. • universal-router は action() の返り値が undefined | null だと、Not found

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

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

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

  27. • Babel の場合 ◦ yarn add babel-plugin-syntax-dynamic-import ◦ babel-loader あたりに

    ↑ を読ませる • TypeScript の場合 ◦ tsconfig.json の module: を esnext にする ◦ es2015 とかになってるとできない 43 Syntax support for import()
  28.   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 これで名前を指定する
  29. 47