0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Day 23: 既存フレームワーク(Vue)との比較

0
Last updated at Posted at 2025-12-22

この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(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でrefreactiveを使用
  • 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:

  • inputvalue属性など自明な場合は自動的に双方向
  • 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:

学ぶべき概念:

  1. 構造パス
  2. {{ }}記法(if, for)
  3. data-bind
  4. getter(派生状態)
  5. ワイルドカード(*

差分: Vueより学ぶことが少ない

Vue:

学ぶべき概念:

  1. テンプレート記法(v-if, v-for, v-bind, v-model
  2. Options API(data, computed, methods, watch)
  3. Composition API(Vue 3)
  4. コンポーネントシステム
  5. リアクティビティの仕組み
  6. ライフサイクルフック
  7. ディレクティブ

パフォーマンスの比較

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の強み:

  1. 一貫性: 構造パスという単一概念で統一
  2. シンプルさ: UIと状態が同じ構造パスを共有
  3. 追跡性: 文字列検索で影響範囲を特定
  4. 効率性: ピンポイント更新で高パフォーマンス

Vueの強み:

  1. 成熟度: 豊富なエコシステムと実績
  2. 学習しやすさ: 直感的なAPIと優れたドキュメント
  3. 柔軟性: プログレッシブフレームワークとして段階的導入可能
  4. ツール: Vue DevToolsなど充実した開発環境

VueとStructiveは、どちらもテンプレートベースリアクティビティを重視している点で似ています。最大の違いは、Structiveが「構造パス」という統一概念でUIと状態を結びつけるのに対し、Vueは多様なAPIとディレクティブを提供する点です。

Vue経験者にとって、Structiveの概念は理解しやすいでしょう。一方、Vueのエコシステムと成熟度は、プロダクション環境では大きなアドバンテージとなります。

明日のDay 24では、Angularとの比較を行います。TypeScriptファーストでエンタープライズ向けのAngularと、Structiveの違いを見ていきます。お楽しみに!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?