こんにちは!日々のフロントエンド開発、本当にお疲れ様です。
突然ですが、こんな経験はありませんか?
「バックエンドのAPIスキーマ(JSONやGraphQLなど)が更新されるたびに、手動でTypeScriptの型定義ファイルを書き直している……」
「環境変数やビルド日時、Gitのコミットハッシュをアプリ内に埋め込みたいだけなのに、わざわざビルドスクリプトで `fs.writeFileSync` を使って一時的な `.ts` ファイルを生成し、終わったら削除するなんて汚いハックをやっている……」
もし心当たりがあるなら、今回の記事はあなたのためのものです。
今回は、次世代の超高速ビルドツール Vite が持つ隠し持った最強の武器、「仮想モジュール(Virtual Modules)」の世界へご案内します。これをマスターすれば、実ファイルを出汚すことなく、ビルドパイプラインのメモリ上でコードを動的に捏ね上げ、完全に型安全なアプリを構築できるようになります。
毎日の退屈なボイラープレート地獄から抜け出し、ワンランク上のアーキテクトへステップアップしましょう!
—
1. そもそも「仮想モジュール」とは何か?
通常のモジュールバンドラ(Webpackや古いViteの挙動など)は、ディスク上(HDDやSSD)に存在するファイルを起点にして依存関係グラフを構築します。つまり、何らかの設定やデータをコードに注入したいときは、どうしても一度ファイルシステム上に `.js` や `.ts` ファイルを書き出す必要がありました。
しかし、Vite(その裏で動くRollupのプラグイン機構)には、「実体が存在しないファイル(仮想モジュール)」をまるでそこにあるかのように錯覚させ、JavaScriptのコードをその場でオンデマンドに生成して流し込む仕組みが備わっています。
仮想モジュールのメカニズム
1. コード内で `import { config } from ‘virtual:my-app-config’` のように、特定のプレフィックス(通常は `virtual:` や `\0virtual:`)がついたモジュールをインポートします。
2. Viteのプラグインがそのリクエストをインターセプト(横取り)します。
3. プラグインはディスクを探しに行く代わりに、メモリ上で動的に生成した文字列(TypeScript/JavaScriptコード)をViteに返却します。
4. ブラウザ(またはビルド成果物)からは、あたかもそこにリアルなファイルが存在していたかのように綺麗に解釈されます。
ディスクI/Oが発生しないため爆速であり、不要な一時ファイルでGitのワーキングツリーを汚す心配もゼロになります。
—
2. 実践:動的APIスキーマを型安全にインジェクトするプラグインを作ろう
今回は、「バックエンドから取得したJSON形式のAPI定義(エンドポイント一覧)」をビルド時に読み込み、完全な型安全を持った状態でフロントエンドのコードからインポートできるカスタムプラグインを実装してみましょう。
これをマスターすれば、APIの変更が即座にフロントエンドのコンパイルエラーとして検知できるようになります。
プロジェクトの準備
まずは、最小限のVite環境を用意します。ターミナルで以下のコマンドを実行してください。
プロジェクトディレクトリの作成と移動
mkdir vite-virtual-demo && cd vite-virtual-demo
最小限のnpm初期化
npm init -y
Viteのインストール(開発依存)
npm install -D vite typescript
ステップ1: 仮想モジュールのデータソースとなるAPI定義を用意する
プロジェクトのルートに、バックエンドから共有されたという想定の `api-schema.json` を作成します。
{
“getUser”: {
“path”: “/api/v1/users”,
“method”: “GET”,
“params”: { “id”: “string” }
},
“updatePost”: {
“path”: “/api/v1/posts”,
“method”: “POST”,
“params”: { “title”: “string”, “content”: “string” }
}
}
ステップ2: 魔法をかけるViteプラグインを書く
ルートディレクトリに `vite-plugin-api-injector.ts` を作成します。ここが今回の心臓部です。
import type { Plugin } from ‘vite’;
import fs from ‘node:fs’;
import path from ‘node:path’;
export function apiInjectorPlugin(): Plugin {
// 仮想モジュールを識別するための固有ID
const moduleId = ‘virtual:api-client’;
const resolvedModuleId = ‘\0’ + moduleId; // Rollupの規約に則り、解決済みIDには \0 を付与
return {
name: ‘vite-plugin-api-injector’,
// 1. インポートされたモジュール名が一致するか解決する
resolveId(id) {
if (id === moduleId) {
return resolvedModuleId;
}
},
// 2. 一致した場合、メモリ上でコードを動的に生成して返す
load(id) {
if (id === resolvedModuleId) {
// api-schema.jsonを同期的に読み込む(ビルド時なので同期処理でOK)
const schemaPath = path.resolve(__dirname, ‘api-schema.json’);
// Viteのファイル監視(HMR)にこのJSONを含めるため、依存関係に追加
this.addWatchFile(schemaPath);
const schemaContent = fs.readFileSync(schemaPath, ‘utf-8’);
const schema = JSON.parse(schemaContent);
// ここでフロントエンドに注入するTypeScriptコードを文字列として組み立てる
// 型定義と、実際のフェッチヘルパーを動的に生成しています
return `
// — 自動生成されたコード (Virtual Module) —
const schema = ${schemaContent} as const;
export function callApi(actionKey, params) {
const apiDef = schema[actionKey];
if (!apiDef) {
throw new Error(\`API action \${actionKey} does not exist.\`);
}
console.log(\`[\${apiDef.method}] Request to \${apiDef.path} with:\`, params);
// 実際のfetch処理などをここに記述
return Promise.resolve({ success: true, data: params });
}
export type ApiSchema = typeof schema;
`;
}
}
};
}
ステップ3: Viteの設定ファイルにプラグインを組み込む
`vite.config.ts` を作成し、先ほど作成したプラグインを登録します。
import { defineConfig } from ‘vite’;
import { apiInjectorPlugin } from ‘./vite-plugin-api-injector’;
export default defineConfig({
plugins: [
// 自作の仮想モジュールプラグインをViteのパイプラインに組み込む
apiInjectorPlugin(),
],
});
—
3. 型安全なHelloWorld:動作確認とIDEの補完を体験する
それでは、実際にこの仮想モジュールをアプリケーション側から利用してみましょう!
ステップ1: TypeScriptのための型宣言(Ambient Modules)
仮想モジュールは実体がないため、そのままではTypeScriptが「そんなモジュール知らん!」と怒ってしまいます。ルートに `env.d.ts` を作成し、TypeScriptへその存在を教えてあげます。
// virtual:api-client というモジュールが存在することをTSに伝える
declare module ‘virtual:api-client’ {
export const callApi:
actionKey: T,
// スキーマから自動的にパラメータの型を推論させる神業
params: Record
) => Promise<{ success: boolean; data: any }>;
export type ApiSchema = {
[key: string]: {
path: string;
method: string;
params: Record
};
};
}
ステップ2: エントリーポイントの作成
プロジェクトのルートに `index.html` と `main.ts` を作成します。
`index.html`
`main.ts`
// 仮想モジュールから関数をインポート!
// ディスク上にはこの名前のファイルは一切存在しません。
import { callApi } from ‘virtual:api-client’;
async function bootstrap() {
const app = document.querySelector
app.innerHTML = `
Vite Virtual Modules Demo
コンソールを確認してください。
`;
document.querySelector(‘#btn’)?.addEventListener(‘click’, async () => {
// ここでIDEの強力な補完が効き、キー名や引数が型安全に保たれます!
const res = await callApi(‘getUser’, { id: ‘123’ });
console.log(‘APIレスポンス:’, res);
});
}
bootstrap();
ステップ3: 開発サーバーを起動して確認!
ターミナルで以下のコマンドを実行し、Viteの開発サーバーを立ち上げます。
npx vite
ブラウザで `http://localhost:5173` にアクセスし、ボタンを押してみてください。ブラウザのコンソールに `[GET] Request to /api/v1/users with:` と出力されれば、仮想モジュールが完璧に機能しています!
さらに感動的なのは、`api-schema.json` の内容を書き換えて保存(HMR)した瞬間、ディスクに一時ファイルを書き出すことなく、一瞬でアプリ側へその変更が反映される点です。
—
4. 先輩エンジニアからの実践的なアドバイス
今回紹介した仮想モジュールは、単なる「お遊びの裏技」ではありません。現場のプロダクション環境において、以下のような極めて高い実用性を誇ります。
- ビルド時メタデータの注入:
CI/CD環境の環境変数(`process.env.GIT_COMMIT_HASH`, `process.env.BUILD_TIME`)を `virtual:build-info` としてアプリ内に安全にインジェクトし、デバッグ画面に「現在のバージョン」として表示させる。
- マルチテナント・機能フラグ(Feature Flags)の動的切り替え:
顧客やデプロイ環境(Staging / Production)ごとに異なる機能定義JSONを、ビルド時に仮想モジュール経由でコードへ焼き込み、死活コードを完全にツリーシェイキング(削除)させる。
「ファイルを生成して、それを読み込ませて……」という古いパラダイムから脱却し、「メモリ上でコードをデザインする」というViteの本質的なパワーを手に入れたあなたなら、明日からのフロントエンド設計が何倍も自由で楽しいものになるはずです。
ぜひ、あなたのプロジェクトの面倒なボイラープレート自動化に、この「仮想モジュール」を取り入れてみてください。毎日のコーディングが劇的に楽になりますよ!