Slide 1

Slide 1 text

Code splitting in Rails Frontend Turbolinks が dynamic import になるまで pixiv Inc. f_subal 2018/09/04

Slide 2

Slide 2 text

2 誰 ● 2016年新卒入社 ● pixiv投稿画面リニューアル (1月 ~ 4月) ● pixivFACTORY フロントエンド (5月~) ● TypeScript, React, Fluxible, Vue 他 ● 巨大フォームの設計ばかりやってる @f_subal

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

最近やったこと 5

Slide 6

Slide 6 text

6

Slide 7

Slide 7 text

今日はその話ではなく 7

Slide 8

Slide 8 text

Code Splitting + Dynamic Import 8

Slide 9

Slide 9 text

Code splitting ● JS などの バンドルファイルを分割すること ● webpack などモジュールバンドラの機能として提供される ● dynamic import による遅延読み込みを伴う ○ 今回は native ES module の話はしません 9

Slide 10

Slide 10 text

Dynamic Import ● import() 関数を用いたモジュールの遅延読み込み ● 「必要なタイミングで」モジュールをロードすることが可能になる ● 実体は 要素を挿入して、利用可能になったら resolve される Promise 10

Slide 11

Slide 11 text

  // static import   import myUtil from './myUtil'   // dynamic import (with webpack)   import('./myUtil').then(util => ... ) 11

Slide 12

Slide 12 text

● SPA のページ遷移を表現する(遷移時、あるいはその手前で import ) ● 重いモジュールだけを遅延で読み込む ● 1枚の *.bundle.js に全部入り をとにかくやめたい場合に使う ○ SPA じゃなくても有用 12 主なユースケース

Slide 13

Slide 13 text

1枚の *.bundle.js をやめる 13

Slide 14

Slide 14 text

14

Slide 15

Slide 15 text

15

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

● Rails + Turbolinks + Webpack ● Turbolinks は全部 1 枚にバンドルされてる状態のほうが都合が良いらしい ○ pjax による擬似 SPA をすべく、JS はそのままで HTML だけ差し替える設計 ● そんなわけでバンドルファイルは泥団子になる ● 変更の影響範囲も読むのが大変 17 pixivFACTORY の場合

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

  // こういう感じ   document.addEventListener('turbolinks:load', () => { if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname)) { return } // ページによって jQuery だったり React だったりする 11 turbolinks 独自の遷 移イベント 今いるページの location.pathname を正規表現で判定 これのせいで何度か事 故っている

Slide 20

Slide 20 text

20

Slide 21

Slide 21 text

21

Slide 22

Slide 22 text

● 各ページの js ファイルが閉じたモジュールになってほしい ● ページの判定を正規表現でやらないで欲しい ● 必要ないページでは余分な js ファイルを読まないで欲しい 22 どうすると良いか ?

Slide 23

Slide 23 text

● 各ページの js ファイルが閉じたモジュールになってほしい ○ 関数モジュールにリファクタリングする ● ページの判定を正規表現でやらないで欲しい ○ ルーターを入れる ● 必要ないページでは余分な js ファイルを読まないで欲しい ○ ルーティング解決時に dynamic import する 23 こうすると良い

Slide 24

Slide 24 text

● 関数モジュールにリファクタリングする ○ turbolinks を捨てる + 関数 export に変更 ● ルーターを入れる ○ universal-router を導入 ● ルーティング解決時に dynamic import する ○ Webpack の設定をいろいろ変更 + webpacker を捨てる 24 必要なこと

Slide 25

Slide 25 text

✂ というわけで ✂ 25

Slide 26

Slide 26 text

● Turbolinks が邪魔なので消えてもらう ● 一旦 turbolinks:load をただの DOMContentLoaded に変えればいい ○ 旧ブラウザ対応考えるなら ded/domready とか使う ● やると遷移は遅くなるが、一旦許容して進む 26 関数モジュールにリファクタリングする

Slide 27

Slide 27 text

27 関数モジュールにリファクタリングする   // こういう感じ   document.addEventListener('turbolinks:load', () => { if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname)) { return } // ページによって jQuery だったり React だったりする これが邪魔

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

が 29

Slide 30

Slide 30 text

30 本当はこうしたいはず      export const setup = () => { if (!/^\/addresses\/(\d+?\/edit|new)/.test(location.pathname)) { return } // ページによって jQuery だったり React だったりする 関数を export する

Slide 31

Slide 31 text

そのためには 31

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

● 何か react-router とか vue-router とか、特定のビュー実装にくっつきすぎじゃね? ● 「全ページ React に載せないと改善できないように見える」問題 ● 「LPとかFAQとか react 化してもなぁ…(いいんだけど優先度が」 ● jQuery のページ、React のページ、いろいろあるけど全部同じ土俵に乗らないんですか? ○ pixivFACTORY には jQuery のページ、Backboneのページ、React のページ、 Redux のページ、fluxible のページがある。死ぬ。 33 クライアントサイドルーターの悩み

Slide 34

Slide 34 text

kriasoft/universal-router 34

Slide 35

Slide 35 text

● 特定のビュー実装に依存しないルーター ● Universal JavaScript ガチ勢といった作り ● 中は path-to-regexp なので、だいたい express ● ルーティング解決、ミドルウェア、 URL生成 ぐらいしか機能がない ○ ブラウザバックでスクロール位置の復元とか、そういうものは一切ない 35 kriasoft/universal-router

Slide 36

Slide 36 text

  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

Slide 37

Slide 37 text

  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 を渡して解決

Slide 38

Slide 38 text

● universal-router は action() の返り値が undefined | null だと、Not found と見なす ● しょうがないので、setup() の型は params => void ではなく、params => () => void にして いる ○ params を受け取ってできた関数を router.resolve 後に実行 ○ 関数じゃなくて JSX.Element を返すようにすれば、今後このルーターを使って SPAに することも可能ではある(大変だけど 38 注意する点

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

41

Slide 42

Slide 42 text

● 導入はいたって簡単 ○ import() が 構文エラーにならないような設定 をする ○ ビルド済みの ファイルに名前がつくように する ○ import() するコードを書く ○ おわり :) 42 dynamic import を入れるには

Slide 43

Slide 43 text

● Babel の場合 ○ yarn add babel-plugin-syntax-dynamic-import ○ babel-loader あたりに ↑ を読ませる ● TypeScript の場合 ○ tsconfig.json の module: を esnext にする ○ es2015 とかになってるとできない 43 Syntax support for import()

Slide 44

Slide 44 text

● モジュール名に名前をつけるマジックコメント ● import() 関数の場合、文字列が好きに渡せるので、読まれるファイル名が自明ではなくな る ● 何も指定しないと、雑に 0.bundle.js のようなファイルが解決順に作られる ● マジックコメントを指定することで、たとえば my-util.bundle.js にできる 44 /* webpackChunkName: */

Slide 45

Slide 45 text

  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 これで名前を指定する

Slide 46

Slide 46 text

● webpack のバージョンが古すぎて(2.2)、webpackChunkName が使えなかった ● バージョンを上げるには webpacker が邪魔だった ● webpacker を剥がした 46 pixivFACTORY の場合

Slide 47

Slide 47 text

47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

● 全ページルーターに載せきったわけじゃないので今後もやっていく ● 各バンドルを軽くする(webpack 設定に改善の余地が...) ● ページごとの不統一とかもなんとか( LP だけ jQuery で他 Redux を目指す… ) ● とはいえ戦える基盤が整った ○ 俺たちの戦いはこれからだ 49 今後

Slide 50

Slide 50 text

● モジュールがでかいと大変なので切ると良い ● dynamic import は簡単 ● ルーターを入れるほうがプロジェクトによっては困難 ○ とにかく universal に寄せることで上手く着地できる 50 まとめ

Slide 51

Slide 51 text

Code splitting in Rails Frontend Turbolinks が dynamic import になるまで pixiv Inc. f_subal 2018/09/04