公開API
公開APIは、モジュールのグループ(スライスなど)とそれを使用するコードとの間の契約です。また、特定のオブジェクトへのアクセスを制限し、その公開APIを通じてのみアクセスを許可します。
実際には、通常、再エクスポートを伴うインデックスファイルとして実装されます。
export { LoginPage } from "./ui/LoginPage";
export { RegisterPage } from "./ui/RegisterPage";
良い公開APIとは?
良い公開APIは、他のコードとの統合を便利で信頼性の高いものにします。これを達成するためには、以下の3つの目標を設定することが重要です。
- アプリケーションの残りの部分は、スライスの構造的変更(リファクタリングなど)から保護されるべきです。
- スライスの動作における重要な変更が以前の期待を破る場合、公開APIに変更が必要です。
- スライスの必要な部分のみを公開するべきです。
最後の目標には重要な実践的な意味があります。特にスライスの初期開発段階では、すべてをワイルドカードで再エクスポートしたくなるかもしれません。なぜなら、ファイルからエクスポートする新しいオブジェクトは、スライスからも自動的にエクスポートされるからです。
// ❌ これは悪いコードの例です。このようにしないでください。
export * from "./ui/Comment"; // 👎
export * from "./model/comments"; // 💩
これは、スライスの理解可能性を損ないます。インターフェースが理解できないと、スライスのコードを深く掘り下げて統合方法を調べなければいけなくなってしまいます。もう一つの問題は、モジュールの内部を誤って公開してしまう可能性があり、誰かがそれに依存し始めるとリファクタリングが難しくなることです。
クロスインポートのための公開API
クロスインポートは、同じレイヤーの別のスライスからインポートする状況です。通常、これはレイヤーに関するインポートルールによって禁止されていますが、しばしば正当な理由でクロスインポートが必要です。たとえば、ビジネスエンティティは現実世界で互いに参照し合うことが多く、これらの関係をコードに反映させるのが最善です。
この目的のために、@x表記として知られる特別な種類の公開APIがあります。エンティティAとBがあり、エンティティBがエンティティAからインポートする必要がある場合、エンティティAはエンティティB専用の別の公開APIを宣言できます。
📂 entities📂 A📂 @x📄 B.ts— エンティティB内のコード専用の特別な公開API
📄 index.ts— 通常の公開API
その後、entities/B/内のコードはentities/A/@x/Bからインポートできます。
import type { EntityA } from "entities/A/@x/B";
A/@x/Bという表記は「AとBが交差している」と読むことを意図しています。
クロスインポートは最小限に抑え、この表記はエンティティレイヤーでのみ使用してください。クロスインポートを排除することがしばしば非現実的だからです。
インデックスファイルの問題
index.jsのようなインデックスファイル(Barrelファイルとも呼ばれる)は、公開APIを定義する最も一般的な方法です。作成は簡単ですが、特定のバンドラーやフレームワークで問題を引き起こすことがあります。
循環インポート
循環インポートとは、2つ以上のファイルが互いに循環的にインポートすることです。
上の図には、fileA.js、fileB.js、fileC.jsの三つのファイルが循環的にインポートしている様子が示されています。
これらの状況は、バンドラーにとって扱いが難しく、場合によってはデバッグが難しいランタイムエラーを引き起こすことさえあります。
循環インポートはインデックスファイルなしでも発生する可能性がありますが、インデックスファイルがあると、循環インポートを誤って作成する明確な機会が生まれます。これは、公開APIのスライスに2つのオブジェクト(例えば、HomePageとloadUserStatistics)が存在し、HomePageがloadUserStatisticsに以下のようにアクセスすると循環インポートが発生してしまいます。
import { loadUserStatistics } from "../"; // pages/home/index.jsからインポート
export function HomePage() { /* … */ }
export { HomePage } from "./ui/HomePage";
export { loadUserStatistics } from "./api/loadUserStatistics";
この状況は循環インポートを作成します。なぜなら、index.jsがui/HomePage.jsxをインポートしますが、ui/HomePage.jsxがindex.jsをインポートするからです。
この問題を防ぐために、次の2つの原則を考慮してください。
- ファイルが同じスライス内にある場合は、常に相対インポートを使用し、完全なインポートパスを記述すること
- ファイルが異なるスライスにある場合は、常に絶対インポートを使用すること(エイリアスなどで)
Sharedにおける大きなバンドルサイズと壊れたツリーシェイキング
インデックスファイルがすべてを再エクスポートする場合、いくつかのバンドラーはツリーシェイキング(インポートされていないコードを削除すること)に苦労するかもしれません。
通常、これは公開APIにとって問題ではありません。なぜなら、モジュールの内容は通常非常に密接に関連しているため、1つのものをインポートし、他のものをツリーシェイキングする必要がほとんどないからです。しかし、公開APIのルールが問題を引き起こす非常に一般的なケースが2つあります。それはshared/uiとshared/libです。
これらの2つのフォルダーは、しばしば一度にすべてが必要ではない無関係なもののコレクションです。たとえば、shared/uiにはUIライブラリのすべてのコンポーネントのモジュールが含まれているかもしれません。
📂 shared/ui/📁 button📁 text-field📁 carousel📁 accordion
この問題は、これらのモジュールの1つが重い依存関係(シンタックスハイライトやドラッグ&ドロップライブラリ)を持っている場合、悪化します。ボタンなど、shared/uiから何かを使用するすべてのページにそれらを引き込むことは望ましくありません。
shared/uiやshared/libの単一の公開APIによってバンドルサイズが不適切に増加する場合は、各コンポーネントやライブラリのために別々のインデックスファイルを持つことをお勧めします。
📂 shared/ui/📂 button📄 index.js
📂 text-field📄 index.js
その後、これらのコンポーネントの消費者は、次のように直接インポートできます。
import { Button } from '@/shared/ui/button';
import { TextField } from '@/shared/ui/text-field';
公開APIを回避することに対する実質的な保護がない
スライスのためにインデックスファイルを作成しても、誰もそれを使用せず、直接インポートを使用することができます。これは特に自動インポートにおいて問題です。なぜなら、オブジェクトをインポートできる場所がいくつかあるため、IDEがあなたのために決定しなければならないからです。時には、直接インポートを選択し、スライスの公開APIルールを破ることがあります。
これらの問題を自動的にキャッチするために、Steigerを使用することをお勧めします。これは、Feature-Sliced Designのルールセットを持つアーキテクチャリンターです。
大規模プロジェクトにおけるバンドラーのパフォーマンスの低下
プロジェクト内に大量のインデックスファイルがあると、開発サーバーが遅くなる可能性があります。これは、TkDodoが「Barrelファイルの使用をやめてください」という記事で指摘しています。
この問題に対処するためにできることはいくつかあります。
- 「Sharedにおける大きなバンドルサイズと壊れたツリーシェイキング」のセクションと同じアドバイス —
shared/uiやshared/libの各コンポーネントやライブラリのために別々のインデックスファイルを持つこと。 - スライスを持つレイヤーのセグメントにインデックスファイルを持たないようにすること。
たとえば、「コメント」フィーチャーのインデックス(📄 features/comments/index.js)がある場合、そのフィーチャーのuiセグメントのために別のインデックスを持つ理由はありません(📄 features/comments/ui/index.js)。 - 非常に大きなプロジェクトがある場合、アプリケーションをいくつかの大きなチャンクに分割できる可能性が高いです。
たとえば、Google Docsは、ドキュメントエディターとファイルブラウザに非常に異なる責任を持っています。各パッケージが独自のレイヤーセットを持つ別々のFSDルートとしてモノレポを作成できます。いくつかのパッケージは、SharedとEntitiesレイヤーのみを持つことができ、他のパッケージはPagesとAppのみを持つことができ、他のパッケージは独自の小さなSharedを含むことができますが、他のパッケージからの大きなSharedも使用できます。