jQueryなどを利用したレガシーコードにVue.jsを導入してコードを書き換える際、雑に分類すると2つの方法があります。
- 気合いで一気にやる
- 段階的に書き換える
全体像が把握できている場合は 「1. 気合いで一気にやる」も全然アリですが、多くの場合はリスク軽減といった観点から「2. 段階的に書き換える」ための手段を模索することになります。
さて、Vue.js 3.x からは Composition API という関数ベースでの新しいAPIが導入されました。
これを利用すると、Vue.js 2.x 系までは主にVue.jsのコンポーネント内部で利用されていたリアクティブシステムについて、Vue.jsの外部でも利用可能となります。
また、CompositionAPIを利用する関数は、Vue.jsコンポーネント内部でも利用可能なので、「意味のある機能単位で関数として切り出す」→「テンプレート部をVue.js化していく」といった順番での書き換えも選択肢に入ります。
Vue Composition API を利用した書き換え(実例)
次のようなシンプルなTODOアプリの置き換えを例に考えてみます。
TODOを追加して件数を表示するだけのシンプルなものです。
<!DOCTYPE html>
<html>
<body>
<button id="addTodo">Add Todo</button>
<div id="todoList"></div>
<span id='todoCount'></span>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<script src="./legacy.js"></script>
</body>
</html>
function addTodo() {
var input = $('<input>');
input.attr('type', 'text');
var todo = $('<div>');
todo.addClass('todo');
todo.append(input);
$('#todoList').append(todo);
}
function updateCount() {
var count = $('.todo').length;
$('#todoCount').text('Total: ' + count);
}
$(function() {
$('#addTodo').on('click', function() {
addTodo();
updateCount();
});
updateCount();
});
ここからCompositionAPIによる切り出しを試してみましょう。
(ビルドの設定などに関する説明はこのエントリでは省略します)
TODOの管理・追加を useTodo.js
というファイルに切り出すと次のようなイメージになります。
import { reactive, computed, toRefs } from "@vue/composition-api";
export const useTodo = () => {
const state = reactive({
todoList: []
});
const count = computed(() => state.todoList.length);
const addTodo = () => {
state.todoList.push({ todo: "" });
};
return {
...toRefs(state),
count,
addTodo
}
};
この関数を適切な箇所で呼び出すことで、レガシーコードと並行して状態管理を構築することができます。
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';
import { useTodo } from './useTodo';
Vue.use(VueCompositionApi); // Vue2.x系で利用する場合は必須
const todoState = useTodo(); // CompositionAPIを呼び出し
function addTodo() {
var input = $('<input>');
input.attr('type', 'text');
var todo = $('<div>');
todo.addClass('todo');
todo.append(input);
$('#todoList').append(todo);
}
function updateCount() {
var count = $('.todo').length;
$('#todoCount').text('Total: ' + count);
}
$(function() {
$('#addTodo').on('click', function() {
addTodo();
updateCount();
todoState.addTodo(); // 状態管理に書き込み
});
updateCount();
});
レガシーコードを動作させたまま、Vue.jsで必要となる状態(データ)のみを収集可能となりました。
こういったコードを増やしていき、状態管理が整った時点でVue.jsに一気に置き換えることで、見通しが良い状態で書き換えに着手することができます。
watchによるDOMからの段階的な切り離し
ここまでに説明した内容は、Vue Composition API 以外の方法でも実現可能です。
外部でVuex/Redux/MobXといった状態管理ライブラリで状態を集約しても良いですし、
Vue2.x系でも Vue.observable
といったAPIを利用することでもほぼ同様のことが可能です。
しかし、Composition API を利用した書き換え時の利点として watch
APIの存在が挙げられます。
watch
APIは Composition API で生成されたリアクティブな値を監視し、変更時に任意の処理を実行できます。
これにより、従来はDOMに依存していた箇所を、少しずつデータを中心とする形に書き換えていけます。
import Vue from 'vue';
import VueCompositionApi, { watch } from '@vue/composition-api';
import { useTodo } from './useTodo';
Vue.use(VueCompositionApi);
const todoState = useTodo();
// 件数の変更をトリガーにDOMを更新
watch(todoState.count, (count) => {
$('#todoCount').text('Total: ' + count);
});
// 件数の増分だけTODOを追加
watch(todoState.count, (count, prevCount = 0) => {
[...Array(count - prevCount)].map(() => {
var input = $('<input>');
input.attr('type', 'text');
var todo = $('<div>');
todo.addClass('todo');
todo.append(input);
$('#todoList').append(todo);
});
});
$(function() {
$('#addTodo').on('click', function() {
todoState.addTodo();
});
});
件数の表示やTODOの追加の処理が、データの変更をトリガーに動作するようになりました。
何が嬉しいのか
コアとなる処理のみを先に切り出せる
古き良きDOM依存コードは、カオスなDOM操作コードの影に隠れている 「アプリケーション全体はどういうデータを必要とするのか?」「結局何をやってるのか?」という点を洗い出すのに非常に苦労します。
今回の方法の場合、予めコアとなるロジック部をデータを含めて切り出すことができ、既存の挙動を維持したままコア部分のみテストを書き足して動作を保証しておく、といったことも可能となります。
DOM依存箇所を徐々に減らしていける
DOMに依存しているコードを削っていく場合、影響箇所を把握するのが一番骨の折れる作業になります。
watch
の利用は人によっては抵抗がありそうですが、データを中心とした世界に徐々にシフトしていくことで、Vue.jsコンポーネントに切り出す際も、どういった単位で進めれば良いかの判断がしやすくなります。
切り出したものがVue.jsコンポーネント内部でそのまま使える
最終的にVue.jsへの置き換えを想定する場合、大きい恩恵になるのがコレだと予想されます。
Vue Composition APIを利用している関数は、当然ですが最終的にテンプレートごとVue.jsコンポーネント化した際にそのまま利用できます。事前にレガシーコード時に切り出したコアロジックが、そのまま持っていけるのです。
上記サンプルコードの場合、最終的に次のような形で利用可能です。
<template>
<div>
<button @click="addTodo">Add Todo</button>
<div>
<div v-for="(todo, index) in todoList" :key="index">
<input type="text" :value="todo.todo" @input="updateTodo(index, $event.target.value)" />
</div>
</div>
<span>Total: {{count}}</span>
</div>
</template>
<script>
import { createComponent } from "@vue/composition-api";
import { useTodo } from "./useTodo";
export default createComponent({
name: 'App',
setup() {
return {
...useTodo() // 事前に作成したコアロジックを利用
}
}
});
</script>
import { reactive, computed, toRefs } from "@vue/composition-api";
export const useTodo = () => {
const state = reactive({
todoList: []
});
const count = computed(() => state.todoList.length);
const addTodo = () => {
state.todoList.push({ todo: "" });
};
const updateTodo = (index, todo) => {
state.todoList[index].todo = todo;
}
return {
...toRefs(state),
count,
addTodo,
updateTodo
}
};
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';
import App from "./App.vue";
Vue.use(VueCompositionApi);
new Vue({
render: (createElement) => createElement(App)
}).$mount("#app");
テストを先に書いておけば、コアロジックは壊していないことを担保したままで、Vue.jsテンプレートとの連携だけを意識して書き換えていくことが可能となり、段階的な移行時には安心して作業を進めていけそうです。
まとめ
というわけで、Vue Composition APIを利用した段階的なモダン化の考察でした。
正直冗長な部分もあるので、どんなシチュエーションでも使える手法ではないとは思います。
(さっさとVue.jsでガッと書き換えた方が速いでしょ、と思う気持ちもありますし、そもそもゼロから全部書き換えたい気持ちになることも多そう)
ひとまず、手札のひとつとして持っておくと便利かもな〜と思いました。