Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Vue.js、Nuxtの機能を使い、 大量のコピペコードをリファクタリングする
Search
igayamaguchi
October 19, 2024
Technology
6.4k
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Vue.js、Nuxtの機能を使い、 大量のコピペコードをリファクタリングする
Vue Fes Japan 2024 登壇資料
igayamaguchi
October 19, 2024
More Decks by igayamaguchi
See All by igayamaguchi
一休/Yahooトラベル、マルチブランドにまたがるデザインシステム
igayamaguchi
1
670
VeeValidate 3
igayamaguchi
3
640
==と===を調べてみた
igayamaguchi
0
530
Other Decks in Technology
See All in Technology
人とエージェントが高め合う協業設計
kintotechdev
0
100
タスクの複雑さでモデルを選ぶ ── Thompson Samplingで動かす“トークン/コスト最適化
satohy0323
0
570
複数プロダクトで進めるAI機能実装 ── 実践から得たリアルな学びとロードマップ実現への挑戦 / AICon2026_yanari
rakus_dev
0
140
「早く出す」より「事業に効く」 ── 顧客の業務サイクルから逆算するAI時代の二重ループ開発と「変化の設計者」 / devsumi2026
rakus_dev
1
420
そのドキュメント、自動化しませんか?
yuksew
1
320
Oracle Exadata Database Service on Cloud@Customer X11M (ExaDB-C@C) サービス概要
oracle4engineer
PRO
2
8.4k
「AIに依存している」と 「AIを使いこなしている」の違い
k8yasuma
0
120
Genie Ontologyは銀の弾丸かを考える / Is Genie Ontology a Silver Bullet?
nttcom
0
420
プロダクト開発組織の現在地(Ver.2026/07) / product-organization
kaonavi
0
120
ヘルスケア領域における AI 活用と その安全性担保のための取り組み (Leveraging AI in Healthcare and Our Efforts to Ensure Its Safety) - Google I/O Extended Tokyo 2026, July 11, 2026
zettaittenani
0
430
OPENLOGI Company Profile for engineer
hr01
1
74k
SREとQA 二人三脚で進めるSLO運用/sre-qa-slo
sugitak
0
1k
Featured
See All Featured
Large-scale JavaScript Application Architecture
addyosmani
515
110k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
2k
Git: the NoSQL Database
bkeepers
PRO
432
67k
A Tale of Four Properties
chriscoyier
163
24k
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
Mobile First: as difficult as doing things right
swwweet
225
10k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
46
2.9k
Practical Orchestrator
shlominoach
191
11k
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
470
Building an army of robots
kneath
306
46k
Information Architects: The Missing Link in Design Systems
soysaucechin
0
1k
Game over? The fight for quality and originality in the time of robots
wayneb77
1
220
Transcript
Vue.js、Nuxtの機能を使い、 大量のコピペコードをリファクタリングする 2024/10/19 Vue Fes Japan 2024 株式会社一休 CTO室 いがにん
(山口将希)
いがにんこと山口将希 Twitter: @igayamaguchi 一休のCTO室で一休.com/Yahoo!トラベルの フロントエンド設計の改善をしています 業務でVue.js/Nuxtをずっと触ってます 自己紹介
概要 • 株式会社一休では一休 .com/Yahoo!トラベルを運営 • Nuxtを使ってフロントエンドを実装 • 開発をしていく中で、大量のコピペコードが存在するように • 問題発生の背景とその課題にどう対処してきたかを話す
アジェンダ • サービスの概要 • 問題の概要 • 原因別の対応と解決策 ◦ 汎用的なUI要素 ◦
一休.com/Yahoo!トラベル特有のコンポーネント ◦ Vueの状態が絡んだロジック
運営サービスの概要 • 一休では2つの宿泊予約サイトを運営 ◦ 一休.com ◦ Yahoo!トラベル (2021年に統合) • フロントエンドをNuxtで実装
• 両サイトは1ソースで実現されており、ビルド時の環境 変数に応じて振る舞いが切り替わる • PC/SPはほとんど別デザイン ビルド時に環境変数で 切り替え
一休.com/Yahoo!トラベルのフロントエンド 一休.com/Yahoo!トラベルの ソースコードを統合 Nuxt v2の導入 Vue.jsを導入 当時はVue2でWebpackでの 自前のビルド 一休.comはもともとjQueryと素 のJavaScriptで書かれていた
2017年12月 2020年4月 2021年10月
何かを変更するときにやけに手数が必要 2022年ごろのこと 当時はVue2/Nuxt2で開発 フロントエンドフレームワークを使用することでリッチにフロントエンドを開発 全体としては開発効率が大きく上がっていた しかし、何かを変更するときにやけに手数が必要で時間がかかると思うことが増えた
コードを紐解くと大量のコピペコードが存在していた 大量のコピペコードが存在していた 300行のうち差分は4行しかな いコンポーネント ほぼ同じ料金表示だが別実装 同じUIの再実装 HTML、CSSが重複している
重複コードの割合 一休の他プロジェクト SonarCloudを使用してプロダクトコードの重複コードの割合を算出 ※TypeScriptの部分のみの計測 一休.com/Yahoo!トラベル 3%
重複コードの割合 20 % 一休の他プロジェクト SonarCloudを使用してプロダクトコードの重複コードの割合を算出 ※TypeScriptの部分のみの計測 一休.com/Yahoo!トラベル 3%
なぜコピペコードが生まれているのか 大きく3つに分類でき、それらを改善していった • 汎用的なUI要素 • 一休.com/Yahoo!トラベル特有のコンポーネント • Vueの状態が絡んだロジック
対応と解決策 • 汎用的な UI要素 • 一休.com/Yahoo!トラベル特有のコンポーネント • Vueの状態が絡んだロジック
汎用的なUI要素 サイト内で統一的な見た目を実現する、 HTMLの拡張のようなもの 例えばボタン、チップ、タブなど 一休.com/Yahoo!トラベル、PC/SP、各ページ間で同じデザインのものがコピペ、再発明されていた <button class=" bg-brand-gradient-p text-white rounded-md
… " type="button" > 検索する </button> ほぼ同じHTML、CSS テキストや色を少し変えているだけ
Vueではコンポーネント化により使いまわしが可能 VueではHTML、CSS、JavaScriptを1つのコンポーネントとして実装できる 実装したコンポーネントを各所で使いまわすことで重複を排除できる <template> <div :class="$style.wrapper" @click="onClick"> <div :class="$style.content">~~~</div> </div>
</template> <style module> .wrapper { /* */ } .content { /* */ } </style> <script> export default { methods: { onClick() { goToHotel() } } } </script>
当時から共通化の仕組みがあるのに、なぜコピペが発生したのか • 実装フローの問題 • UIコンポーネントを適切に切るノウハウ、思考がなかった • ノウハウ、思考もないので UIコンポーネントがそろっていなかった 上記理由により積極的に UIコンポーネントを切ることをしない開発が常態化した
当時の実装フローの問題 • 画面、特定機能を実装することに集中する形 • サイト全体でどういったものが汎用的に使われる UIなのか、切り出して共通化すべきかを考えることがで きていなかった デザイナーがXDで1枚のページを作成 XDのモックをエンジニアに共有 エンジニアがモックを見て画面を実装
解決方法 この問題にデザインシステムを構築して対応 • デザイン、開発のフローをコンポーネントを考慮したものに • UIコンポーネントライブラリの実装 • UIコンポーネント構築のノウハウを貯める
デザインシステム 標準の定義があるわけではないが • デザインガイドライン • デザインパターンライブラリ • コンポーネントライブラリ(実装) からなるものが多い Design
Systems―デジタルプロダクトのためのデザインシステム実践ガイド https://www.borndigital.co.jp/book/11908.html
デザインシステムを作る やったこと • Figmaの導入 • トンマナの洗い出し • コンポーネントの洗い出し • Figma上でコンポーネントの構築
• コンポーネントのコード化 • Storybookで一覧化 • 開発フローの整備
デザインシステムを作る やったこと • Figmaの導入 • トンマナの洗い出し • コンポーネントの洗い出し • Figma上でコンポーネントの構築
• コンポーネントのコード化 • Storybookで一覧化 • 開発フローの整備 デザインシステムはそれ1つで大きなトピック 今日は一部だけピックアップ
コンポーネントの洗い出し / Figma上でコンポーネント化 一休.com/Yahoo!トラベルのサイト全体を見て、サイト内でまとまりとなるものを洗い出し デザイン上どんなUIコンポーネントが存在し、どういった形で使われるかが明確になる
コンポーネントのコード化 Figmaで洗い出したコンポーネントを実装 デザインのまとまりを 1コンポーネントで表現 propsに応じていくつかのバリエーションで表示できるように <BrandButton theme="primary" size="large" > 検索する
</button> propsに応じて見た目が変化 export default Vue.extends({ props: { theme: { type: String as PropType< 'primary' | 'secondary' | 'tertiary' | 'search' >, default: 'primary', }, 基本的に内部で状態を持たないように props、slot、emitで表現
Storybookで一覧化 コンポーネントの様々なバリエーションを簡単に試すことができるように UIコンポーネントとして完成度を高めるには独立した UIコンポーネント開発環境が必要 Storybookを使うことは必須 Vueのpropsを自由に変更 して試すことができる import type {
Meta, StoryObj } from '@storybook/vue3' import BrandButton from './BrandButton.vue' const meta = { title: 'elements/BrandButton', component: BrandButton, } satisfies Meta<typeof BrandButton> type Story = StoryObj<typeof meta> export default meta export const Primary: Story = { args: { theme: 'primary', default: 'Button', }, }
コンポーネントのディレクトリを整理
開発フローの整備 Figma、モックを共有 実装したUIコンポーネントを組 み立てて 画面を実装 デザイナーが Figmaで 1枚のページを作成 デザイナーとエンジニアで Figmaを
見てコンポーネントについて議論 コンポーネントをStorybookに カタログ化 土台が整ったので、最後に開発フローを整備 コンポーネントについて議論、開発するフェーズを追加 意識が、画面だけでなくコンポーネントにも焦点が当たるように
<BrandButton theme="search" size="large" > 検索する </button> 汎用的なUI要素 UIコンポーネントが実装され、開発フローも整備された これにより汎用的なUI要素の重複は解消 コンポーネントを使いまわしつつ、各
propsを変えるだけ
対応と解決策 • 汎用的なUI要素 • 一休.com/Yahoo!トラベル特有のコンポーネント • Vueの状態が絡んだロジック
一休.com/Yahoo!トラベル特有のコンポーネント デザインシステムでは汎用的なものがカバーされた 次はサイト特有のコンポーネント 例えば料金や部屋プランなど
デザインの統一 デザイナーとエンジニアでコミュニケーションをとり統一を図る 一休.com/Yahoo!トラベルで明確に違うものにしたいわけではないのであればデザインを統一 既存のデザインから引き継がれたものや、なんとなくでデザインを分けていたものが多く、統一して問題ないも のが多かった
コンポーネントの分割 大枠だけ共通化して、細部だけ別のコンポーネントに 青枠がそれぞれ共通コンポーネント、赤枠だけ実行時の環境変数を見て差し替え <template> <div> <!-- 一部だけ動的にコンポーネント描画 --> <Component :is="activeTemplateAmount"
:amount2="amount" /> </div> </template> <script setup lang="ts"> import AmountIkyu from '~/Amount.ikyu.vue' import AmountYahoo from '~/Amount.yahoo.vue' // 他処理… const activeTemplateAmount = computed(() => { // modeは環境変数から設定される if (mode === 'yahoo') { return AmountYahoo } return AmountIkyu }) </script>
一休.com/Yahoo!トラベル特有のコンポーネント デザインを統一することでコードを分ける必要がなくなった 分けざるを得ないところはコンポーネント分割により、共通化できる箇所を増やすことで、影響が小さくなった
対応と解決策 • 汎用的なUI要素 • 一休.com/Yahoo!トラベル特有のコンポーネント • Vueの状態が絡んだロジック
Vueの状態が絡んだロジック Vueの状態が絡んだロジックがコピペされていた 例えばGraphQLから取得するコード 同じようなコードが様々なコンポーネントに存在 export default Vue.extend({ data() { return
{ query: undefined, plan: undefined, } }, created() { this.query = getByUrl() }, apollo: { plan: { query: PlanDocument, variables() { return { pln: this.query.pln, } }, } }, })
なぜコピペが発生したのか 当時はVue2を使用、Vueの状態が絡んだロジックの良い共通化手法が提供されていなかった Vueの状態を使わない純粋な処理として切り出せるところも限界があった
当時はOptions API 当時のVueの標準的な記述方法 宣言的に状態、状態からなる処理を記述可能 変数の変化に応じて UIが更新されるように適切に各 グループ内で記述する必要がある export default Vue.extend({
data() { return { lastName: '', firstName: '', } }, computed: { fullName() { return `${this.lastName} ${this.firstName}` }, }, watch: { fullName() { showNotice('本名が変更されました') }, }, }) 持っている状態 状態を使って計算した値 値を監視し、変更に 応じて行う処理
Options APIでの共通化手法の問題 Vue2のOptions APIにおいてもMixinという共通化手法はあ る。しかし • 実装が隠ぺいされる • 暗黙的なオーバーライドが発生する など、よい共通化手法とは言えず積極的に使う判断はできな
かった Vueの状態が絡んだロジックのよい共通化手法が必要だった // Mixin export default { data() { return { lastName: '', firstName: '', } }, computed: { fullName() { return `${this.lastName} ${this.firstName}` }, }, } // Component import NameMixin from './TestMixin' export default Vue.extend({ mixins: [NameMixin], computed: { displayName() { return `${this.fullName}様` }, }, }) Mixinを呼び出すコードからは Mixinで どんな状態が追加されたかは分からない
Composition API • Vue3で導入されたAPI • 用意された関数を呼び出すことで状態、状態からなる 処理を定義 • Vueの状態も含めたロジックを関数に切り出すことが可 能
これを使うためにはVue3/Nuxt3へのアップデートが必要にな る // 別ファイル function useName() { const lastName = ref('') const firstName = ref('') const fullName = computed(() => { return `${lastName.value} ${firstName.value}` }) return { lastName, firstName, fullName, } } // コンポーネント側 const { lastName, firstName, fullName, } = useName() 関数として複数コンポーネントで 呼び出し可能 明示的にどんな値を使うのかが分かる
Vue3/Nuxt3へのアップデート Composition APIを使うために、2023年2月にアップデート コピペ問題以外にも開発環境の改善、型の改善など良いことが多数 これでロジックのコピペ問題への対応準備ができた
一休.com/Yahoo!トラベル、 PC/SP間のコピペを共通化 一休.com/Yahoo!トラベルは1ソースで実装している。 PC/SPも別のデザイン コンポーネントはコピペで作られていたため、一休 .com/Yahoo!トラベル × PC/SPそれぞれに同じロジックが別 のコードとして実装されていた export
default Vue.extend({ data() { return { query: undefined, plan: undefined, } }, created() { this.query = getByUrl() }, apollo: { plan: { query: PlanDocument, variables() { return { pln: this.query.pln, } }, } }, }) PlanDetail.ikyu.pc.vue PlanDetail.ikyu.sd.vue PlanDetail.yahoo.pc.vue PlanDetail.yahoo.sd.vue
一休/Y!、PC/SPでのコピペを共通化 export default usePlanDetail() { const query = useRouteQuery() const
{ data } = useAsyncQuery( PlanDocument, computed(() => ({ pln: query.value.pln })), ) // 取得した値からの処理... return { plan: data, } } PlanDetail.ikyu.pc.vue PlanDetail.ikyu.sd.vue PlanDetail.yahoo.pc.vue PlanDetail.yahoo.sd.vue 共通のふるまいを抽出して関 数化 Composition API導入前は同じ コードをN倍書かなければいけな かったが1つにまとまる // 各コンポーネント setup const { plan } = usePlanDetail() 関数を参照 コンポーネントに書かれたロジックを関数として抽出、共通化
小さい責務で分ける 例えばURLからの値のマッピング どのページ、コンポーネントであろうとやることは変わらないので共通の関数に Top.vue AccommodationList.vue PlanList.vue Plan.vue // 各コンポーネント setup
const { query } = useRouteQuery() 関数を参照 export default function useRouteQuery() { const nuxtApp = useNuxtApp() const route = useRoute() const config = useRuntimeConfig().public const { interpretedKeywords } = useInterpretedKeywords() const query = computed(() => { return getQuery( route, config.mode, nuxtApp, interpretedKeywords.value, ) }) return query }
小さい責務で分ける 例えばデータ取得の関数 一休ではGraphQLを使用しており、GraphQLのqueryと variablesの設定のコードは大きくなりがち // コンポーネントのsetup内 const { query }
= useRouteQuery() // GraphQLとの通信処理 const { facet } = useFetchFacet() const { areaMaster } = useFetchAreaMaster() const { searchStatusNames } = useFetchSearchStatusNames() // 取得した値を使って別のコンポーザブルを呼び出し const { tags, searchLabel } = useTags( query, facet, searchStatusNames, areaMaster, ) export function useFetchFacet() { const query = useRouteQuery() const { skipAccommodationSearch } = useSearchSkip(query) const { data } = useAsyncQuery( graphql(` query UseFetchFacet($input: SearchAccommodationsInput!) { # 省略... } `), computed(() => { // variablesの組み立て処理が入る return { /* ... */} }), { server: false, skip: skipAccommodationSearch, }, ) const facet = computed(() => { if (!data.value) { return undefined } return new Facet(data.value.searchAccommodations.facet) }) return { facet, } }
まだロジックが綺麗ではない。もう一歩 … 共通化したはずのComposition APIが分かりにくいところが … const { accommodation, amenities, attribute,
booking, bookingLoading, calendar, checkInOutLabel, discount, inventory, limitations, plan, room, roomPlan, settlementLabels, isZenkokuCouponTarget, landingPageUrl, meals, } = useRoomPlanDetail({ isPage: props.isPage, }) 共通関数から大量の返り値が返っ てきて把握コスト増 共通関数の中身も600行を超え、あり とあらゆる責務を持つように
コンポーネントが大きすぎる 例えばプランの詳細モーダルが巨大なコンポーネントになっ ていた HTML部分だけで1200行… コンポーネントが責務(機能)を持ちすぎている これを責務(機能)ごとに分ける 目安はざっくりHTML部分が300行以下に収める 共通化のためのコンポーネント切り出しではなく、責務わ けのためのコンポーネント切り出し
分割例 Header SearchCondition Calendar SearchCondition PeopleAndRoom IntroductionPlan IntroductionRoom AmountIkyu BookingButton
AddComparison List RoomPlanSharing const props = defineProps<{ data: FragmentType<typeof fragment> | undefined | null bookableDateTime: | FragmentType<typeof bookableDateTimeFragment> | undefined | null }>() const { inventory } = useInventory() const { bookableDateTimeTo } = useBookableDateTime( toRefs(props).bookableDateTime, ) const { firstImages, plan, bookingAmount, room, } = useHeader(toRefs(props).data) そのコンポーネントに必要な関 数だけを呼び出して必要な返り 値だけしようすればよい 各コンポーネントの ロジックがシンプルに
ディレクトリ、ファイルを整理 PlanDetail/ lib/ plan.yahoo.ts plan.ikyu.ts amount.ts calendar.ts composables/ useInventory.ts usePlanDetail.ts
PlanDetail.ikyu.vue PlanDetail.yahoo.vue components/ PlanDetail/ plan.yahoo.ts plan.ikyu.ts useInventory.ts usePlanDetail.ts PlanDetail.ikyu.vue PlanDetail.yahoo.vue Header/ Coupon/ BookingButton/ Header/ Coupon/ BookingButton/ コンポーネント、コンポーザブルが大量に増えるので分かりやすいように 特定コンポーネントでしか使用されないものはそのコンポーネントの配下に配置する 配下はそれぞれlib、components、composablesを切り、配置する 入れ子で同様の構造が 続く
Vueの状態が絡んだロジック Composition APIによりロジックが共通化された コンポーネント分割によって、共通化されたロジックも保守しやすいものに
まとめ 一休.com/Yahoo!トラベルのフロントエンド開発において発生したコピペ問題に対して 以下のように対応した • 汎用的なUI要素 ◦ UIコンポーネントの作成 ◦ ワークフローの整備 •
一休.com/Yahoo!トラベル特有のコンポーネント ◦ デザインの統一 ◦ コンポーネント分割 • Vueの状態が絡んだロジック ◦ Composition APIの活用 ◦ コンポーネント分割
ご清聴ありがとうございました