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

Vueと共に走ったフロントエンドリプレイス1年間

komiyamast
January 30, 2019

 Vueと共に走ったフロントエンドリプレイス1年間

【Nuxt.js/Vue.js】スタートアップ企業導入事例
https://re-build.connpass.com/event/111146/

komiyamast

January 30, 2019
Tweet

Other Decks in Technology

Transcript

  1. 目次 • 自己紹介 • 概要 • 技術構成 • コンポーネントとルーティング設計 •

    データフロー設計 • テスト • 1年かけた学び • おまけ
  2. 自己紹介 • 小宮山智也(Komiyama Tomoya)@株式会社スタディスト • 最近の趣味 ◦ ランニング ◦ ウォーキング

    ◦ ボルダリング • 近況報告 ◦ VSCodeからNeovimに乗り換えた 2017年10月 2018年11月
  3. 概要 • 開発環境 ◦ Docker、Webpack(3系)、Babel、Eslint、Sass、PostCSS • テスト ◦ Karma、Mocka、power-assert、Storybook •

    フレームワーク ◦ Vue、Vuex、vue-router • 使っていない/やっていないもの ◦ Nuxt、TypeScript、Webpacker、E2E
  4. 利用パッケージ① "dependencies": { "@storybook/addons": "", "axios": "", "chart.js": "", "encoding-japanese":

    "", "es6-promise": "", "jquery": "", "jquery-ui": "", "marked": "", "pdfjs-dist": "", "raphael": "", "ress": "", "storybook-router": "", "vue": "", "vue-analytics": "", "vue-chartjs": "", "vue-i18n": "", "vue-loader": "", "vue-router": "", "vue-slider-component": "", "vue-template-compiler": "", "vue-touch": "", "vuedraggable": "", "vuejs-datepicker": "", "vuex": "", "webpack-merge": "", "zxcvbn": "" }, • deb or 非debの区分けは適当 ◦ ライブラリとして配布するわけではな いので気にしていない ◦ webアプリとしていい感じの運用方法 があるなら知りたい • jqueryはリプレイスしきれなかった 過去実装の残骸
  5. "eslint-plugin-html": "", "eslint-plugin-import": "", "eslint-plugin-node": "", "eslint-plugin-promise": "", "eslint-plugin-standard": "",

    "eslint-plugin-vue": "", "extract-text-webpack-plugin": "", "file-loader": "", "jsdom": "", "json-loader": "", "karma": "", "karma-chrome-launcher": "", "karma-firefox-launcher": "", "karma-ie-launcher": "", "karma-jsdom-launcher": "", "karma-mocha": "", "karma-mocha-reporter": "", "karma-opera-launcher": "", "karma-safari-launcher": "", "karma-webpack": "", "devDependencies": { "@storybook/addon-actions": "", "@storybook/addon-knobs": "", "@storybook/addon-links": "", "@storybook/vue": "", "aglio": "", "axios-mock-adapter": "", "babel-core": "", "babel-eslint": "", "babel-loader": "", "babel-plugin-espower": "", "babel-plugin-transform-runtime": "", "babel-preset-env": "", "css-loader": "", "drakov": "", "eslint": "", "eslint-config-standard": "", "eslint-config-vue": "", "eslint-friendly-formatter": "", "eslint-loader": "", 利用パッケージ② "mocha": "", "node-sass": "", "postcss-cssnext": "", "postcss-smart-import": "", "power-assert": "", "sass-loader": "", "style-loader": "", "url-loader": "", "vue-style-loader": "", "webpack": "", "webpack-dev-server": "", "webpack-manifest-plugin": "" }
  6. Atomic Design - src - components - atoms - molecules

    - organisms - pages - routerLayouts • ディレクトリ構成は右図 • routerLayoutsは独自のもの(後述) • templatesは設けていない ◦ SPAだとページ内容は動的に流し込むので、 pagesと実質 同じという記事をどこかで読んだ(見失った) • コンポーネントの粒度とディレクトリの表示順が一 致するのが地味に有効
  7. 良かったこと • 必ずApp.vueがルートコンポーネントになる ◦ 全画面で共通する処理を組み込みやすい ▪ フラッシュメッセージ、アラートダイアログ、ローディング • レイアウトを統一できる ◦

    レイアウトに関わるCSSが散在しない ◦ レイアウト種類は想像よりも少ない • ファイル数が減る(?) ◦ 共通処理化、レイアウト統一の恩恵 ◦ (調べたら.vueファイルが102あった、画面数は77、全然減ってない)
  8. 画面の基点 • contentが実質的に基点となっている ◦ 初期データfetchもここで行っている ◦ 元々の設計意図ではなく、なし崩し的にそうなった ◦ ルーティングに依存させたくない意識があった •

    各パーツを跨ぐ処理が難しくなる ◦ headerなどはrouterにより分離されているので、 contentから 制御できない ◦ pluginやstoreなどグローバルな存在に頼らざるを得ない ◦ 例) ▪ パワポのようなサイドメニューでパネルを選んだら、メイ ン部分をそこまでスクロールさせる App.vue
  9. • contentを画面の基点と認める ◦ pagesは画面の基点置き場とする ◦ ディレクトリ構造をルーティングに合わせる ◦ 実質そうなりつつあるが、設計の前提にしていたらもっと良く なったと悔やまれる •

    共通レイアウトを見極める ◦ パーツまではめ込んだものをレイアウトにする ◦ content部分だけをslot(router-view)ではめ込む ◦ contentと連動していると感じたら共通レイアウト化はさっさ と諦める ◦ 複雑でダイナミックな画面も共通レイアウト化はさっさと諦め る こうすればもっと良くなったかも(未検証) slot - pages - folders - FolderList.vue - FolderProfile.vue - users - UserList.vue - UserProfile.vue
  10. レイアウトの<router-view>ロックイン • 共通レイアウトで作ったが、共通にできない独自な画面であることが後から判明す ることもある ◦ 例)contentとsidebarを跨ぐ制御が必要になったことが実際にあった ◦ 共通の親がいればpropsと$emitで制御できるのに、その親がいない ▪ <router-view>で分断されてしまっている

    • 共通レイアウトから分離するコストが高い ◦ 独自制御はあるが、見た目のレイアウトは共通と同じ ◦ <router-view>が<slot>にさえなっていれば使い回せる(はず) ▪ → レイアウトを常に<slot>で作り、それを<router-view>でラップする存在がいたら解決した のではないか?
  11. ディレクトリ構造改善案(未検証) • <slot>利用の純粋なスタイル付きレイアウトを organismsに置く • pagesには各画面のメインとなるコンポーネントを ルーティングのネストに沿って置く • レイアウトを利用し、<router-view>や共通パーツを 配置した存在をpageLayoutsに置く

    ◦ <style>は不要 ◦ <script>は最低限 - organisms - layouts - HSCLayout.vue - HCLayout.vue - pages - folders - FolderList.vue - FolderProfile.vue - users - UserList.vue - UserProfile.vue - routingLayouts(pageLayouts) - MainLayout.vue - settings - FolderLayout.vue - UserLayout.vue
  12. 非ドメインなデータ • 全てのデータがドメインとは限らない ◦ APIからもらえるものはほぼドメインとみなして問題ない • 画面都合で一時的に保持したい状態がある ◦ 通信中のローディング表示をおこなうための状態 ◦

    エラーメッセージを表示するための状態 • Vuexはドメインデータに完全特化させるため、非ドメインなデータはstoreに保管し ない ◦ 単一storeというFluxの理想を追いすぎない
  13. 非ドメインなデータ置き場 • 各コンポーネントのdata ◦ コンポーネントに閉じるようなデータならばここに置くのが原則 ◦ 一覧データのページネーション、ドロップダウン開閉状態、保存前のフォーム入力など • Vueのplugin機能 ◦

    コンポーネントを跨ぐようなデータはここに置く ◦ モーダルなローディング状態、アラートメッセージなど • コンポーネントのレイアウト構成に失敗すると、その画面でしか使わないのに pluginに状態を置かざるを得ない状況が生まれてしまう ◦ レイアウト構成を見直すべし
  14. - store/ - folders/ - users/ - actions.js - actionTypes.js

    - getters.js - getterTypes.js - index.js - mutations.js - mutationTypes.js - users/ - folders/ - index.js store構成 • ディレクトリ構成例は右を参照 ◦ ドメインごとにディレクトリを作る ◦ 従属リソースはディレクトリをネストする ◦ モジュール毎のファイル構成は同一 • モジュール数は24だった ◦ 画面ごとにモジュールを作るよりは少なく済んだはず ◦ 画面で忠実に分けたなら 77必要だったかもしれない
  15. 従属リソース • フォルダに所属しているユーザーを folders/users というモジュールに切り分けて いる ◦ ユーザーというドメインに従うなら、 usersに全てまとめるべきかもしれない •

    分けているのは実践的な理由 ◦ フォルダ所属ユーザー一覧画面では、所属ユーザーの一覧と、所属させたい候補ユーザーの一覧 を同時に表示する必要がある →「ユーザー一覧」が2つ必要 ◦ 他の従属リソースも大抵同様の画面がある ◦ usersにまとめると、「ユーザー一覧」が usersのstateに大量発生してしまう ◦ 付随してactionやmutationも増えていく ◦ 切り出してしまったほうが見通しが良い
  16. store定数 • actionTypes、getterTypes、mutationTypesは関数名の定数定義 ◦ actionは永続化データの変更を伴うので、 CREATE/DELETE/UPDATEなどの命名 ◦ mutationはstore内データの変更なので、 ADD/REMOVE/UPDATEなどの命名 ◦

    getterはドメインモデル名そのまま(右参照) • 良かったこと ◦ 利用箇所をgrepで探しやすい • 悪かったこと ◦ ファイル数とimportが増える ▪ 1ファイルにまとめてみる ▪ 右のようなtypes.jsを設置してみる
  17. 一覧データと詳細データ • データ置き場は分けている(右図) ◦ 一覧 = users#index ◦ 詳細 =

    users#show • モデルも異なるものとして扱う ◦ 一覧をベースに、プロパティを追加したものが詳細 ▪ 一覧:id、name、icon、createdAt… ▪ 詳細:一覧+α(自己紹介とか) 一覧 詳細 一覧 詳細 V.S.
  18. 一覧画面から詳細画面への遷移 • 一覧画面 ◦ usersをfetch ▪ ページネーションやソート情報はコンポーネントが管理 • URLクエリとの連携もコンポーネント側で管理 ▪

    ページネーションなしの総数( users_size)はstateに保管 ◦ 詳細表示したいユーザーを選択したら、その idと共に$router.push • 詳細画面 ◦ routerからprops経由でidを受け取る ◦ userをfetch ◦ stateには一覧が残ったままだが気にしない ◦ userに更新を加えても一覧側には反映しない ▪ 一覧画面に遷移したら usersのfetchが常に走らせる 初期データfetchは遷移先画面の役割 => 画面リロードしても同じ処理で済む
  19. 詳細データfetch工夫の歴史(中期) • 新規の詳細データfetchを行うときに、前回データのクリアを行った ◦ 前回データが表示される問題は解決 • 問題 ◦ 詳細データが必要な画面は複数存在したりする ◦

    それらの画面間で遷移した場合、毎回詳細データクリアが挟まり UXが悪い ▪ コンポーネントはv-ifで描画制御を行っていたりするため stateクリアmutation (gif)
  20. 詳細データfetchのコンポーネント側 • getterからモデルを取得 • モデルロードをactionに要求 ◦ mountedなどで実行する • ロードやアラートなど、画面都合の処理はコンポーネントが担当 •

    アンダーバー付き命名は独自ルール ◦ 見分けやすい ◦ 基本的に直実行はしない ◦ 前後処理をラップした methodを作る ◦ (Vue的にはアンダーバー非推奨かも)
  21. ユニットテスト • Karma、Mocka、power-assertで構成 • Vuexのactions、getters、mutationsは網羅 • utilとして切り出した処理は網羅 • .vueファイルはやっていない ◦

    開発初期にわりきってしまった ◦ storybookで頑張る方針 ◦ 難しい処理は書かない &utilに切り出してテスト ▪ 権限判定、バリデーションなど
  22. コンポーネントテスト • Storybookで.vueファイルを網羅 ◦ 入力(props)による分岐はなるべく網羅 • 全画面をモックで表示 ◦ バックエンドなしの完全モック ▪

    独立したフロントエンドの実現 ◦ knobを使って権限やプランを切り替え可能にした ▪ 全パターン網羅は辛すぎた ▪ せめて切り替えて任意の条件を再現できるようにした
  23. How: Vuexモック • APIとの対話はVuexに集約されている ◦ ドメイン駆動Vuex ◦ VuexをモックできればAPIを丸ごとモックしたことになる • コンポーネントとVuexのインタフェースはactionsとgettersのみ

    ◦ actionsは return Promise.resolve() すればいいだけ ▪ モックなので全部正常系にしてしまう ▪ 非同期間が欲しければ setTimeout を挟む ▪ 一部 resolve でデータを返している actionがあったので、それは都度対応が必要 ◦ gettersはモックのモデルを返せばいいだけ • 最初は手間だが、storeの丸ごとモックを一度作ってしまえば全画面で使うことがで きる
  24. 闇のextends V.S. 薄闇のmixins • コンポーネントは親に対してpropsと$emitというインタフェースを公開している • その他のdata、computed、methodsは全てprivateな存在 • extendsはこの原則に横穴を開けてしまう ◦

    継承されていると知らずに修正したことで不具合にしばしば遭遇した ◦ コンポーネントは閉じた存在という意識が強いため、横穴に気付くのが非常に難しい • mixinsであれば、横穴が存在することが前提なので危険が減る • どちらも使わないに越したことはない ◦ slotで表現できないか十分に検討するべき ◦ slotを制する者はVueを制する ◦ どうしても無理なら最後の手段で mixins
  25. 禁断の /deep/ • scopedなスタイルを外側からこじ開けて上書きできる禁術 ◦ https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html • 使っていいシーンは非常に少ない ◦ 外部ライブラリによるコンポーネントスタイルをどうしても変更したいとき

    ◦ v-htmlなどで動的にdom生成してスタイルを当てたいとき • 軽い気持ちで絶対に使ってはいけない ◦ 「この画面でだけマージンがちょっと違うから /deep/で変えよう」→NG ◦ 「フォントサイズだけ変えれば使い回せるから(ry」 →NG ◦ 「色が違うだけ(ry」→NG • どんなに些細なスタイル変更であっても、propsによる明示的なインタフェースを公 開しておくことが世のため人のため
  26. 魔性の $refs • $refs自体は悪いものではない ◦ エレメントからgetBoundingClientRectしたいときだってある • $refsを通してdataやcomputedに触るべきではない ◦ それはプライバシーの侵害

    ◦ 先頭を大文字にしたからって publicにはならない ◦ 多少汚い実装になってでも $emitや.syncを駆使して避ける • $refsを通してmethodsに触るべきではない ◦ それはプライバシーの侵害 ◦ 先頭を大文字にしたからって publicにはならない ◦ 関数に切り出してutil化できるはず
  27. 1年かけた学び • コンポーネントは自己完結してこそ美しい ◦ 親とのインタフェースである propsと$emit以外はいつでも変更できるのが理想 • scopedなスタイルは神聖かつ不可侵である ◦ ルート要素は例外

    • 親が子のdata、computed、methodsにアクセスしてよい場面はない ◦ アクセスする手段はあるがそれは地獄の入口 コンポーネントの外部インタフェースは propsと$emitだけであることを徹底する
  28. シャイなダイアログ • App.vueに共通で設置した以外のダイアログは使いたい場所で設置する • 最前面に表示できないときがある ◦ 無慈悲なfixedやz-indexに阻まれやすい ◦ 特にsafari •

    domをbody直下に突っ込んでしまう ◦ コンポーネントフレームワークも普通にやってる ◦ 片付けは完璧に行う必要あり ◦ ドロップダウン系もやってしまうと楽 巷のコンポーネントフレームワークも普通にやっている https://github.com/vuematerial/vue-material/blob/4fa84a4005484563632c898c14974f65eaaca916/src/components/ MdPortal/MdPortal.js#L133