この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの15日目です。
Structiveについて詳しくはこちらより
Week 3 のスタート
Week 1と2で、構造パスの基礎とコア機能を学びました。Week 3では、アプリケーションをスケールさせるためのコンポーネント化について学びます。今日は、その基盤となるSFC(シングルファイルコンポーネント)の設計思想を見ていきます。
SFCとは?
**SFC(Single File Component)**は、1つのファイルにコンポーネントのすべてを記述する形式です。
基本構造
<template>
<!-- HTML: UIの構造 -->
</template>
<style>
/* CSS: スタイル */
</style>
<script type="module">
// JavaScript: ロジックと状態
export default class {
// 状態とメソッド
}
</script>
3つのセクション:
-
<template>: UIの構造(HTML) -
<style>: スタイル(CSS) -
<script>: ロジックと状態(JavaScript)
なぜSFCなのか?
問題:分散したコード
従来の開発では、1つの機能が複数のファイルに分散します:
components/
ProductCard.html # HTML
ProductCard.css # CSS
ProductCard.js # JavaScript
問題点:
- ファイル間を行き来する必要がある
- 関連コードが見つけにくい
- コピー&ペーストでファイルを揃える手間
- リネーム時に複数ファイルを変更
解決:SFCによる集約
components/
ProductCard.st.html # すべてが1つに
利点:
- 関連コードが一箇所に
- ファイル間の移動不要
- コンポーネント単位で管理
- リネームが簡単
SFCの実例
シンプルなボタンコンポーネント
<template>
<button
data-bind="
onclick:handleClick;
class.primary:isPrimary;
disabled:isDisabled
"
>
{{ label }}
</button>
</template>
<style>
button {
padding: 0.5em 1em;
border: none;
border-radius: 4px;
font-size: 1em;
cursor: pointer;
background: #ccc;
color: #333;
}
button.primary {
background: #007bff;
color: white;
}
button:hover:not(:disabled) {
opacity: 0.9;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<script type="module">
export default class {
label = "Click me";
isPrimary = true;
isDisabled = false;
handleClick() {
console.log("Button clicked!");
}
}
</script>
このコンポーネントの特徴:
- 自己完結している
- UIとロジックが明確に分離
- スタイルが閉じ込められている
- 再利用可能
関心の分離(Separation of Concerns)
SFCは「関心の分離」の原則に従っています。
1. template: 構造の関心
<template>
<div class="product-card">
<img data-bind="src:product.imageUrl">
<h3>{{ product.name }}</h3>
<p>${{ product.price|fix,2 }}</p>
<button data-bind="onclick:addToCart">カートに追加</button>
</div>
</template>
役割:
- UIの構造を定義
- 何を表示するか
- どのように配置するか
2. style: 見た目の関心
<style>
.product-card {
border: 1px solid #ccc;
padding: 1em;
border-radius: 8px;
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.product-card h3 {
margin: 0.5em 0;
}
</style>
役割:
- 見た目を定義
- レイアウト
- 色やフォント
3. script: ロジックの関心
<script type="module">
export default class {
product = {
name: "Laptop",
price: 999,
imageUrl: "/images/laptop.jpg"
};
addToCart() {
console.log("Added to cart:", this.product);
}
}
</script>
役割:
- 状態の管理
- ビジネスロジック
- イベント処理
カプセル化の実現
SFCは、コンポーネントをカプセル化します。
スタイルのスコープ
このフレームワークでは、Web ComponentsのShadow DOMを使ってスタイルをカプセル化できます。
Shadow DOMなし:
<!-- コンポーネントA -->
<style>
.title { color: red; }
</style>
<!-- コンポーネントB -->
<style>
.title { color: blue; } /* コンフリクト! */
</style>
Shadow DOMあり:
<!-- コンポーネントAのスタイルは外部に漏れない -->
<!-- コンポーネントBのスタイルも独立している -->
Shadow DOMの制御
<!-- Shadow DOMを使う(デフォルト) -->
<script type="module" src="components.js"></script>
<!-- Shadow DOMを使わない -->
<script type="module" src="components--shadow-dom-mode-none.js.js"></script>
使い分け:
- Shadow DOM ON: コンポーネントを完全に独立させたい場合
- Shadow DOM OFF: グローバルなスタイルを適用したい場合
実践例:商品カードコンポーネント
実用的なコンポーネントを作ってみましょう。
product-card.st.html
<template>
<div class="product-card">
<div class="image-container">
<img data-bind="src:product.imageUrl" alt="商品画像">
{{ if:product.isOnSale }}
<div class="sale-badge">SALE</div>
{{ endif: }}
{{ if:product.stock|lt,5 }}
<div class="stock-badge low">残り{{ product.stock }}点</div>
{{ endif: }}
</div>
<div class="content">
<h3 class="name">{{ product.name }}</h3>
<p class="description">{{ product.description|slice,0,100 }}...</p>
<div class="price-section">
{{ if:product.isOnSale }}
<span class="original-price">${{ product.originalPrice|fix,2 }}</span>
<span class="sale-price">${{ product.price|fix,2 }}</span>
<span class="discount">{{ product.discountRate|percent }}%OFF</span>
{{ else: }}
<span class="price">${{ product.price|fix,2 }}</span>
{{ endif: }}
</div>
<div class="actions">
{{ if:product.isAvailable }}
<button
class="btn-primary"
data-bind="onclick:addToCart"
>
カートに追加
</button>
{{ else: }}
<button class="btn-secondary" disabled>
在庫切れ
</button>
{{ endif: }}
<button
class="btn-icon"
data-bind="onclick:toggleFavorite"
>
{{ favoriteIcon }}
</button>
</div>
</div>
</div>
</template>
<style>
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
}
.product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.image-container {
position: relative;
width: 100%;
height: 250px;
background: #f5f5f5;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
.sale-badge {
position: absolute;
top: 10px;
right: 10px;
background: #e74c3c;
color: white;
padding: 0.3em 0.6em;
border-radius: 4px;
font-weight: bold;
font-size: 0.9em;
}
.stock-badge {
position: absolute;
bottom: 10px;
left: 10px;
background: #f39c12;
color: white;
padding: 0.3em 0.6em;
border-radius: 4px;
font-size: 0.85em;
}
.content {
padding: 1em;
}
.name {
margin: 0 0 0.5em 0;
font-size: 1.2em;
}
.description {
color: #666;
font-size: 0.9em;
line-height: 1.4;
margin: 0 0 1em 0;
}
.price-section {
margin-bottom: 1em;
display: flex;
align-items: center;
gap: 0.5em;
}
.original-price {
text-decoration: line-through;
color: #999;
}
.sale-price {
font-size: 1.5em;
font-weight: bold;
color: #e74c3c;
}
.price {
font-size: 1.5em;
font-weight: bold;
color: #333;
}
.discount {
background: #e74c3c;
color: white;
padding: 0.2em 0.5em;
border-radius: 4px;
font-size: 0.85em;
font-weight: bold;
}
.actions {
display: flex;
gap: 0.5em;
}
.btn-primary {
flex: 1;
padding: 0.75em;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-secondary {
flex: 1;
padding: 0.75em;
background: #ccc;
color: #666;
border: none;
border-radius: 4px;
cursor: not-allowed;
}
.btn-icon {
width: 40px;
padding: 0.75em;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
.btn-icon:hover {
background: #f5f5f5;
}
</style>
<script type="module">
export default class {
product = {
name: "Premium Laptop",
description: "High-performance laptop with 16GB RAM and 512GB SSD. Perfect for professional work and gaming.",
price: 899.99,
originalPrice: 999.99,
imageUrl: "/images/laptop.jpg",
stock: 3,
isOnSale: true,
isAvailable: true
};
isFavorite = false;
get "product.discountRate"() {
if (!this["product.isOnSale"]) return 0;
const original = this["product.originalPrice"];
const current = this["product.price"];
return (original - current) / original;
}
get favoriteIcon() {
return this.isFavorite ? "❤️" : "🤍";
}
addToCart() {
console.log("Added to cart:", this.product.name);
// カート追加ロジック
}
toggleFavorite() {
this.isFavorite = !this.isFavorite;
}
}
</script>
使用方法
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Product Card Demo</title>
<script type="importmap">
{
"imports": {
"@components/product-card": "./product-card.st.html"
}
}
</script>
<script type="module" src="components.js"></script>
</head>
<body>
<product-card></product-card>
</body>
</html>
SFCの利点まとめ
1. 保守性の向上
従来:
- 3つのファイルを開く
- どれがどれか確認
- 同期を保つ
SFC:
- 1つのファイルですべて完結
- スクロールするだけ
- 常に同期
2. 再利用性
<!-- 同じコンポーネントを何度でも -->
<product-card></product-card>
<product-card></product-card>
<product-card></product-card>
各インスタンスは独立した状態を持ちます。
3. 理解しやすさ
新しいメンバーがコードを見たとき:
- ファイルを開く → 全体像がわかる
- template → 何を表示しているか
- style → どう見えるか
- script → どう動くか
4. バージョン管理
Gitでの差分が明確:
<template>
<div class="product-card">
- <h3>{{ name }}</h3>
+ <h3>{{ product.name }}</h3>
</div>
</template>
1つのファイルなので、変更が追いやすい。
SFCのベストプラクティス
1. 適切なサイズ
目安:
- 200行以下: 理想的
- 200-500行: 許容範囲
- 500行以上: 分割を検討
2. 単一責任の原則
1つのコンポーネントは1つの責任だけを持つ:
❌ UserProfileAndSettings.st.html (2つの責任)
✅ UserProfile.st.html (プロフィール表示)
✅ UserSettings.st.html (設定編集)
3. 意味のある命名
❌ Component1.st.html
❌ MyComp.st.html
❌ Thing.st.html
✅ ProductCard.st.html
✅ UserProfile.st.html
✅ ShoppingCart.st.html
4. プロップスのドキュメント
/**
* 商品カードコンポーネント
*
* 必須の状態:
* - product.name (string): 商品名
* - product.price (number): 価格
* - product.imageUrl (string): 画像URL
*/
export default class {
product = {
name: "",
price: 0,
imageUrl: ""
};
}
まとめ
今日は、SFC(シングルファイルコンポーネント)の設計思想を学びました:
SFCとは:
- template, style, scriptを1ファイルに記述
- コンポーネントの自己完結性
- Web Componentsとして実装
なぜSFCか:
- 関心の分離
- カプセル化
- 再利用性
- 保守性
設計原則:
- 適切なサイズ(200行以下が理想)
- 単一責任
- 意味のある命名
- ドキュメント化
利点:
- コードの集約
- 理解しやすさ
- テストしやすさ
- バージョン管理の明確さ
次回予告:
明日は、「Web Componentsとの統合」として、SFCがどのようにWeb標準のカスタム要素として実装されるか、ライフサイクルやイベントについて詳しく学びます。
次回: Day 16「Web Componentsとの統合」
SFCについて質問があれば、コメントでぜひ!