システム開発本部アプリケーションエンジニアの Edy です。
マイクロアドの WEB アプリケーションのフロントエンドは、大半を Vue.js を用いて開発しています。
その Vue.js ですが、「Vue 3 is now in RC! (Vue 3 is now in RC! · Issue #189 · vuejs/rfcs · GitHub)」が公開され、version 3 ( Vue 3 ) が盛り上がりを見せています。
今回は、Vue 2 から 3 へ移行する理由と、 2 から 3 へ version up するために行なっている Composition API の導入 についてお話していこうと思います。
そもそも Vue.js とは ?
Vue.js はユーザーインターフェースを構築するための フレームワークの 1 つ です。 他のモノリシックなフレームワークと違い、部分的に適用することが可能で、別ライブラリとの併用・統合がとても簡単です。 モダンなツールとそれをサポートするライブラリによって 洗練されたシングルページアプリケーションの開発 が行えます。
以前の記事を参考になるとより詳しく理解頂けるかと思います。
ここでは、参考記事に載っていない技術について 2 点、抜粋して軽く説明します。
SFC
単一ファイルコンポーネント ( Single File Component ) と呼ばれ、 HTML, JavaScript, CSS の3点を 1 ファイルに記述できる書き方です。SFC を用いることで関心毎にテンプレート・ロジック・スタイルをまとめることができます。
開発規模が大きくなればなるほど、UI と付随する挙動を把握するのはコスト がかかり始めます。 そんなときに、SFC を用いることで解決を図ることができ、コンポーネント内部では密結合、コンポーネント間では疎結合な設計が行え、コードの一貫性と保守性を保つことができます。
<template> <div> <p>Hello {{ text }} !!</p> </div> </template>
<script> export default { data () { return { text: 'SFC' } } </script>
<style scoped> p { font-size: 2em; text-align: center; } </style>
TypeScript
Vue.js は名前の通り JavaScript を用います。しかし「JavaScript is in TypeScript」と言われているように TypeScript を用いた開発も可能です。
静的型チェック される TypeScript は安全にコーディングを行うことができて保守性を保てます。また、型があることでオブジェクトのインターフェースを形づけることができ、業務知識を把握しやすくなるなどのメリットが享受できます。
Vue 2 の限界
Vue.js は強力なフレームワークではありますが、プロダクトへの Vue.js の採用が拡大したことでいくつかの問題が発覚しました。 この問題によって Vue.js 自体の設計モデルは限界に突き当たりました。
大規模なプロジェクトになるほど開発効率が落ちる
大規模になればなるほど、既存のコンポーネントに記述されているロジック、UI 操作は複雑化し、論理的にそして直感的に理解することが難しくなります。 そしてこの状況は、複数人が携わり自分自身以外が記述したコードを読んでいる時に頻発します。
例としては以下のようなものです。
- 既存 UI・機能への改修が多いプロダクト
- ViewModel と Model が絡み合いすぎているプロダクト
ロジックの再利用にかかるコストが大きい
実務で体験したものでは以下のようなものがあります。
- vue の context が this に含まれているためロジックとして切り出しづらい
- data, computed などが区切られているため関心ごとでスコープが切りづらい
- SFC での script 記述部分 ( TypeScript ) が肥大化しやすく可読性が落ちる
単体テストの難しさ
- View, ViewModel, Model が分離しづらくテストが難しい
- リアクティブな値に対するテストの難しさ
TypeScript の型推論が完全にサポートされていない
Vue 2 では標準言語が JavaScript であることもあり、TypeScript の型サポートは 不完全な状態 です。 TypeScript を導入しても最大限の恩恵を享受できるわけではありません。
- 状態遷移の store/state において型推論がサポートされていない ( mapGetters などのヘルパー関数は全て any 型として処理されます )
- props や emit 上で厳密な型推論をさせることができない
上記のこれらの問題点はクリーンアーキテクチャの導入、プロダクトチーム内でのコーディングルールの統一、コンポーネント分割の粒度の変更などにより一部解決することができます。 しかし、フレームワークとしては根本的に問題が残る結果となってしまいます。
Composition API の導入
これらの問題を解決するために考えられたのが Vue 3 です。
- 標準言語が TypeScript となる ( JavaScript もサポートされています )
- vue context は this から解放される
- data などの区切りは撤廃され、変数・メソッドは同一階層に記述できるようになる
これらが Vue 3 になって変更される大まかな内容になります。
既存プロダクトを Vue 3 に一気に切り替えるには改修コストが大きすぎるため、前段階として Vue 2 の状態で Vue 3 の書き方ができる Composition API を導入します。
導入方法
Composition API はライブラリとして公開されているため npm
を用いて既存プロダクトに追加します。
必要なものはこれだけになります。
( 実際に使っていくなかで Vue.use(VueCompositionApi)
したり、 import ~ from '@vue/composition-api'
する必要は出てきます。)
$ npm i @vue/composition-api
Composition API への置き換え
プロダクト内において composition-api と既存の vue component は共存できます。 ですので、改修する箇所のコンポーネントに対してのみ置き換えを行なっていきます。
( 共存は置き換えを柔軟にしますが、Vue.js に長けた人が少ない場合はかえって混乱をきたす可能性があるので注意が必要です。)
例として、従来の書き方と Composition API の書き方の違いを説明します。
従来の書き方
export default { props: { id: Number }, data: function() { return { dataForServiceA: 'DataA', dataForServiceB: 'DataB' }; }, methods: { emit: function() { this.$emit('click', `${this.dataForServiceA}`); }, console: function() { console.log(`${this.id}:${this.dataForServiceB}`); } } };
Composition API
type TypeA = { id: number }; export default createComponent({ props: { id: Number }, setup(props: TypeA, context) { const dataForServiceA = 'DataA'; const dataForServiceB = 'DataB'; const emit = () => { context.emit('click', `${dataForServiceA}`); }; const console = () => { console.log(`${props.id}:${dataForServiceB}`); }; return { emit, console }; } });
従来のコードにある、 data
, methods
がなくなり、全てが setup
内直下に記述されるようになりました。
そのため setup 内の data にアクセスするときは this が不要になり簡潔になりました。
Vue 2 では this があることでアロー関数による記述が敬遠されがちですが、Vue 3 からは積極的に使っていくことができます。
また、 setup の引数として props と context ( emit などを持つオブジェクト ) が渡されるように変更されたため、emit などはこちらを利用するようになりました。
props, components などのプロパティーは既存と同じように記述します。
export default createComponent({ props: { [...'propertyName']: ... }, // Vue 3 では emit に対してもある程度の制約を持たせられます emit: ['emitName'], components: { [...'componentName']: ... }, setup() { ... } });
Composition API で得られるいいこと
今回 Composition API を導入したことでコーディングしやすいと感じた部分についてまとめていきたいと思います。 Vue 2 との書き方の変更点など全て紹介することができませんがご了承ください。
関心の分離と可読性の向上
data, computed などの区切りがなくなったことで 関心ごとの分離が可能 となります。 色ごとでそれぞれの関心ごとになっているのが見て取れます。
具体的にはこのように記述できます。
import { createComponent, SetupContext } from '@vue/composition-api'; type TypeA = { id: number }; export default createComponent({ props: { id: Number }, setup(props: TypeA, context) { const viewModelA = serviceA(context); const viewModelB = serviceB(props); return { viewModelA, viewModelB }; } }) // 分離 const serviceA = (context: SetupContext, props: TypeA) => { const emit = () => { context.emit('click', props.id) }; return { emit }; }; // 分離 const serviceB = (props: TypeA) => { const console = () => { console.log(props.id) }; return { console }; };
関心ごとの 2 つを createComponent
外で記述しています。 ( serviceA
, serviceB
)
vue context は this から解放されたことによって service に引数として渡すことが可能となっています。
関心ごとの分離ができたことでコードがすっきりし、 変更頻度の多い改修などにも柔軟に対応できる ようになりました。
テスタビリティの向上
前述の service は .vue
ファイルに記述せずに、.ts
ファイルに切り出すことができます。
1 ファイルに書く TypeScript のコード量を減らして見晴らしをよくできます。
// export, 参照する側で import する必要があります export const serviceA = (context: SetupContext, props: TypeA) => { const emit = () => { context.emit('click', props.id) }; return { emit }; };
従来の単体テストはコンポーネントを読み込ませそこから methods や変数を取得してテストを行なっていました。
model, viewModel, logic をファイルごとに分けられるようになったこと、関心の分離ができるようになったことで、単体テストが記述しやすくなり テスタビリティが向上 しました。
ここでのポイントとしては、vue コンポーネントにロジックを書かないようにすることです。
終わりに
Composition API は従来の Options API に比べ書きやすく、非常に使い勝手の良いライブラリだと感じました。 プロダクトへ Composition API を導入し初めて約半年たちましたが、コンポーネント全体に対して約 4 割ほどの置き換えが完了しています。 ( その間に viewModel, model に対しての単体テストも並行で追加していきました。 )
全てのコンポーネントを置き換えられるように引き続き作業を頑張っていきたいと思います。
最後に、既存プロダクトに対して Vue 3 への置き換えを考えている方への参考基準 として以下を提示させていただいて終わりとします。
- 可読性の高い、クリーンな設計を行なっていきたい
- プロダクトの規模が既に大規模である、今後大規模になる
- TypeScript を用いた開発を積極的に行なっていきたい
- 業務ロジックがフロントエンドに多く記述される
少しでも参考になれば幸いです、ご覧いただきありがとうございました。