この記事は「構造パスで作るシンプルなフロントエンドフレームワーク (Structive)」Advent Calendarの2日目です。
Structiveについて詳しくはこちらより
前回のおさらい
1日目では、従来のフレームワークが抱える課題と、構造パスという新しいアプローチを紹介しました。今日は、その核となる「構造パス」の仕組みを深く掘り下げていきます。
構造パスとは何か?
構造パスは、階層的なデータ構造における特定の値の位置を、文字列で一意に表現したものです。
例えるなら、郵便の住所のようなものです:
- 住所: "東京都渋谷区1-2-3" → 特定の建物を指す
- 構造パス: "user.profile.name" → 特定のデータを指す
基本的な例
const data = {
user: {
profile: {
name: "Alice",
age: 25
},
settings: {
theme: "dark",
notifications: true
}
}
};
// 構造パス → 値
"user.profile.name" // → "Alice"
"user.profile.age" // → 25
"user.settings.theme" // → "dark"
"user.settings.notifications" // → true
ドット(.)で区切られた各部分が、オブジェクトの階層を表しています。
JavaScriptのオブジェクト階層を表現するドット記法とほぼ変わりませんね。
ワイルドカードによる配列の抽象化
ちょっと違うのが、構造パスでは、配列の要素をワイルドカード(*)を使ってパスで表現してしまうところなんです。
配列データの例
const data = {
users: [
{ name: "Alice", age: 25, city: "Tokyo" },
{ name: "Bob", age: 30, city: "Osaka" },
{ name: "Carol", age: 28, city: "Kyoto" }
]
};
ワイルドカード(*)の使い方
*を使うことで、配列の全要素を抽象的に表現できます:
// ワイルドカードを使った構造パス
"users.*.name" // → すべてのユーザーの name を指す
"users.*.age" // → すべてのユーザーの age を指す
"users.*.city" // → すべてのユーザーの city を指す
// 実際には以下のように展開される
"users.0.name" // → "Alice"
"users.1.name" // → "Bob"
"users.2.name" // → "Carol"
"users.0.age" // → 25
"users.1.age" // → 30
"users.2.age" // → 28
"users.0.city" // → "Tokyo"
"users.1.city" // → "Osaka"
"users.2.city" // → "Kyoto"
なぜワイルドカードが重要か?
UIテンプレートでは、配列の各要素に対して同じ操作を繰り返すことがよくあります:
<!-- すべてのユーザーの名前を表示したい -->
<ul>
<li>Alice</li>
<li>Bob</li>
<li>Carol</li>
</ul>
ワイルドカードを使えば、これを抽象的に記述できます:
{{ for:users }}
<li>{{ users.*.name }}</li>
{{ endfor: }}
ポイント: users.*.nameという一つの構造パスで、すべてのユーザーの名前を表現できます。
構造パスの実装イメージ
実際の実装では、構造パスを解析してデータにアクセスします。
基本的な get 関数
function get(obj, path) {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current == null) return undefined;
current = current[key];
}
return current;
}
// 使用例
const data = { user: { profile: { name: "Alice" } } };
console.log(get(data, "user.profile.name")); // "Alice"
ワイルドカードを含む場合
function getAll(obj, path) {
const keys = path.split('.');
let current = [obj];
for (const key of keys) {
if (key === '*') {
// 配列の全要素を展開
current = current.flatMap(item =>
Array.isArray(item) ? item : []
);
} else {
current = current.map(item => item?.[key]);
}
}
return current;
}
// 使用例
const data = {
users: [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 }
]
};
console.log(getAll(data, "users.*.name")); // ["Alice", "Bob"]
UIとの関係
構造パスの本当の威力は、UIと状態を結びつけるときに発揮されます。
同じ構造、同じパス
状態とUIが同じ構造を持つため、同じ構造パスでアクセスできます:
// 状態の定義
class State {
product = {
name: "Laptop",
price: 999
};
}
<!-- UIテンプレート -->
<div>
<h2>{{ product.name }}</h2>
<p>Price: ${{ product.price }}</p>
</div>
ここが重要: UIの{{ product.name }}と状態のproduct.nameは、まったく同じ構造パスです。
なぜこれが革新的なのか?
- 中間層が不要: UIと状態の間に変換層が不要
-
明示的な依存関係:
{{ product.name }}を見れば、このUIがproduct.nameに依存していることが一目瞭然 -
ピンポイントな更新:
product.nameが変わったら、そのパスを使っているUIだけを更新すれば良い
構造パスの記法まとめ
| 構造パス | 説明 | 例 |
|---|---|---|
object.property |
オブジェクトのプロパティ | user.name |
object.nested.property |
ネストしたプロパティ | user.profile.age |
array.*.property |
配列の各要素のプロパティ | users.*.name |
array.* |
配列の各要素 | items.* |
実践例:商品リスト
構造パスを使って、商品リストを表現してみましょう:
// 状態
class State {
products = [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Mouse", price: 29 },
{ id: 3, name: "Keyboard", price: 79 }
];
}
<!-- UI -->
<ul>
{{ for:products }}
<li>
{{ products.*.name }} - ${{ products.*.price }}
</li>
{{ endfor: }}
</ul>
展開されるHTML:
<ul>
<li>Laptop - $999</li>
<li>Mouse - $29</li>
<li>Keyboard - $79</li>
</ul>
まとめ
今日は構造パスの基本を学びました:
構造パスとは:
- データの位置を文字列で表現する「住所」
- ドット記法でネストを表現
- ワイルドカードで配列を抽象化
構造パスの利点:
- データへの統一的なアクセス方法
- 動的なパス構築が可能
- UIと状態を同じパスで表現できる
次回予告:
明日は、「UIと状態を同じ構造で表現する思想」について、より深く掘り下げます。なぜこのアプローチがシンプルさとスケーラビリティを両立できるのか、具体例とともに解説します。
次回: Day 3「UIと状態を同じ構造で表現する思想」
構造パスについて質問があれば、ぜひコメント欄で!