この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの14日目です。
Structiveについて詳しくはこちらより
前回のおさらい
Advent CalendarのDay 24、最終日です。これまでReact(Day 22)、Vue(Day 23)との比較を行ってきました。今日は、エンタープライズ向けフレームワークとして確固たる地位を築いているAngularとの比較を行います。
Angularは、TypeScriptファースト、フルスタックフレームワーク、そして強力な依存性注入(DI)システムを特徴としています。Structiveのシンプルさと、Angularの包括的なアプローチを比較することで、それぞれの設計思想の違いが明確になるでしょう。
Angularでの実装
同じショッピングカート機能をAngular(v17以降のスタンドアロンコンポーネント)で実装してみましょう。
コンポーネント実装
// shopping-cart.component.ts
import { Component, computed, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
interface Product {
id: number;
name: string;
price: number;
}
interface CartItem {
productId: number;
quantity: number;
}
interface EnrichedCartItem extends CartItem {
product: Product;
price: number;
}
@Component({
selector: 'app-shopping-cart',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './shopping-cart.component.html',
styleUrls: ['./shopping-cart.component.css']
})
export class ShoppingCartComponent {
private readonly TAX_RATE = 0.1;
readonly MIN_QUANTITY = 1;
products: Product[] = [
{ 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 }
];
private productById = new Map(
this.products.map(p => [p.id, p])
);
// Signalsで状態管理
cartItems = signal<CartItem[]>([
{ productId: 1, quantity: 1 },
{ productId: 5, quantity: 2 }
]);
selectedProductId = signal<number>(this.products[0].id);
// 計算されたSignal: カートアイテムに商品情報を追加
enrichedCartItems = computed<EnrichedCartItem[]>(() => {
return this.cartItems().map(item => {
const product = this.productById.get(item.productId)!;
return {
...item,
product,
price: product.price * item.quantity
};
});
});
// 計算されたSignal: 合計金額
totalPrice = computed(() => {
return this.enrichedCartItems().reduce(
(sum, item) => sum + item.price,
0
);
});
// 計算されたSignal: 税額
tax = computed(() => this.totalPrice() * this.TAX_RATE);
// 計算されたSignal: 総合計
grandTotal = computed(() => this.totalPrice() + this.tax());
// 税率(パーセント表示用)
taxRatePercent = this.TAX_RATE * 100;
// カートに追加
handleAddToCart(): void {
const currentItems = this.cartItems();
const existingIndex = currentItems.findIndex(
item => item.productId === this.selectedProductId()
);
if (existingIndex >= 0) {
// 既存アイテムの数量を増やす
const updatedItems = [...currentItems];
updatedItems[existingIndex] = {
...updatedItems[existingIndex],
quantity: updatedItems[existingIndex].quantity + 1
};
this.cartItems.set(updatedItems);
} else {
// 新しいアイテムを追加
this.cartItems.set([
...currentItems,
{ productId: this.selectedProductId(), quantity: 1 }
]);
}
}
// 数量変更
handleQuantityChange(index: number, newQuantity: number): void {
const currentItems = this.cartItems();
const updatedItems = [...currentItems];
updatedItems[index] = {
...updatedItems[index],
quantity: newQuantity
};
this.cartItems.set(updatedItems);
}
// カートから削除
handleDelete(index: number): void {
if (!window.confirm("Are you sure you want to delete this item?")) {
return;
}
const currentItems = this.cartItems();
this.cartItems.set(currentItems.filter((_, i) => i !== index));
}
// 選択された商品IDの変更を追跡
onProductSelectionChange(event: Event): void {
const select = event.target as HTMLSelectElement;
this.selectedProductId.set(Number(select.value));
}
}
テンプレート
<!-- shopping-cart.component.html -->
<section>
<label>
Select Product:
<select
[value]="selectedProductId()"
(change)="onProductSelectionChange($event)"
>
<option
*ngFor="let product of products"
[value]="product.id"
>
{{ product.name }} - ${{ product.price.toFixed(2) }}
</option>
</select>
</label>
<button type="button" (click)="handleAddToCart()">Add to Cart</button>
</section>
<section>
<h2 *ngIf="cartItems().length > 0; else emptyCart">Your Shopping Cart</h2>
<ng-template #emptyCart>
<h2>Your cart is empty.</h2>
</ng-template>
<table>
<colgroup>
<col style="width: 5%;">
<col style="width: 35%;">
<col style="width: 15%;">
<col style="width: 15%;">
<col style="width: 20%;">
<col style="width: 10%;">
</colgroup>
<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 *ngFor="let item of enrichedCartItems(); let i = index; trackBy: trackByProductId">
<td>{{ i + 1 }}</td>
<td class="left">{{ item.product.name }}</td>
<td class="right">${{ item.product.price.toFixed(2) }}</td>
<td>
<input
type="number"
[min]="MIN_QUANTITY"
[value]="item.quantity"
(input)="handleQuantityChange(i, +($any($event.target).value))"
/>
</td>
<td class="right">${{ item.price.toFixed(2) }}</td>
<td>
<button type="button" (click)="handleDelete(i)">Delete</button>
</td>
</tr>
<tr>
<td colspan="4" class="right"><strong>Total ($):</strong></td>
<td class="right">{{ totalPrice().toFixed(2) }}</td>
<td></td>
</tr>
<tr>
<td colspan="4" class="right">
<strong>Tax ({{ taxRatePercent.toFixed(0) }}%) ($):</strong>
</td>
<td class="right">{{ tax().toFixed(2) }}</td>
<td></td>
</tr>
<tr>
<td colspan="4" class="right"><strong>Grand Total ($):</strong></td>
<td class="right">{{ grandTotal().toFixed(2) }}</td>
<td></td>
</tr>
</tbody>
</table>
</section>
スタイル
/* shopping-cart.component.css */
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;
}
.left {
text-align: left;
}
.right {
text-align: right;
}
input[type=number] {
width: 4em;
text-align: right;
}
StructiveとAngularの比較
1. 状態管理システム
Structive:
- Proxyベースのリアクティビティ
- クラスプロパティとgetterで状態を定義
- 構造パスで統一的にアクセス
export default class {
cart = { items: [...] };
get "cart.totalPrice"() {
return this.$getAll("cart.items.*.price", [])
.reduce((sum, value) => sum + value, 0);
}
}
Angular:
- Signals(Angular 16+)またはRxJS Observable
-
signal()で状態を作成、computed()で派生状態 - 関数呼び出しでアクセス(
cartItems())
cartItems = signal<CartItem[]>([...]);
totalPrice = computed(() => {
return this.enrichedCartItems().reduce(
(sum, item) => sum + item.price,
0
);
});
類似点:
- どちらも自動的な依存関係追跡
- 派生状態の自動更新
- 細かい粒度での更新
相違点:
- Angularは明示的な
signal()とcomputed() - Structiveは構造パスという文字列ベース
- Angularは型安全性が高い(TypeScript)
2. テンプレート構文
Structive:
- カスタムテンプレート構文
-
{{ }}と{{ for: }}、{{ if: }} -
data-bindで双方向バインディング
{{ for:cart.items }}
<td>{{ cart.items.*.product.name }}</td>
<input data-bind="valueAsNumber: cart.items.*.quantity">
{{ endfor: }}
Angular:
- 構造ディレクティブ(
*ngFor,*ngIf) - プロパティバインディング(
[property])、イベントバインディング((event)) - 双方向バインディング(
[(ngModel)])
<tr *ngFor="let item of enrichedCartItems(); let i = index">
<td>{{ item.product.name }}</td>
<input
type="number"
[value]="item.quantity"
(input)="handleQuantityChange(i, +($any($event.target).value))"
/>
</tr>
類似点:
- どちらも宣言的なテンプレート
- 条件分岐とループのサポート
相違点:
- Angularは標準HTML属性として構造ディレクティブを記述
- Angularはプロパティとイベントを明確に分離(
[]と()) - Structiveは
*でループ要素にアクセス、Angularはlet itemで変数宣言
3. 型安全性
Structive:
- JavaScriptベース(TypeScript対応は可能だが、構造パスは文字列)
- 実行時のエラー検出
- 構造パスの誤りは実行時に判明
// タイポがあっても、実行時まで気づかない
this["cart.itmes.*.quantity"] = 5; // itmes → items
Angular:
- TypeScriptファースト
- コンパイル時の型チェック
- 厳格な型定義によるエラーの早期発見
// コンパイル時にエラーを検出
interface CartItem {
productId: number;
quantity: number;
}
cartItems = signal<CartItem[]>([...]);
// 型に合わないデータを入れるとコンパイルエラー
4. 依存性注入(DI)
Structive:
- DI機能なし
- シンプルなクラスインスタンス
- サービスは通常のクラスとしてインポート
export default class {
// サービスは直接インポートして使用
productService = new ProductService();
}
Angular:
- 強力なDIシステム
-
@Injectable()デコレーターでサービスを定義 - コンストラクタインジェクション
@Injectable({ providedIn: 'root' })
export class ProductService {
// サービスロジック
}
@Component({...})
export class ShoppingCartComponent {
constructor(private productService: ProductService) {
// DIコンテナから自動的に注入される
}
}
相違点:
- Angularは大規模アプリ向けのDIシステム
- テスタビリティの向上(モックの注入が容易)
- Structiveはシンプルだが、大規模化時は工夫が必要
5. イベントハンドリング
Structive:
-
data-bindで構造パスを指定 - ループコンテキスト(
$1)自動取得 - メソッド内で構造パスを使って操作
onDeleteItemFromCart(event, $1) {
this["cart.items"] = this["cart.items"].toSpliced($1, 1);
}
<button data-bind="onclick: onDeleteItemFromCart">Delete</button>
Angular:
-
(event)構文でイベントバインディング - 明示的にパラメータを渡す
-
$eventで元のイベントオブジェクトにアクセス
handleDelete(index: number): void {
const currentItems = this.cartItems();
this.cartItems.set(currentItems.filter((_, i) => i !== index));
}
<button type="button" (click)="handleDelete(i)">Delete</button>
6. 双方向バインディング
Structive:
- 自動判定で双方向バインディング
-
data-bindだけで完結
<input data-bind="valueAsNumber: cart.items.*.quantity">
Angular:
-
[(ngModel)]で双方向バインディング(FormsModuleが必要) - または
[property]と(event)の組み合わせ
<!-- 双方向バインディング -->
<input [(ngModel)]="item.quantity">
<!-- または明示的に -->
<input
[value]="item.quantity"
(input)="handleQuantityChange(i, +($any($event.target).value))"
/>
コード量と複雑度の比較
| 項目 | Structive | Angular |
|---|---|---|
| 総コード行数 | 約110行 | 約180行 |
| 状態定義 | クラスプロパティ | signal/computed |
| 型定義 | 不要(JSの場合) | 必須(interface) |
| インポート | 最小限 | モジュール、decorator等 |
| ボイラープレート | 少ない | 多い(decorator、型定義) |
| 学習曲線 | 構造パスの理解 | Angular全体のエコシステム |
Angularは約180行とより多くのコードが必要ですが、これは型安全性と明示性のトレードオフです。
状態更新の比較
カートへの商品追加:
// Structive
onAddProductToCart() {
if (existingIndex >= 0) {
this[`cart.items.${existingIndex}.quantity`] += 1;
} else {
this["cart.items"] = this["cart.items"].concat(newCartItem);
}
}
// Angular
handleAddToCart(): void {
const currentItems = this.cartItems();
const existingIndex = currentItems.findIndex(
item => item.productId === this.selectedProductId()
);
if (existingIndex >= 0) {
const updatedItems = [...currentItems];
updatedItems[existingIndex] = {
...updatedItems[existingIndex],
quantity: updatedItems[existingIndex].quantity + 1
};
this.cartItems.set(updatedItems);
} else {
this.cartItems.set([
...currentItems,
{ productId: this.selectedProductId(), quantity: 1 }
]);
}
}
Angularはイミュータブルな更新とSignalの.set()メソッドで明示的です。
パフォーマンスの比較
Structive:
- 構造パス単位でピンポイント更新
- 仮想DOMなし
- 最小限の再計算
Angular:
- Signals(v16+)による細かい粒度の変更検知
- OnPush変更検知戦略で最適化
- Zone.jsによる自動変更検知(オプション)
- コンパイル時最適化(AOT)
どちらも高いパフォーマンスを発揮しますが:
- Structiveは「シンプルさ」で効率化
- Angularは「高度な最適化技術」で効率化
開発体験の比較
Structive
長所:
- 学習コストが低い(構造パスという一つの概念)
- ボイラープレートが少ない
- UIと状態の一貫性(同じ構造パス)
- 小〜中規模プロジェクトで素早く開発
短所:
- 型安全性が低い(文字列ベース)
- 大規模プロジェクト向けの機能が不足(DI、モジュールシステム等)
- エコシステムが未成熟
- エンタープライズ向けの実績がない
Angular
長所:
- TypeScriptによる高い型安全性
- 強力なDIシステム
- 包括的なフレームワーク(ルーティング、HTTP、Forms等すべて揃っている)
- 大規模チーム開発向けの設計
- エンタープライズでの豊富な採用実績
- Angular CLI による強力な開発支援
- 明確なベストプラクティスとスタイルガイド
短所:
- 学習曲線が急(RxJS、DI、デコレーター等)
- ボイラープレートが多い
- 小規模プロジェクトにはオーバースペック
- バージョンアップによる破壊的変更の歴史
エコシステムとツールの比較
Structive
- 現状: コア機能に集中
- ツール: 基本的なローダー
- コミュニティ: 成長段階
- ドキュメント: 概念説明が中心
Angular
- 成熟度: 10年以上の開発とGoogle のバックアップ
-
公式ツール:
- Angular CLI(プロジェクト生成、ビルド、テスト)
- Angular Material(UIコンポーネントライブラリ)
- Angular CDK(コンポーネント開発キット)
-
エコシステム:
- Nx(モノレポ管理)
- NgRx(状態管理)
- Universal(SSR)
- 企業採用: Google、Microsoft、Forbes等多数
設計思想の違い
Structive: シンプリシティ
- 哲学: 「UIと状態は同じデータの異なる表出」
- 焦点: 構造パスという単一概念での統一
- 対象: 小〜中規模の高速開発
- 学習: 数時間〜数日で習得可能
Angular: エンタープライズファースト
- 哲学: 「包括的で意見の強いフレームワーク」
- 焦点: 大規模チーム開発の標準化
- 対象: エンタープライズアプリケーション
- 学習: 数週間〜数ヶ月の学習期間
3つのフレームワークとの比較まとめ
Day 22〜24の比較を総括すると:
| 項目 | 構造パス | React | Vue | Angular |
|---|---|---|---|---|
| 記述スタイル | 構造パス中心 | JSX/関数型 | テンプレート | テンプレート/TS |
| 状態管理 | Proxyとgetter | Hooks | ref/computed | Signals |
| 型安全性 | 低 | 中 | 中 | 高 |
| 学習曲線 | 緩やか | 中程度 | 緩やか | 急 |
| ボイラープレート | 最小 | 少 | 少 | 多 |
| エコシステム | 未成熟 | 非常に豊富 | 豊富 | 包括的 |
| 企業採用 | なし | 非常に多い | 多い | エンタープライズ |
| 適用規模 | 小〜中 | 小〜大 | 小〜大 | 中〜大 |
Structiveの位置づけ:
- シンプルさ: 4つの中で最もシンプル
- 一貫性: 構造パスという統一概念
- 効率性: 直接DOM操作によるパフォーマンス
- ニッチ: 小規模プロジェクトや学習用途
実際のプロジェクト選択の指針
Structiveを選ぶ場合
- プロジェクト規模: 小〜中規模
- 開発速度: 高速プロトタイピング
- チーム: 小規模(1〜5人)
- 要件: シンプルさとパフォーマンス重視
Reactを選ぶ場合
- プロジェクト規模: 小〜大規模
- エコシステム: 豊富なライブラリが必要
- チーム: 柔軟な開発スタイル
- 要件: 高い柔軟性とコミュニティサポート
Vueを選ぶ場合
- プロジェクト規模: 小〜大規模
- 学習: 初心者にも優しい
- チーム: 段階的な導入が必要
- 要件: バランスの取れた開発体験
Angularを選ぶ場合
- プロジェクト規模: 中〜大規模
- 要件: 型安全性とテスタビリティ
- チーム: 大規模チーム(10人以上)
- 環境: エンタープライズ、長期保守
まとめ
3日間にわたって、Structiveを主要な3つのフレームワーク(React、Vue、Angular)と比較してきました。
Structiveの本質:
- 構造パス: UIと状態を結ぶ単一の統一概念
- シンプリシティ: ボイラープレートの最小化
- 一貫性: 同じ構造パスでUIと状態を共有
- 効率性: ピンポイント更新による高パフォーマンス
各フレームワークから学べること:
- React: コンポーネント設計とエコシステムの重要性
- Vue: 直感性とプログレッシブな導入の価値
- Angular: 型安全性と大規模開発の標準化
Structiveは、既存のフレームワークを置き換えることを目指すものではありません。むしろ、**「UIと状態を結ぶより直接的な方法はないか?」**という問いかけから生まれた、別のアプローチです。
小規模なプロジェクトや、学習目的、あるいはパフォーマンスが重視される特定のユースケースでは、この新しいアプローチが魅力的な選択肢となるでしょう。
一方、大規模なエンタープライズアプリケーションや、豊富なエコシステムが必要なプロジェクトでは、React、Vue、Angularの成熟度と実績が大きなアドバンテージとなります。
明日はいよいよ最後です。