ASP.NET MVCでKnockoutからVue.jsに移行してみた

この記事は Vue.js #3 Advent Calendar 2017 の22日目の記事です。

ASP .NET MVCでは、数年前までデフォルトのJavaScriptライブラリとしてKnockoutが付属していた時期が長く続きました(現在はAngularやReactを選択できる)。

Knockout自体は細々とメンテナンスされているものの、メジャーバージョンアップはされない雰囲気になっています。そのため、当時開発したアプリを改修しようとすると、情報が不足していたり、モダンなフロントエンドの開発テクニックが使えなかったりと少し残念な状況になっています。

そこで、最近のメジャーなスタックを使って改修できないかといろいろ検証していたところ、クラシックなASP .NET MVCでは、Vue.jsへの移行がわりといい感じにできたので、この記事ではKnockoutからVue.jsに移行する方法を紹介したいと思います。

Vue.jsの導入

NuGetからVue.jsのパッケージをインストールします。NuGetにもわりと新しめのバージョン(執筆時点で2.5.2)が反映されていました。パッケージの説明欄にEvanさんの名前も書いてあったので、これが公式だと思われます。

PM> Install-Package vue

これでScriptsフォルダにvue.jsとvue.min.jsが保存されます。というかNuGetがやってくれるのはこれだけなので、CDNでも十分かもしれません。

ちなみに、パッケージ管理はnpmの利用も考えましたが、ASP .NET MVCの仕組みにおいてはあまりメリットを感じなかったので、基本的にASP .NET MVCデフォルトの流儀に従うようにしました。

次にバンドラの設定を行います。フロントエンド開発では通常webpackのようなバンドラを使うと思いますが、ここではASP .NET MVCの流儀に逆らわず、BundleConfig.csで設定します。

bundles.Add(new ScriptBundle("~/bundles/vue").Include
("~/Scripts/vue.js"));

これで、Releaseビルド時にはASP .NET側でMinifyをしてくれるようになります。BundleConfigの工夫次第ではjsファイルの結合もできますが、webpackのように依存関係を使ったバンドリングではないので、あまりメリットはありません。そもそも、ASP .NET MVCベースだとサーバサイドでのルーティングになるため、結合したところで画面切替のたびに結局JSファイルのロードは行われるので。。

Vue.jsの各種設定

今回のアプリ構成ではSPAやVue.jsでいうところのSFC(Single File Components)を前提としてはいないので、各HTMLファイルからScriptタグを使ってvue.jsファイルを読み込むという最もクラシックな方法を採らざるを得ません。それでも各ページに同じ定義は書きたくないので、レイアウト用のcshtmlで読むようにします。

ただし、全てのページを一気に移行するわけではないので、デフォルトの共通レイアウトページ(通常は~/Views/Shared/_Layout.cshtml)には引き続きKnockoutを利用する設定を残し、Vueに移行するページ用のレイアウトページは別途作成し(例えば~/Views/Shared/_LayoutVue.cshtml)、そこでvue.jsを読み込むようにします。

knockout用レイアウト: ~/Views/Shared/_Layout.cshtml

@Scripts.Render("~/bundles/knockout") @* knockoutの読み込み *@
@RenderSection("scripts", required: false)

vue用レイアウト: ~/Views/Shared/_LayoutVue.cshtml

@Scripts.Render("~/bundles/vue") @* vueの読み込み *@
@RenderSection("scripts", required: false)

そして、Vue.jsベースに移行したい個別のcshtmlで、レイアウトファイルを切り替えておきます。

@{
ViewBag.Title = "Todo";
Layout = "~/Views/Shared/_LayoutVue.cshtml";
}

スクリプトの移行

個別のjsファイルに書かれたKnockoutベースのコードをVue.jsベースに移行します。基本的にはライブラリに依存する箇所をVue方式に変えていくイメージです。

以下の例では、両ライブラリの入力・出力・条件付きレンダリング・リストレンダリング・イベントハンドラをそれぞれ比較できるようなごく簡単なTodo画面を作っています。ちなみに動くコードはGithubに置きましたので、ご興味のある方はどうぞ。

knockoutでの実装

function ViewModel() {
var self = this;

self.todoSummary = ko.observable("");
self.todoList = ko.observableArray([
{
summary: ko.observable("1つ目のtodo"),
done: ko.observable(true)
},
{
summary: ko.observable("2つ目のtodo"),
done: ko.observable(false)
}
]);

self.addTodo = function () {
var todo = {
summary: self.todoSummary(),
done: ko.observable(false)
};

self.todoList.push(todo);
self.todoSummary("");
};
}

ko.applyBindings(new ViewModel(), document.getElementById("#todo-list"));

Vueに変更した例

var vm = new Vue({
el: "#todo-list",
data: {
todoSummary: "",
todoList: [
{
summary: "1つ目のtodo",
done: true
},
{
summary: "2つ目のtodo",
done: false
}
]
},
methods: {
addTodo : function (event) {
var todo = {
summary: this.todoSummary,
done: false
};

this.todoList.push(todo);
this.todoSummary = "";
}
}
})

Vueではだいぶシンプルになりました。functionの嵐になりがちなKnockoutに比べ、オブジェクトリテラルをベースに書けるのはやはりいいです。特に、Knockoutで面倒だったobservable()の記述が不要になる点は嬉しいですね。あと、selfを使ったthisの参照も不要になるので、コードの可読性も上がります。

それでも基本的なオブジェクトやメソッドの構造は変わっていないので、これなら移行前の仕様を熟知していなくてもなんとか移行できそうです。

マークアップの移行

マークアップの移行は、どちらのフレームワークも基本的なバインディングの考え方が同じなので、ほぼ機械的に対応できました。

Knockoutでのマークアップ:

<div id="todo-list">
<input type="text" data-bind="value: todoSummary, valueUpdate: 'afterkeydown'" placeholder="todoを入力" />
<button data-bind="click: addTodo">追加</button>

<ul class="list-group" data-bind="foreach: todoList">
<li class="list-group-item">
<input type="checkbox" data-bind="checked: done" />
<span data-bind="text: summary, css: { 'text-success': done }"></span>
</li>
</ul>
</div>

Vueに変更したマークアップ:

<div id="todo-list">
<input type="text" v-model="todoSummary" placeholder="todoを入力" />
<button v-on:click="addTodo">追加</button>

<ul class="list-group">
<li class="list-group-item" v-for="item in todoList">
<input type="checkbox" v-model="item.done" />
<span v-bind:class="{ 'text-success': item.done}">{{ item.summary }}</span>
</li>
</ul>
</div>

配列周りはループを書く要素が変わったりするので少し注意が必要でしたが、それ以外の要素はほとんどがdata-bind="xxx: yyy"v-xxxに変えるだけという感じです。HTML自体の構造はほとんど変える必要がないので、マークアップの移行は比較的簡単だと思います。HTMLの雰囲気はほとんど変わりませんね!

さらなる改善を目指すには

このアプローチでは、少しずつKnockoutからVue.jsに移行することが可能です(変更が入った画面単位でじっくり移行とか)。しかも、そんなにソースの構造も変わらないので移行リスクも小さいと思います。

それでもよりモダンなフロントエンドへの移行を目指すなら、続けて以下のような対応をやっていく感じになると思います。

  • 全てのスクリプトとマークアップをVueベースに移行する
  • Knockout関連リソースを削除する
  • パッケージ管理をnpmベースに変更する
  • バンドラをwebpack等に変更する
  • TypeScriptを導入する
  • コンポーネント化 などなど

やることがいっぱいありますね!
でも、今回紹介した方法でも十分Vue.jsの強みは活かせると思うので、これら追加の対応するだけの大きなメリットがなければ、今回紹介した方法でも十分だと思っています。

個人的にはもしASP .NETベースでアプリを新しく開発するなら、バックエンドは .NET CoreでAPIのみ実装し、フロントエンドはVueを使ったSPAで構成し、フロントエンドはピュアなJSの各種スタックを使えるようにすると思います。