クライアントサイドアーキテクチャー

Laravelを採用したシステム開発では同時にクライアントサイドのJavaScriptのアーキテクチャーも見直しました。

React, Vue.js

まずは、クライアントサイドのフレームワークではReactVue.jsなどが知られていますのでこれを研究しました。

React のチュートリアルを進めていくと、激しくリファクタリングを行う必要があり、開発の負担が重そうに感じました。

Vue.js はあまり複雑ではなく開発しやすそうに思いました。しかしながら、Model と View が分離しているようで分離していないので、シングルモデル - マルチビューが実現しにくいと思いました。

独自フレームワークの作成

そこでレイヤーアーキテクチャーによって Model と View を分離するフレームワークを新たに作成しました。View(アプリケーション層を含む)+ドメイン層+データアクセス層に分けています。React や Vue.js が実現している1文字入力するたびに反応する Reactive な機能は実現できていませんが、当方の開発範囲ではフィールド毎に反応する機能があればよいと考えています。

いくつか割り切った部分があります。

ECMAScript 6 を使用しています。現在では ECMAScript 6を前提としても問題ない状況になってきていると判断しました。また、jqueryの使用をやめました。

サーバーとのデータ交換は JSON で行うと限定し、FORMによる GET, POST をサポートする機能を実装しませんでした。レンダリング対象となるデータは JSON でクライアントに送信し、クライアント側で DOM を構築するようにしました。

データアクセス層のオブジェクトの取得は依存性注入を行いたかったのですが JavaScript では若干困難ですので諦めました。

バインディングによる Model と View の連係

Model と View との連係は双方向データバインディングによっています。

下記のように、HTML 上の要素に属性を記述することでバインディングを指定しています。

1
2
3
4
5
6
7
8
9
<dl>
<dt>注文番号</dt>
<dd><b>data-property="orderNo"</b> data-input-type="static"></dd>
</dl>
<dl>
<dt>顧客名</dt>
<dd><input type="text" <b>data-property="customerName"</b> data-uneditable-behavior="disabled"/>
<span class="error-message" data-property-error="customerName"></span></dd>
</dl>

上記コードでdata-nodeはプロパティの親となるエンティティや埋め込みバリューを指定し、data-propertyはプロパティを指定しています。

独自のテンプレート構文を導入するのではなく pure HTML で実装することで、デザイナーとの分業がスムーズになります。お納めしたシステムをお客様のデザイナーが更新しています。

サーバー側に Laravel を採用すれば Blade ファイルとなるのですが、JavaScript ファイル読み込みのためにタグを使っているのみなのでデザイナー様も問題なく扱っておられます。

算出プロパティ

Vue.js を調べるうちに 算出プロパティ という機能に興味を持ちました。

この機能は計算式を書いておけば必要なときに計算式が自動的に実行されるものです。

算出プロパティはangularJSで既に実現されていたようです。

以前に当方も独自に同様の機能を作成したことがあるのですが、計算式と合わせて計算のトリガーとなるプロパティ名を設定しなければならないもので、あまり便利とは言えませんでした。
一方 Vue.js の算出プロパティでは計算式さえ設定すればよく、非常に便利なものでした。仕組みを調べると、計算式を評価するたびに依存元を分析・記録するようになっていました。
パフォーマンス的な点が心配になりましたが、そもそも依存元の値に変化がなければ計算式を評価しないので問題は少ないだろうと結論付けています。

早速、同様の機能を当方の Model にも実装しました。採用してみると大変便利で依存性のことをほとんど意識しなくてよく、宣言的に計算式を書きさえすればよくなりました。また、オブジェクトの境界を越えて依存性をたどってくれる点も便利です。

算出プロパティ実装時に工夫した点はリストのようにメソッド呼び出しによって内部の値を変えるケースにも対応したことです。例えば、明細を集計した金額を表示するといったケースにも対応できます。

次のように記述できます。このコードとデータバインディングを組み合わせることによって明細が入力されたら常に合計を計算できます。※コピペで使えるものではありません。

1
2
3
4
5
6
7
8
9
10
11
12
13
export class Order extends Data.ObservableObject {
/**
* 初期化します。
*/
initialize() {
const totalAmountExpression = () => {
return <b>Enumerable.from(this.orderDetails).sum(item => item.amount)</b>
} //合計金額
this.defineProperty('totalAmount', DataType.Number, totalAmountExpression) //合計金額
this.defineInternalChangeProperty('orderDetails',
new Data.ListChangeDetector(),
new Data.ObservableList({createItem: () => new OrderDetails(this)})) //受注明細リスト
}

今回の開発とは関係がありませんが、算出プロパティを C# にも移植し、便利に使っております。

仮想 DOM

React、Vue.js では仮想 DOM によってレンダリングの負担を軽減しています。当方の開発したフレームワークでは仮想 DOM を採用していません。Model が変化すればバインディングによって同時に DOM も変化します。この点でパフォーマンスの心配がありました。

動作させてみると DOM が変更されたらすぐにブラウザーがレンダリングするのではなく、一連のスクリプトが終了してからレンダリングしているようです。

仮想DOMのパフォーマンスについて Vue・React・Angularのパフォーマンス比較検証 という興味深い記事がありました。リンク先のテストではいずれの場合も仮想DOMを使った場合パフォーマンスが落ちています。但し、ほぼすべての要素が変更される条件でのテストなので仮想DOMのメリットが出にくいです。

当方は Firefox でテストしてみたところそれでも仮想DOMの方が早かったです。

Firefox では仮想DOMが早く、Chrome では実在DOMの方が早いという結果から、Chromeでは逐次的にDOMに変更が加えられてもパフォーマンスが落ちないように対策されていると言えます。

以上のことから対象ブラウザーやスピードに求められる条件にもよりますが仮想DOMなしで実用になると考えています。

不変オブジェクト

React では仮想DOMの変更箇所を効率的に特定するために各オブジェクトを不変としています。

不変オブジェクトの場合、参照が同じであれば配下の構造は変更がないと解釈できるので構造を末端までたどる必要がないのです。

オブジェクトのプロパティを変更する場合は、該当するプロパティに変更後の値が設定された新しいオブジェクトを作る必要があります。新しいオブジェクトを作ればそのオブジェクトを所有するオブジェクトも連鎖的に作成する必要があります。オブジェクトグラフはツリー構造になっていますので、変更を末端から最上位まで連鎖的に伝える必要があります。

この作業は負担になりますので、immer など不変でない操作を不変操作に変換するライブラリを使って負担を軽減できます。

当方の仕組みでは、可変オブジェクトを使用していますのでこのような配慮は不要です。また対応する DOM 要素はバインディングによってピンポイントに結合されているので、変更箇所を特定するためのオーバーヘッドはありません。

開発したフレームワークを採用した結果

開発したフレームワークを採用した結果、下記のメリットを感じました。

  • Model, View に分離することですっきりとした構成になった。特に今回の開発で同じエンティティを2箇所以上に表示すると言った要件がたまたまあったが、これに難なく対応できた。
  • サーバーとのデータ交換を JSON のみにしたことで HTML の POST, GET に関連する複雑な仕様に悩まされることがなくなった。
  • 算出プロパティによって記述量が減った。要件変更にも対応しやすくなった。
参照