この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの23日目です。
Structiveについて詳しくはこちらより
前回のおさらい
Day 22では、Reactとの比較を行いました。今日は、テンプレート記法が類似しているVueとの比較を、ショッピングカートアプリを例に見ていきます。
Vueでの実装
同じショッピングカート機能をVue 3のComposition APIで実装してみましょう。
SFCコンポーネント
<template>
<section>
Select Product:
<select v-model.number="selectedProductId">
<option
v-for="product in products"
:key="product.id"
:value="product.id"
>
{{ product.name }} - ${{ product.price.toFixed(2) }}
</option>
</select>
<button @click="handleAddToCart">Add to Cart</button>
</section>
<section>
<h2 v-if="cartItems.length > 0">Your Shopping Cart</h2>
<h2 v-else>Your cart is empty.</h2>
<table>
<thead>
<tr>
<th>No.</th>
<th>Product</th>
<th>Unit Price($)</th>
<th>Quantity</th>
<th>Price($)</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in enrichedCartItems" :key="item.productId">
<td>{{ index + 1 }}</td>
<td>{{ item.product.name }}</td>
<td>${{ item.product.price.toFixed(2) }}</td>
<td>
<input
type="number"
:min="minQuantity"
v-model.number="item.quantity"
/>
</td>
<td>${{ item.price.toFixed(2) }}</td>
<td>
<button @click="handleDelete(index)">Delete</button>
</td>
</tr>
<tr>
<td colspan="4">Total ($):</td>
<td>${{ totalPrice.toFixed(2) }}</td>
<td></td>
</tr>
<tr>
<td colspan="4">Tax ({{ (taxRate * 100).toFixed(0) }}%) ($):</td>
<td>${{ tax.toFixed(2) }}</td>
<td></td>
</tr>
<tr>
<td colspan="4">Grand Total ($):</td>
<td>${{ grandTotal.toFixed(2) }}</td>
<td></td>
</tr>
</tbody>
</table>
</section>
</template>
<script setup>
import { ref, computed } from 'vue';
const products = [
{ id: 1, name: "Laptop", price: 999.99 },
{ id: 2, name: "Smartphone", price: 499.99 },
{ id: 3, name: "Headphones", price: 199.99 },
{ id: 4, name: "Smartwatch", price: 149.99 },
{ id: 5, name: "Tablet", price: 299.99 },
{ id: 6, name: "Camera", price: 599.99 },
{ id: 7, name: "Printer", price: 89.99 },
{ id: 8, name: "Monitor", price: 249.99 },
{ id: 9, name: "Keyboard", price: 49.99 },
{ id: 10, name: "Mouse", price: 29.99 }
];
const taxRate = 0.1;
const minQuantity = 1;
// リアクティブな状態
const cartItems = ref([
{ productId: 1, quantity: 1 },
{ productId: 5, quantity: 2 }
]);
const selectedProductId = ref(products[0].id);
// 商品IDマップ
const productById = new Map(products.map(p => [p.id, p]));
// 計算プロパティ: カートアイテムに商品情報を追加
const enrichedCartItems = computed(() => {
return cartItems.value.map(item => ({
...item,
product: productById.get(item.productId),
price: productById.get(item.productId).price * item.quantity
}));
});
// 計算プロパティ: 合計金額
const totalPrice = computed(() => {
return enrichedCartItems.value.reduce((sum, item) => sum + item.price, 0);
});
// 計算プロパティ: 税額
const tax = computed(() => totalPrice.value * taxRate);
// 計算プロパティ: 総合計
const grandTotal = computed(() => totalPrice.value + tax.value);
// カートに追加
const handleAddToCart = () => {
const existingIndex = cartItems.value.findIndex(
item => item.productId === selectedProductId.value
);
if (existingIndex >= 0) {
cartItems.value[existingIndex].quantity++;
} else {
cartItems.value.push({
productId: selectedProductId.value,
quantity: 1
});
}
};
// カートから削除
const handleDelete = (index) => {
if (!window.confirm("Are you sure you want to delete this item?")) {
return;
}
cartItems.value.splice(index, 1);
};
</script>
<style scoped>
section {
margin-bottom: 2em;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
}
table, th, td {
border: 1px solid #ccc;
}
td {
padding: 0.5em;
text-align: center;
}
input[type=number] {
width: 4em;
text-align: right;
}
</style>
StructiveとVueの比較
1. リアクティビティシステム
Structive:
- Proxyベースのリアクティビティ
- 構造パスをキーとして変更を追跡
- setトラップで変更を検知し、該当する構造パスのUIのみ更新
// 構造パスで直接代入
this["cart.items.2.quantity"] = 5;
// → "cart.items.2.quantity"に紐づくUIだけが更新される
Vue:
- Vue 3もProxyベースのリアクティビティ(Vue 2はObject.defineProperty)
- オブジェクト全体を監視し、プロパティアクセスを追跡
- ネストされたオブジェクトも自動的にリアクティブ化
// refまたはreactiveでラップ
const cartItems = ref([...]);
cartItems.value[2].quantity = 5;
// → 依存する計算プロパティとテンプレートが再評価される
類似点:
- どちらもProxyを使用
- 自動的な依存関係の追跡
- 細かい粒度での更新
相違点:
- Structiveは構造パス単位で更新箇所を特定
- Vueは依存関係グラフを構築して影響範囲を計算
2. 状態管理のアプローチ
Structive:
- クラスベースの状態管理
- プロパティとgetterで状態を定義
- 構造パスで統一的にアクセス
export default class {
cart = { items: [...] };
get "cart.items.*.price"() {
return this["cart.items.*.product.price"] * this["cart.items.*.quantity"];
}
}
Vue:
- Composition APIで
refとreactiveを使用 -
computedで派生状態を定義 -
.value経由でアクセス(refの場合)
const cartItems = ref([...]);
const enrichedCartItems = computed(() => {
return cartItems.value.map(item => ({
...item,
price: item.product.price * item.quantity
}));
});
3. テンプレート構文
Structive:
- カスタムテンプレート構文
-
{{ }}で変数展開、{{ for: }}でループ -
data-bindでバインディング
{{ for:cart.items }}
<td>{{ cart.items.*.product.name }}</td>
<input data-bind="valueAsNumber: cart.items.*.quantity">
{{ endfor: }}
Vue:
- HTMLベースのテンプレート構文
-
{{ }}で変数展開、v-forでループ -
v-modelで双方向バインディング、:で属性バインディング
<tr v-for="(item, index) in enrichedCartItems" :key="item.productId">
<td>{{ item.product.name }}</td>
<input v-model.number="item.quantity">
</tr>
類似点:
- どちらも宣言的なテンプレート
- マスタッシュ構文(
{{ }})の使用 - 双方向バインディングのサポート
相違点:
- Vueは標準的なHTML属性として指令を記述(
v-for,v-if) - Structiveは独自の
{{ for: }}構文 - Vueの
v-modelは明示的、構造パスフレームワークは自動判定
4. 派生状態(計算プロパティ)
Structive:
- getterで派生状態を定義
- 構造パスをそのままgetter名として使用
- ワイルドカード対応で配列要素ごとの派生状態も定義可能
get "cart.totalPrice"() {
return this.$getAll("cart.items.*.price", [])
.reduce((sum, value) => sum + value, 0);
}
get "cart.items.*.price"() {
return this["cart.items.*.product.price"] * this["cart.items.*.quantity"];
}
Vue:
-
computed関数で計算プロパティを定義 - 依存する値が変更されると自動的に再計算
- 配列要素ごとの派生状態は中間データ構造を作成
const totalPrice = computed(() => {
return enrichedCartItems.value.reduce((sum, item) => sum + item.price, 0);
});
const enrichedCartItems = computed(() => {
return cartItems.value.map(item => ({
...item,
price: item.product.price * item.quantity
}));
});
類似点:
- どちらも依存関係を自動追跡
- キャッシュされ、依存値が変わらない限り再計算されない
- 宣言的な記述
相違点:
- Structiveは構造パスで一貫した記述
- Vueは中間データ構造を明示的に作成する必要がある
5. イベントハンドリング
Structive:
-
data-bindで構造パスを指定 - ループコンテキスト(
$1)が自動的に渡される - メソッド内で構造パスを使って直接操作
onDeleteItemFromCart(event, $1) {
this["cart.items"] = this["cart.items"].toSpliced($1, 1);
}
<button data-bind="onclick: onDeleteItemFromCart">Delete</button>
Vue:
-
@click(またはv-on:click)でイベントバインディング - インライン式またはメソッドを指定
- インデックスは明示的に渡す
const handleDelete = (index) => {
cartItems.value.splice(index, 1);
};
<button @click="handleDelete(index)">Delete</button>
類似点:
- どちらも宣言的なイベントバインディング
- メソッドベースのハンドリング
相違点:
- Vueは引数を明示的に渡す
- Structiveはループコンテキストを自動取得
- Vueはインライン式も記述可能(
@click="count++")
6. 双方向バインディング
Structive:
-
inputのvalue属性など自明な場合は自動的に双方向 -
data-bindで構造パスを指定するだけ
<input type="number" data-bind="valueAsNumber: cart.items.*.quantity">
<select data-bind="value|number: selectedProductId">
Vue:
-
v-modelディレクティブで明示的に指定 - 修飾子(
.number,.trim,.lazy)でカスタマイズ可能
<input type="number" v-model.number="item.quantity">
<select v-model.number="selectedProductId">
類似点:
- どちらも双方向バインディングをサポート
- 型変換(number)のサポート
相違点:
- Vueは
v-modelで明示的 - 構造パスフレームワークは自動判定だが、型変換はパイプで指定
コード量と可読性の比較
| 項目 | Structive | Vue |
|---|---|---|
| 総コード行数 | 約110行 | 約120行 |
| 状態定義 | クラスプロパティ |
ref/reactive
|
| 派生状態 | getter | computed |
| テンプレート | カスタム構文 | HTML拡張 |
| 学習曲線 | 構造パスの理解 | Vue APIの習得 |
Vueの方がやや行数が多いですが、大きな差はありません。より重要なのは記述スタイルの違いです。
状態更新の比較
カートへの商品追加:
// Structive
onAddProductToCart() {
if (existingIndex >= 0) {
this[`cart.items.${existingIndex}.quantity`] += 1;
} else {
this["cart.items"] = this["cart.items"].concat(newCartItem);
}
}
// Vue
const handleAddToCart = () => {
if (existingIndex >= 0) {
cartItems.value[existingIndex].quantity++;
} else {
cartItems.value.push(newCartItem);
}
};
Vueの方がJavaScriptとして自然な記述です。Structiveは文字列ベースの構造パスを使います。
学習曲線の比較
Structive:
学ぶべき概念:
- 構造パス
-
{{ }}記法(if, for) data-bind- getter(派生状態)
- ワイルドカード(
*)
差分: Vueより学ぶことが少ない
Vue:
学ぶべき概念:
- テンプレート記法(
v-if,v-for,v-bind,v-model) - Options API(data, computed, methods, watch)
- Composition API(Vue 3)
- コンポーネントシステム
- リアクティビティの仕組み
- ライフサイクルフック
- ディレクティブ
パフォーマンスの比較
Structive:
- 構造パス単位でのピンポイント更新
- 仮想DOMなし
- 変更検知から描画まで最短経路
Vue:
- Vue 3の最適化されたリアクティビティシステム
- 仮想DOMによる効率的な更新
- コンパイラによる最適化(静的ホイスティング、パッチフラグ)
両者とも高いパフォーマンスを発揮しますが、アプローチが異なります:
- 構造パスフレームワークは「仮想DOMを使わない」ことで効率化
- Vueは「仮想DOMを最適化する」ことで効率化
開発体験の比較
Structive
長所:
- UIと状態の一貫性(同じ構造パスを共有)
- 影響範囲の追跡が容易(文字列検索で完結)
- ボイラープレートが少ない
- ループコンテキストの自動取得
短所:
- 新しい概念(構造パス)の学習
- 文字列ベースのアクセスによる型安全性の低下
- エコシステムの未成熟
- IDEサポートの不足
Vue
長所:
- 直感的で学習しやすいAPI
- 豊富なエコシステム(Vuetify, Nuxt, Pinia等)
- 優れた公式ドキュメント
- Vue DevToolsによる強力なデバッグ機能
- TypeScript統合(Vue 3)
- 段階的な導入が可能(プログレッシブフレームワーク)
短所:
- Composition APIとOptions APIの両方の学習
-
.valueの記述が冗長になることがある - 大規模アプリでは状態管理ライブラリ(Pinia)が必要
実装パターンの具体例
数量の変更
Structive:
<!-- 自動的に双方向バインディング -->
<input type="number" data-bind="valueAsNumber: cart.items.*.quantity">
// 状態が自動的に更新される
// 派生状態も自動的に再計算される
Vue:
<!-- v-modelで双方向バインディング -->
<input type="number" v-model.number="item.quantity">
// itemはenrichedCartItemsから来ている
// 元のcartItems.valueが更新される
// 計算プロパティが再評価される
条件付きレンダリング
Structive:
{{ if:cart.items.length|gt,0 }}
<h2>Your Shopping Cart</h2>
{{ else: }}
<h2>Your cart is empty.</h2>
{{ endif: }}
Vue:
<h2 v-if="cartItems.length > 0">Your Shopping Cart</h2>
<h2 v-else>Your cart is empty.</h2>
Vueの方がHTML標準に近く、視覚的にシンプルです。
エコシステムの比較
Structive
- 現状: 新興フレームワークとしてコアコンセプトを確立中
- ツール: 開発ツールはこれから
- コミュニティ: 成長段階
- ライブラリ: 基本機能に集中
Vue
- 成熟度: 10年近い開発とコミュニティの蓄積
- 公式ツール: Vite, Vue Router, Pinia
- エコシステム: Nuxt(フルスタックフレームワーク)、Vuetify(UIライブラリ)等
- 企業採用: Alibaba, GitLab, Adobe等多数
VueとReactの比較から見えるStructiveの位置づけ
昨日のReactとの比較も含めて考えると:
記述スタイル:
- React: JSX、関数型、明示的
- Vue: テンプレート、宣言的、直感的
- Structive: テンプレート、構造パス中心、一貫性
リアクティビティ:
- React: 明示的な状態管理、イミュータブル更新
- Vue: 自動追跡、ミュータブル可
- Structive: 自動追跡、構造パスで統一
学習曲線:
- React: Hooksとパターンの習得
- Vue: 段階的、直感的
- Structive: 構造パスという一つの概念
Structiveは、Vueの「宣言的テンプレート」と「リアクティビティ」の良さを引き継ぎつつ、「構造パス」という統一概念でさらにシンプル化を図っていると言えます。
まとめ
VueとStructiveの比較を行いました。
Structiveの強み:
- 一貫性: 構造パスという単一概念で統一
- シンプルさ: UIと状態が同じ構造パスを共有
- 追跡性: 文字列検索で影響範囲を特定
- 効率性: ピンポイント更新で高パフォーマンス
Vueの強み:
- 成熟度: 豊富なエコシステムと実績
- 学習しやすさ: 直感的なAPIと優れたドキュメント
- 柔軟性: プログレッシブフレームワークとして段階的導入可能
- ツール: Vue DevToolsなど充実した開発環境
VueとStructiveは、どちらもテンプレートベースでリアクティビティを重視している点で似ています。最大の違いは、Structiveが「構造パス」という統一概念でUIと状態を結びつけるのに対し、Vueは多様なAPIとディレクティブを提供する点です。
Vue経験者にとって、Structiveの概念は理解しやすいでしょう。一方、Vueのエコシステムと成熟度は、プロダクション環境では大きなアドバンテージとなります。
明日のDay 24では、Angularとの比較を行います。TypeScriptファーストでエンタープライズ向けのAngularと、Structiveの違いを見ていきます。お楽しみに!