Vite との統合

今どきの JavaScript のアプリケーションには洗練されたビルドの道具が要ります。Nette Assets は、次の世代のフロントエンドのビルドの道具 Viteとの一流の組み合わせを備えています。Hot Module Replacement(HMR)による稲妻のように速い開発と、設定の面倒がまったくない最適化された本番のビルドが手に入ります。

  • 設定なし – Vite と PHP のテンプレートを自動的につなぎます
  • 依存関係をまるごと管理 – ひとつのタグがすべてのアセットを受け持ちます
  • Hot Module Replacement – JavaScript と CSS がすぐに更新されます
  • 最適化された本番のビルド – コードの分割と使わないコードの除去

Nette Assets は Vite となめらかに組み合わさるので、テンプレートをいつもどおり書きながら、これらの恩恵をすべて受けられます。

Vite を用意する

Vite を一歩ずつ用意しましょう。ビルドの道具がはじめてでも心配は要りません。すべて説明します。

第 1 歩: Vite を入れる

まず Vite と Nette のプラグインをプロジェクトに入れます。

npm install -D vite @nette/vite-plugin

これで Vite と、Vite が Nette と見事に働くのを助ける特別なプラグインが入ります。

第 2 歩: プロジェクトの構造

標準のやり方は、もとのアセットのファイルをプロジェクトの根の assets/ のフォルダに置き、ビルドされたものを www/assets/ に置くことです。

web-project/
├── assets/                   ← もとのファイル(SCSS、TypeScript、もとの画像)
│   ├── public/               ← 静的ファイル(そのままコピーされます)
│   │   └── favicon.ico
│   ├── images/
│   │   └── logo.png
│   ├── app.js                ← 主な入口
│   └── style.css             ← あなたのスタイル
└── www/                      ← 公開のディレクトリ(document root)
	├── assets/               ← ビルドされたファイルがここに置かれます
	└── index.php

assets/ のフォルダには、あなたが書くもとのファイルが入ります。Vite はそれらを処理し、ビルドされたものを www/assets/ に置きます。

第 3 歩: Vite を設定する

プロジェクトの根に vite.config.ts のファイルを作ります。このファイルは、もとのファイルがどこにあり、ビルドされたものをどこへ置くかを Vite に伝えます。

Nette の Vite のプラグインには賢い既定値が備わっていて、設定が簡単になります。フロントエンドのもとのファイルは assets/ のディレクトリにあり(root のオプション)、ビルドされたファイルは www/assets/ へ行く(outDir のオプション)ものと考えます。あなたが指定する必要があるのは入口だけです。

import { defineConfig } from 'vite';
import nette from '@nette/vite-plugin';

export default defineConfig({
	plugins: [
		nette({
			entry: 'app.js',
		}),
	],
});

内側では、rootoutDir のほかにもいくつかの Vite のオプションをプラグインが設定して、すべてがかみ合うようにします。base'' に(アセットは document root から直接配られます)、build.manifesttrue に(Nette Assets がハッシュの付いたファイル名を対応づけられるように)、build.assetsDir'' に(ビルドされたファイルは static/ の下位のフォルダなしに outDir へ直接置かれます)します。どれも上書きできます。

既定の outDirwww/assets)には、www/ のディレクトリがすでにあることが要ります。なければプラグインは “The output directory … does not exist” のエラーで止まります。

アセットをビルドするディレクトリに別の名前を使いたいなら、いくつかのオプションを変える必要があります。

export default defineConfig({
	root: 'assets', // もとのアセットの根のディレクトリ

	build: {
		outDir: '../www/assets',  // ビルドされたファイルの置き場所
	},

	// ... そのほかの設定 ...
});

outDir のパスは root からの相対と見なされます。だから先頭に ../ があるのです。

第 4 歩: Nette を設定する

common.neon で Nette Assets に Vite のことを伝えます。

assets:
	mapping:
		default:
			type: vite      # ViteMapper を使うよう Nette に伝えます
			path: assets

第 5 歩: スクリプトを足す

package.json に次のスクリプトを足します。

{
	"scripts": {
		"dev": "vite",
		"build": "vite build"
	}
}

これで次のことができます。

  • npm run dev – ホットリロードの付いた開発サーバーを立ち上げます
  • npm run build – 最適化された本番のファイルを作ります

入口

入口 は、あなたのアプリケーションが始まる主なファイルです。このファイルからほかのファイル(CSS、JavaScript のモジュール、画像)を import して、依存関係の木を作ります。Vite はこれらの import をたどり、すべてをひとつにまとめます。

入口 assets/app.js の例です。

// スタイルを import します
import './style.css'

// JavaScript のモジュールを import します
import netteForms from 'nette-forms';
import naja from 'naja';

// アプリケーションを立ち上げます
netteForms.initOnLoad();
naja.initialize();

テンプレートでは、入口を次のように差し込めます。

{asset 'app.js'}

Nette Assets は必要なすべての HTML のタグを自動的に作ります。JavaScript、CSS、そのほかの依存関係もです。

複数の入口

大きめのアプリケーションでは、別々の入口が要ることがよくあります。

export default defineConfig({
	plugins: [
		nette({
			entry: [
				'app.js',      // 公開のページ
				'admin.js',    // 管理画面
			],
		}),
	],
});

それらを違うテンプレートで使います。

{* 公開のページで *}
{asset 'app.js'}

{* 管理画面で *}
{asset 'admin.js'}

大事な点: もとのファイルとビルドされたファイル

本番では、Vite が使えるようにしたファイルしか読み込めないことを分かっておくのが肝心です。それはマニフェスト経由か、public のフォルダからそのままコピーされたものです。

  1. entry で定めた入口(それが動的に import するモジュールも含みます)と、JavaScript や CSS から参照されるアセット(画像、フォントなど)。これらはすべてマニフェストに記録されます
  2. assets/public/ のディレクトリのファイル。これらはマニフェストには ありません。そのままコピーされ、{asset} はファイルシステムの代わりの手立てでそれを見つけます

assets/ の中の好きなファイルを {asset} で読み込むことはできません。どこからも参照されていないファイルはビルドされないからです。ほかのアセットを Vite に知らせたいなら、public のフォルダへ移せます。

既定では、Vite は 4KB より小さいアセットをすべて埋め込むので、それらのファイルを直接参照できないことに注意してください(Vite のドキュメントをご覧ください)。

{* ✓ これは働きます - 入口だからです *}
{asset 'app.js'}

{* ✓ これは働きます - assets/public/ にあるからです *}
{asset 'favicon.ico'}

{* ✗ これは働きません - assets/ の中のただのファイルです *}
{asset 'components/button.js'}

開発モード

開発モードは完全に省略できますが、使うと大きな恩恵があります。いちばんの利点は Hot Module Replacement(HMR) で、アプリケーションの状態を失わずに変更がすぐ見えるので、開発の体験がずっとなめらかで速くなります。

Vite は今どきのビルドの道具で、開発を信じられないほど速くします。昔ながらのバンドラと違い、Vite は開発中コードをブラウザへ直接配るので、プロジェクトがどれだけ大きくてもサーバーはすぐ立ち上がり、更新は稲妻のように速くなります。

開発サーバーを立ち上げる

開発サーバーを走らせます。

npm run dev

次のように出ます。

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

開発しているあいだ、この端末は開いたままにしてください。

開発サーバーが動いているあいだ、プラグインはその URL の入った小さな合図のファイル www/assets/.vite/nette.json を書きます。PHP 側の Nette Assets はこのファイルを読み、次の 2 つがそろったときに開発サーバーからの読み込みへ切り替えます。

  1. Vite の開発サーバーが動いている(合図のファイルがある)こと、そして
  2. Nette のアプリケーションがデバッグモードであること。

結果は次のとおりです。

{asset 'app.js'}
{* 開発中: <script src="http://localhost:5173/@vite/client" type="module"></script>
                   <script src="http://localhost:5173/app.js" type="module"></script> *}
{* 本番: <script src="/assets/app-4f3a2b1c.js" type="module" crossorigin></script> *}

設定は要りません。ただ動きます。この判別を切ったり、開発サーバーの URL を手で決めたりするには、devServer のオプションをご覧ください。

合図のファイルは www/assets/.vite/nette.json にあります(本番の manifest.json のすぐ隣です)。プラグインの infoFile のオプションで名前を変えられ、既定は .vite/nette.json です。Nette が動いている開発サーバーに気づかないなら、このファイルがあって正しい URL を指しているかを確かめてください。

違うドメインで作業する

開発サーバーが localhost 以外(たとえば myapp.local)で動いているなら、CORS(Cross-Origin Resource Sharing)の問題に出くわすかもしれません。CORS はウェブブラウザの安全の機能で、違うドメインどうしのリクエストを既定で遮ります。PHP のアプリケーションが myapp.local で動き、Vite が localhost:5173 で動いていると、ブラウザはこれらを違うドメインと見なしてリクエストを遮ります。

解決の方法は 2 つあります。

方法 1: CORS を設定する

いちばん単純な解は、PHP のアプリケーションからの別オリジンのリクエストを許すことです。

export default defineConfig({
	// ... そのほかの設定 ...

	server: {
		cors: {
			origin: 'http://myapp.local',  // あなたの PHP のアプリケーションの URL
		},
	},
});

方法 2: Vite を同じドメインで動かす

もうひとつの解は、Vite を PHP のアプリケーションと同じドメインで動かすことです。

export default defineConfig({
	// ... そのほかの設定 ...

	server: {
		host: 'myapp.local',  // PHP のアプリケーションと同じ
	},
});

実のところこの場合も CORS の設定が要ります。開発サーバーは同じホスト名でも違うポートで動くからです。とはいえこの場合、CORS は Nette の Vite のプラグインが自動的に設定します。

HTTPS での開発

HTTPS で開発しているなら、Vite の開発サーバーに証明書が要ります。いちばん簡単なのは、証明書を自動的に作るプラグインを使うことです。

npm install -D vite-plugin-mkcert

vite.config.ts での設定の仕方です。

import mkcert from 'vite-plugin-mkcert';

export default defineConfig({
	// ... そのほかの設定 ...

	plugins: [
		mkcert(),  // 証明書を自動的に作り、https を有効にします
		nette(),
	],
});

CORS の設定(上の方法 1)を使っているなら、origin の URL を http:// ではなく https:// に更新する必要があることに注意してください。

Docker での開発

Vite を Docker のコンテナの中で走らせるときは、2 つのことに気を配る必要があります。あなたの機械のブラウザが開発サーバーに届けること、そして Vite がコンテナの境をまたいでファイルの変更に気づけることです。

まず、コンテナから Vite のポートを 公開し、開発サーバーをすべてのインターフェースに結び付けて、コンテナの外から届くようにします。

export default defineConfig({
	// ... そのほかの設定 ...

	plugins: [
		nette(),
	],
	server: {
		host: '0.0.0.0',      // すべてのインターフェースで待ち受けます(コンテナでは必須)
		port: 5173,           // 公開したポートと合わせなければなりません
		strictPort: true,     // 別のポートを選ばず失敗させます
		watch: {
			usePolling: true, // マウントしたボリュームでファイルの変更に気づかないなら有効にします
		},
	},
});

プラグインは開発サーバーの URL を PHP 側のために nette.json へ書きます。host: '0.0.0.0' はブラウザからは使えない(アセットごとに localhost へリダイレクトされてしまう)ので、プラグインはその URL の中でそれを自動的に localhost へ書き換え、アセットが正しく読み込まれるようにします。

localhost ではなく独自のドメインでアプリケーションを開くなら、プラグインの host のオプションをそのドメインにしてください。

	plugins: [
		nette({ host: 'myapp.local' }),  // PHP のアプリケーションと同じドメイン
	],

そうするとプラグインはそのホストを開発サーバーの URL に使い、CORS の origin に足し、Vite の allowedHosts にも入れるので、CORS を手で設定しなくても働きます。

もっと込み入った構えでは、たとえば Vite がリバースプロキシの後ろで動いていて、公のホスト、ポート、プロトコルがどれも内側のアドレスと違う場合、Vite の server.origin を公の URL 全体に設定してください。プラグインはそれを尊重し、手元のソケットから URL を導く代わりに、そのまま nette.json へ書きます。

	server: {
		origin: 'https://myapp.local:8443',  // ブラウザが Vite に届く公の URL
	},

本番のビルド

最適化された本番のファイルを作ります。

npm run build

Vite は次のことをします。

  • すべての JavaScript と CSS を最小化します
  • コードを最適なかたまりに分けます
  • キャッシュを無効にするためにハッシュの付いたファイル名を作ります
  • Nette Assets のためにマニフェストのファイルを作ります

出力の例です。

www/assets/
├── app-4f3a2b1c.js       # あなたの主な JavaScript(最小化済み)
├── app-7d8e9f2a.css      # 抜き出された CSS(最小化済み)
├── vendor-8c4b5e6d.js    # 共有の依存関係
└── .vite/
	└── manifest.json     # Nette Assets のための対応づけ

ハッシュの付いたファイル名のおかげで、ブラウザはいつも最新の版を読み込みます。

public のフォルダ

assets/public/ のディレクトリのファイルは、処理されずに出力先へコピーされます。

assets/
├── public/
│   ├── favicon.ico
│   ├── robots.txt
│   └── images/
│       └── og-image.jpg
├── app.js
└── style.css

ふつうに参照します。

{* これらのファイルはそのままコピーされます *}
<link rel="icon" href={asset 'favicon.ico'}>
<meta property="og:image" content={asset 'images/og-image.jpg'}>

public のファイルでは FilesystemMapper の機能を使えます。extension のオプションは拡張子のない参照に当たります(たとえば {asset 'images/og-image'} はまず og-image.webp を見つけます)。

assets:
	mapping:
		default:
			type: vite
			path: assets
			extension: [webp, jpg, png]  # 拡張子のない参照のため
			versioning: true             # キャッシュを無効にする値を足します

vite.config.ts の設定では、publicDir のオプションで public のフォルダを変えられます。

動的な import

Vite は最適な読み込みのためにコードを自動的に分けます。動的な import を使うと、本当に必要になったときにだけコードを読み込めるので、最初のかたまりの大きさを減らせます。

// 重い部品を必要になったときに読み込みます
button.addEventListener('click', async () => {
	let { Chart } = await import('./components/chart.js')
	new Chart(data)
})

動的な import は、必要になったときにだけ読み込まれる別々のかたまりを作ります。これは「コードの分割」と呼ばれ、Vite のもっとも強力な機能のひとつです。動的な import を使うと、Vite は動的に import されるモジュールごとに別々の JavaScript のファイルを自動的に作ります。

{asset 'app.js'} のタグは、これらの動的なかたまりを自動的に先読みしません。これは意図した振る舞いです。使われないかもしれないコードをダウンロードしたくないからです。かたまりは、動的な import が実行されたときにだけダウンロードされます。

とはいえ、ある動的な import が肝心で、すぐ必要になると分かっているなら、先読みできます。

{* 主な入口 *}
{asset 'app.js'}

{* 肝心な動的な import を先読みします *}
{preload 'components/chart.js'}

これでブラウザは chart の部品を裏でダウンロードするので、必要になったときにはすぐ使えます。

TypeScript への対応

TypeScript はそのまま動きます。

// assets/main.ts
interface User {
	name: string
	email: string
}

export function greetUser(user: User): void {
	console.log(`Hello, ${user.name}!`)
}

TypeScript のファイルをふつうに参照します(どのファイルとも同じく、main.ts は入口でなければなりません)。

{asset 'main.ts'}

TypeScript に完全に対応させるには、それを入れます。

npm install -D typescript

そのほかの Vite の設定

役に立つ Vite の設定オプションを、詳しい説明とともにいくつか挙げます。

export default defineConfig({
	// もとのアセットの入った根のディレクトリ
	root: 'assets',

	// 中身が出力先のディレクトリへそのままコピーされるフォルダ
	// 既定: 'public'('root' からの相対)
	publicDir: 'public',

	build: {
		// ビルドされたファイルの置き場所('root' からの相対)
		outDir: '../www/assets',

		// ビルドの前に出力先のディレクトリを空にしますか
		// 前のビルドの古いファイルを取り除くのに役立ちます
		emptyOutDir: true,

		// 作られるかたまりとアセットのための outDir の中の下位のディレクトリ
		// 出力の構造を整えるのに役立ちます
		assetsDir: 'static',

		rollupOptions: {
			// 入口。ひとつのファイルでもファイルの配列でもかまいません
			// それぞれの入口が別々のかたまりになります
			input: [
				'app.js',      // 主なアプリケーション
				'admin.js',    // 管理画面
			],
		},
	},

	server: {
		// 開発サーバーを結び付けるホスト
		// ネットワークへ公開するには '0.0.0.0' を使います
		host: 'localhost',

		// 開発サーバーのポート
		port: 5173,

		// 別オリジンのリクエストのための CORS の設定
		cors: {
			origin: 'http://myapp.local',
		},
	},

	css: {
		// 開発中に CSS のソースマップを有効にします
		devSourcemap: true,
	},

	plugins: [
		nette(),
	],
});

上の rollupOptions.input の入口のパスには気をつけてください。Rollup は相対パスを今の作業ディレクトリ(あなたのプロジェクトの根)から解決し、root: 'assets' からは解決しません。ですから裸の 'app.js' はビルドのときに存在しません。プラグインの entry のオプション(入口のパスを root からの相対で解決します)を使うか、パスをプロジェクトの根からの相対で、たとえば 'assets/app.js' と書いてください。

これで終わりです。今どきのビルドのしくみが Nette Assets と組み合わさりました。

バージョン: 1.x