レガシーアプリケーションのリニューアルにNuxt.jsで戦う #vuefes_reject

35b58828e4e24c579c35529061711dfd?s=47 Kenta Suzuki
November 10, 2018

レガシーアプリケーションのリニューアルにNuxt.jsで戦う #vuefes_reject

Vue Fes Japan 2018 Reject Conference で話しました #vuefes_reject
https://vuejs-meetup.connpass.com/event/97557/

35b58828e4e24c579c35529061711dfd?s=128

Kenta Suzuki

November 10, 2018
Tweet

Transcript

  1. 1.

    エムスリー株式会社 鈴木 健太 @suusan2go 2018/11/10 Vue Fes Japan 2018 Reject

    Conference レガシーアプリケーション リニューアルにNuxt.jsで戦う #vuefes_reject
  2. 2.

    自己紹介: 鈴木 健太 • GitHub / Twitter @suusan2go • Software

    Engineer @ M3, Inc • リニューアル プロジェクトで 、Kotlin APIサーバ / フロント Nuxt.jsを担当 • 現在 AI・機械学習チームというところで機械学習 部分以外 こと全部やってます • 自慢 娘(2歳)が同年代で 可愛いことです(当親 比)。今月第二子が生まれます! 2
  3. 8.

    • 10年前 レガシーシステムをサーバサイドKotlinで、フロントエンドを Vue.js(Nuxt.js)でリニューアルしました。 • リニューアルしたレガシーシステム 2つ ◦ 1. 医師

    キャリア支援システム 企業向け画面 ◦ 2. 薬剤師 キャリア支援システム 企業向け画面 ◦ 事業規模感: 年間売上合わせて100億円超 ◦ システム 規模感: それぞれテーブル数150〜200程度 ◦ いずれ システムも10年前 技術スタック (独自Java FW、View XSLT) ◦ 新規 機能開発がとても難しい状態になっていた リニューアルについて 8
  4. 17.

    17

  5. 18.

    • 通常 機能開発チームと 別に、リニューアルチームを立ち上げ • 薬剤師キャリア リニューアルチーム 2名 ◦ suusan2go(メイン

    エンジニア) ◦ maeharin (リーダー、他プロジェクト兼任、0.2稼働) • リニューアル 開発完了後に、徐々に通常 機能開発チームに引き継 ぎ • 現状 通常 機能開発チームで開発・運用されています リニューアル 体制 18
  6. 19.

    • サーバーサイド DB そ ままに、アプリケーション部分をKotlinでAPI 化 ◦ 独自FW、既存仕様 ドキュメント 当然まったくないところからスタート

    ◦ まずコードや、出力されるSQL ログを読み解いて現状 仕様調査 ◦ ほぼフルスクラッチで書き直して、 Kotlin + SpringBoot化 • フロントエンド UIからスクラッチで刷新 ◦ XSLTというテンプレートを使っておりそ まま移行する が困難だった ▪ サーバーサイドでやるべきロジックがかなり漏れ出している・・・ ◦ ただし見た目 変わっても、画面構成 できるだけ変えないようにした ▪ 何年も変わってないUIをいきなりかえると混乱が起きる ▪ 画面を大きく変えると仕様 考慮漏れ、ユースケース考慮漏れが起きがち リニューアル 方針 19
  7. 21.

    24

  8. 24.

    リニューアル計画時 検討事項 • チームで 既にRails + Vue.jsで開発を経験しており、フロントエンド 技術にVue.jsを使うこと ほぼ確定していた •

    先行する医師キャリアチーム SPAで なく、バックエンド API Rails が叩いて、フロント 一部でVue.jsを使うMPAになっていた 27
  9. 26.

    Rails ほぼバックエンドAPI プロキシ ような存在になる(コード サンプル) 29 class Ajax::FilmsController < ApplicationController

    def show film = film_api.staff_get_film(params[:id]) # APIへのリクエスト render json: film end def create film_api.staff_create_film(_film_create_params) # APIへのリクエスト end private def _film_create_params params.require(:film).permit! end end
  10. 27.

    こ アーキテクチャ メリット・デメリット 30 • メリット ◦ セッション管理 Railsに任せられる ◦

    Vueが必要 ない画面で Rails 知識だけで構築できる ◦ サーバーサイド 経験が中心 メンバーでも比較的キャッチアップ しやすい • デメリット ◦ Vue.js エコシステムに乗り切れない ▪ 例え Vue Routerが使えない / 使いにくい ◦ VueとRails 責務が曖昧になる ▪ 例え Vue.js フォームでポストしたあと 画面遷移・バリデーションエラー どっちで出す? ◦ せっかくバックエンド APIがSwaggerで型情報付き API定義をくれている に、 Railsを通すことで型情報が失われてしまう・・・ ◦ Vue UIフレームワークをRails側 テンプレートで使いにくい
  11. 28.

    Rails ほぼバックエンドAPI プロキシになっている 31 • メリット ◦ セッション管理 Railsに任せられる ◦

    Vueが必要 ない画面で Rails 知識だけで構築できる ◦ サーバーサイドが中心 メンバーでもキャッチアップが容易 • デメリット ◦ Vue.js エコシステムに乗り切れない ▪ 例え Vue Routerが使えない / 使いにくい ◦ VueとRails 責務が曖昧になる ▪ 例え 、Vue.js フォームでポストしたあと 画面遷移 どっち 責任? ◦ せっかくバックエンド APIがSwaggerで型情報付き API定義をくれている に、 Railsを通すことで型情報が失われてしまう・・・ Vueで構成されたページが多くなってくると、 VueによるSPAにUIをよせたほうが確実に設計 綺麗になりそう…… でもサーバーサイド中心 メンバーがキャッチアップできるだろうか?
  12. 29.

    そこでNuxt.js 32 • 当時(2017年11月) まだベータ • リニューアル リリースまでに 1.0が出るだろうという見込みで選択 ◦

    実際、リニューアル開発中に1.0がリリース ◦ 破壊的な変更 少なく、スクロール挙動 調整くらいでなんとかなった
  13. 30.

    強力な規約と実用的な機能 33 • pages/ 配下がそ ままルーティングになる • layoutプロパティでレイアウトを切り替え • 組み込み

    Vuexストア • 共通処理をmiddlewareで管理できる • データ 取得処理をどこで書け いいか明確(fetch / asyncData) • 組み込み エラーハンドリング(context.error) • プラグイン機構(Sentry / Google Analyticsなど) 実用的なアプリケーションがすぐに開発できる 設計 ベストプラクティスに乗ることができる
  14. 32.

    Nuxt.jsを採用してよかったところ 35 • フロントエンド 開発にレールができた • 以下 ような問いに公式ドキュメントが答えてくれる ◦ 初期データどこでとってくる?

    ◦ ルーティングどうする? ◦ こ ページ レイアウト変えたいんだけど ◦ Webpackでこれどうやって設定する ? • セットアップに時間が取られがちなところ、最初からアプリケーション 開発に集中できた! • SPAを作るコストをめちゃくちゃ下げられた
  15. 36.

    • リニューアル対象 システム 、いわ 企業向け 採用管理業務システム • 歴史的な経緯により巨大なフォームを作らざるをえない • サーバーサイド

    ロジックをできるだけ変えないようにするに 、数十プロパティ でネストした構造を持つJSONを送らざるをえない • サーバーサイド レスポンス / 要求するリクエストが変わったときに、きちんと追 随していく が辛い • サーバーサイドと 連携に型がほしい なぜTypeScriptが使いたかった か 39
  16. 37.

    サーバーサイド 実装からTypeScriptクライアントを自動生成する 41 Kotlin + Spring Boot API サーバ Spring

    Foxで実装から Swagger Definition作成 Swagger Codegenで TS APIクライアントを自動生成
  17. 40.

    Nuxt.jsをTypeScript対応させる 44 • 実 Nuxt.js examples配下にTypeScript サンプルがあります! ◦ https://github.com/nuxt/nuxt.js/tree/dev/examples/typescript •

    modules/typescript.jsを用意して、nuxt.config.jsで読み込め OK • Vue2.5からTypeScirptサポートが強化された で、通常 オブジェクトリテラル 構文でも殆ど問題なかった(クラススタイル 使わなかった) • プロジェクトがでかくなると、初回 ビルドに1分以上かかるようになる で fork-ts-checker-webpack-plugin で、TypeCheckを別プロセスに
  18. 41.

    Nuxt.jsをTypeScript対応させる 45 const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); export default function() {

    this.nuxt.options.extensions.push('ts'); this.extendBuild(config => { // ~~省略 ~~ config.plugins.push( new ForkTsCheckerWebpackPlugin({ vue: true, }), ); }); };
  19. 42.

    Vue 型定義をNuxt.js用に拡張する 46 • Nuxt.jsが拡張しているコンポーネント オプション(layout, middleware, fetch, asyncData, watchQuery….)

    ために、型定義 自前で拡張する必要がある • もしかしたら公式で近々サポートがあるかも? ( #4050 #4164)
  20. 43.

    pages配下で拡張されるオプション 型定義 47 declare module 'vue/types/options' { interface ComponentOptions<V extends

    Vue> { layout?: string; middleware?: string | string[]; fetch?: (context: NuxtContext) => void; asyncData?: (context: NuxtContext) => void; watchQuery?: string | string[]; head?: () => { title?: string; meta?: Array<{ hid?: string; name?: string; content?: string }>; }; } }
  21. 44.

    interface NuxtContext { isClient: boolean; isServer: boolean; isStatic: boolean; idDev:

    boolean; isHMR: boolean; route: Route; store: Store<State>; env: object; query: object; nuxtState: object; req: Request; res: Response; redirect: (path: string) => void; error: (params: { statusCode?: string; message?: string }) => void; beforeNuxtRender: ({ Components, nuxtState }) => void; } Nuxt.jsが提供するContextオブジェクト 型定義 48
  22. 45.

    declare namespace NodeJS { export interface Process { client: boolean;

    server: boolean; } } process.client / process.server 型定義 49
  23. 46.

    TypeScriptで Linterについて 52 • TypeScriptな で普通だったらtslint……な だけれどeslint-plugin-vueが使い たい • typescript-eslint-parser

    でTypeScriptにeslintをかけられる • こ 辺り joe_reさん ブログにかなり詳しく書かれてて参考にさせていただき ました。
  24. 47.

    Nuxt.jsにTypeScriptいれてよかった? 54 • 多少最初 設定 時間とられるけど間違いなくYES • ハマりどころがなかったわけで ありませんが、型で守られるメリット 方が大き

    かったです。 • 開発中 テンプレート部分に型がなくても充分恩恵受けられるなーと思ってたけ ど、QAでテンプレート部分も型で守られてたら・・・的なバグが沢山見つかった でやっ りテンプレートにも型ほしいです
  25. 49.

    components/配下 管理 57 • Nuxt.js components配下 管理 仕方について ノータッチな で、自分た

    ちでど ように管理していくか考える必要がある • 自分たち プロジェクトで ElementUIというUIフレームワークを使っていて、自 作 コンポーネント 数 それほど多くなかったが、コンポーネントが増えてくる と、コンポーネント 粒度に応じて階層をつけたくなる • Atomic Design 考え方でコンポーネントを管理していくことにしました
  26. 51.

    59

  27. 53.

    ElementUI 話をします 61 • ElementUI Vue.js デスクトップUIフレームワーク • TypeScript 型定義も提供してくれる

    • 特にフォームでリッチな表現が簡単にできる、バリデーションルール(中身 yiminghe/async-validator)が柔軟に組める で重宝してました • 詳しく @maeharin 資料をご確認ください ◦ 10年前 レガシーシステムをVue.js TypeScript Elementでフルリニューアルしてい る話 • こういうコンポーネント集、業務要件に合わせようとするとカスタマイズが面倒だったり不可 能だったりしますが、ElementUI 拡張ポイントというかカスタマイズ性が高く、ほぼ困りま せんでした
  28. 57.

    ElementUI Input実装 (packages/input/src/input.vue) 65 // Inputイベントのたびに高さ調節のためにこのメソッドが呼ばれる resizeTextarea() { if (this.$isServer)

    return; var { autosize, type } = this; if (type !== 'textarea') return; if (!autosize) { this.textareaCalcStyle = { minHeight: calcTextareaHeight(this.$refs.textarea).minHeight }; return; } const minRows = autosize.minRows; const maxRows = autosize.maxRows; this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows); },
  29. 58.

    if (!hiddenTextarea) { hiddenTextarea = document.createElement('textarea'); document.body.appendChild(hiddenTextarea); } let {

    paddingSize, borderSize, boxSizing, contextStyle } = calculateNodeStyling(targetElement); hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`); hiddenTextarea.value = targetElement.value || targetElement.placeholder || ''; let height = hiddenTextarea.scrollHeight; ElementUI Input実装 ( packages/input/src/calcTextareaHeight.js ) 66
  30. 61.

    • export default ElInput.extend({ methods: { // override 自動での高さ調節が必要なとき以外は、minHeightは初回の計算結果をキャッシュしておく resizeTextarea()

    { if (this.$isServer) return; const { autosize, type } = this; if (type !== 'textarea') return; if (!autosize) { this.textareaCalcStyle = { minHeight: this.textareaCalcStyle.minHeight || calcTextareaHeight(this.$refs.textarea).minHeight, }; return; } const minRows = autosize.minRows; const maxRows = autosize.maxRows; this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows); }, }, }); カスタムコンポーネントを作ってワークアラウンド 69
  31. 71.

    • 例え fetch APIを考えてみる • 認証絡む場合に Cookieを含めてリクエストを送る必要がある クライアントサイドでfetch APIでCookieを送る 79

    // クライアントサイドはこれでOK fetch("https://example.com", {credentials: "include"}) .then(e => e.text()) .then(text => { console.log(text) })
  32. 72.

    • 例え fetch APIを考えてみる • 認証絡む場合に Cookieを含めてリクエストを送る必要がある サーバーサイドでfetch APIでCookieを送る 80

    // サーバーサイドでは、Cookieはリクエストオブジェクトに含まれる const parsedCookie = cookie.parse(context.req.headers.cookie); fetch("https://example.com", { headers: { Cookie: `session_id=${cookie["session_id"]}` } }) .then(e => e.text()) .then(text => { console.log(text) })
  33. 74.

    window / document へ アクセス 82 • 不用意に window、 documentにアクセスするようなコードがあると、

    SSRするときだけエラーが起きてしまう • ただしこれ 書いていれ すぐ気がつける、Vueを使っていると window / document を書く必要がある機会 少ない で、それほど問 題にならないかも
  34. 75.

    SSR非対応なプラグインへ 対応 83 • document / window を参照するようなプラグイン 読み込むだけでエ ラーになってしまう

    // plugins/wysiwyg.js // window / documentをめちゃくちゃ参照しそうなプラグイン import Vue from 'vue'; import HogeWysiwygEditor from 'hoge-wysiwyg-editor'; Vue.use(HogeWysiwygEditor);
  35. 77.

    Nuxt.jsで SSRについて 85 • 結論として SSRする 途中でやめることにしました ◦ SSRしたときだけ起こる見つけにくいバグを生んでしまいそう ◦

    QAが大変になりそう • ほぼログイン前提かつ、SEOが必要ないアプリケーションで、SSR 必 須要件で なかったため、途中からmode: spaに切り替え • デプロイ mode: spaでビルドしたも を配信 • SSR簡単にできるけど、認証含んだAPIが必要だったり、外部ライブラリ 読み込んだりしていくと、辛くなってくる で、最初 設計でサーバーサ イドとクライアントサイド 違いをよく考慮しましょう〜
  36. 79.

    Nuxt.js アプリケーションをテストしやすくするために • components/とpages/ 棲み分けを意識する • components/ にマウントしただけでAPIアクセスしたりするコンポーネントがある と、扱いにくいしテストしにくい •

    初期データ 取得など 基本的に pages/ 配下で行うようにする • pages/コンポーネント Nuxt.jsに依存する で、できるだけ薄くしておくと、コン ポーネント テストがしやすくなる 87
  37. 80.

    Nuxt.jsで テスト 考え方 90 • pages/ テスト E2Eテストで担保する • components/配下

    テストをjest + vue-test-utilsで頑張る • コンポーネント テスト どうする?
  38. 82.

    92

  39. 84.

    まとめ • レガシーなアプリケーション UIをNuxt.jsでリニューアルしました ◦ Nuxt.jsを使うとフロントエンド 開発にレールができる ◦ ドキュメントが豊富でキャッチアップが容易 •

    Nuxt.js開発 Tips ◦ TypeScriptも使えます!おすすめです! ◦ components/配下 管理 ◦ SSR クライアント / サーバー環境 違いを意識して、設計初期段階から 抽象化しよう ◦ pages/配下 薄く保って、テストしやすい状態を作ろう • VueFes 資料 知見に溢れている で皆さん読みま しょう!!!(来年 採択されるぞ!) 94