Nette Assets

ウェブアプリケーションの静的ファイルを手で管理するのに疲れましたか。パスを直に書いたり、キャッシュを無効にする手立てを考えたり、ファイルのバージョン付けを気にしたりするのはもう終わりです。Nette Assets は、画像、スタイルシート、スクリプト、そのほかの静的な資源との付き合い方を一変させます。

  • 賢いバージョン付け でブラウザはいつも最新のファイルを読み込みます
  • ファイルの種類と寸法の 自動の判別
  • 直感的なタグによる なめらかな Latte との統合
  • ファイルシステム、CDN、Vite に対応する しなやかな設計
  • 性能を最大にする 遅延の読み込み

なぜ Nette Assets なのか

静的ファイルを扱う仕事は、しばしば繰り返しが多く間違いやすいコードになります。URL を手で組み立て、キャッシュを無効にするバージョンのパラメータを足し、ファイルの種類ごとに違う扱いをします。それはこんなコードにつながります。

<img src="/images/logo.png?v=1699123456" width="200" height="100" alt="Logo">
<link rel="stylesheet" href="/css/style.css?v=2">

Nette Assets なら、この込み入った作業はすべて消えます。

{* すべて自動 - URL、バージョン付け、寸法 *}
<img n:asset="images/logo.png">
<link n:asset="css/style.css">

{* あるいはこれだけ *}
{asset 'css/style.css'}

これだけです。このライブラリは自動的に次のことをします。

  • ファイルが変わった時刻をもとにバージョンのパラメータを足します
  • 画像の寸法を見分けて HTML に入れます
  • ファイルの種類ごとに正しい HTML の要素を作ります
  • 開発の環境でも本番の環境でも面倒を見ます

インストール

Nette Assets は Composerで入れます。

composer require nette/assets

PHP 8.1 以上が要り、Nette Framework と見事に組み合わさりますが、単独でも使えます。

はじめの一歩

Nette Assets は設定なしでそのまま動きます。静的ファイルを www/assets/ のディレクトリに置いて、使いはじめてください。

{* 寸法を自動で付けて画像を表示します *}
{asset 'logo.png'}

{* バージョン付きでスタイルシートを読み込みます *}
{asset 'style.css'}

{* スクリプトを読み込みます *}
{asset 'app.js'}

作られる HTML をもっと思いどおりにしたいなら、n:asset の属性か asset() の関数を使います。

どう働くか

Nette Assets は 3 つの中心の考え方の上に建っていて、それが強さと使いやすさを両立させています。

アセット – 賢くなったあなたのファイル

アセット はアプリケーションのどんな静的ファイルも表します。それぞれのファイルは、役に立つ読み取り専用のプロパティを持つオブジェクトになります。

$image = $assets->getAsset('photo.jpg');
echo $image->url;      // '/assets/photo.jpg?v=1699123456'
echo $image->file;     // '/var/www/assets/photo.jpg'(手元のパス、または null)
echo $image->width;    // 1920
echo $image->height;   // 1080
echo $image->mimeType; // 'image/jpeg'

ファイルの種類ごとに違うプロパティが得られます。

  • 画像: 幅、高さ、代わりの文、遅延の読み込み
  • スクリプト: module の種類、整合性のハッシュ、crossorigin
  • スタイルシート: メディアクエリ、整合性
  • 音声/動画: 長さ、寸法(動画のみ)
  • フォント: CORS を伴う正しい先読み

このライブラリはファイルの種類を自動的に見分け、ふさわしいアセットのクラスを作ります。

マッパー – ファイルはどこから来るのか

マッパー は、ファイルの見つけ方とその URL の作り方を知っています。目的ごとに複数のマッパーを持てます。手元のファイル、CDN、クラウドの保管場所、ビルドの道具などです(それぞれに名前があります)。組み込みの FilesystemMapper は手元のファイルを、ViteMapper は今どきのビルドの道具を受け持ちます。

マッパーは設定で定めます。

レジストリ – あなたの主な窓口

レジストリ はすべてのマッパーを管理し、主な API を差し出します。

// サービスにレジストリを注入します
public function __construct(
	private Nette\Assets\Registry $assets
) {}
// いろいろなマッパーからアセットを得ます
$logo = $this->assets->getAsset('images:logo.png'); // 'images' のマッパー
$app = $this->assets->getAsset('app:main.js'); // 'app' のマッパー
$style = $this->assets->getAsset('style.css'); // 既定のマッパーを使います

レジストリは正しいマッパーを自動的に選び、性能のために結果を蓄えます。

PHP でアセットを扱う

Registry にはアセットを取り出すメソッドが 2 つあります。

// ファイルが存在しなければ Nette\Assets\AssetNotFoundException を投げます
$logo = $assets->getAsset('logo.png');

// ファイルが存在しなければ null を返します
$banner = $assets->tryGetAsset('banner.jpg');
if ($banner) {
	echo $banner->url;
}

マッパーを指定する

どのマッパーを使うかをはっきり選べます。

// 既定のマッパーを使います
$file = $assets->getAsset('document.pdf');

// 接頭辞で特定のマッパーを使います
$image = $assets->getAsset('images:photo.jpg');

// 配列の書き方で特定のマッパーを使います
$script = $assets->getAsset(['scripts', 'app.js']);

アセットのプロパティと種類

アセットの種類ごとに、それに合う読み取り専用のプロパティが得られます。

// 画像のプロパティ
$image = $assets->getAsset('photo.jpg');
echo $image->width;     // 1920
echo $image->height;    // 1080
echo $image->mimeType;  // 'image/jpeg'

// スクリプトのプロパティ
$script = $assets->getAsset('app.js');
echo $script->type;     // null(Vite の入口なら 'module')

// 音声のプロパティ
$audio = $assets->getAsset('song.mp3');
echo $audio->duration;  // 長さ(秒)

// どのアセットも文字列にできます(URL を返します)
$url = (string) $assets->getAsset('document.pdf');

寸法や長さのようなプロパティは、触れられたときにだけ遅れて読み込まれるので、ライブラリは速いままです。

静的な解析を正確にするには、nette/phpstan-rulesの拡張を入れてください。そうすると PHPStan はそれぞれのアセットの具体的な型を知るので、getAsset('photo.jpg')ImageAsset と理解され、->width に触れてもエラーになりません。

Latte のテンプレートでアセットを使う

Nette Assets はタグと関数による直感的な Latteとの組み合わせを備えています。

{asset}

{asset} のタグは完全な HTML の要素を描きます。

{* 描かれるもの: <img src="/assets/hero.jpg?v=123" width="1920" height="1080"> *}
{asset 'hero.jpg'}

{* 描かれるもの: <script src="/assets/app.js?v=456"></script> *}
{asset 'app.js'}

{* 描かれるもの: <link rel="stylesheet" href="/assets/style.css?v=789"> *}
{asset 'style.css'}

このタグは自動的に次のことをします。

  • アセットの種類を見分けてふさわしい HTML を作ります
  • キャッシュを無効にするバージョン付けを入れます
  • 画像には寸法を足します
  • 正しい属性(type、media など)を設定します

HTML の属性の中や <style><script> の要素の中で使うと、URL だけを出力します。

<div style="background-image: url({asset 'bg.jpg'})">
<img srcset="{asset 'logo@2x.png'} 2x">

n:asset

HTML の属性を完全に思いどおりにしたいときはこれです。

{* n:asset の属性が src や寸法などを埋めます *}
<img n:asset="product.jpg" alt="Product" class="rounded">

{* 当てはまるどの要素でも働きます *}
<script n:asset="analytics.js" defer></script>
<link n:asset="print.css" media="print">
<audio n:asset="podcast.mp3" controls></audio>

変数とマッパーを使います。

{* 変数も自然に使えます *}
<img n:asset="$product->image">

{* 波かっこでマッパーを指定します *}
<img n:asset="images:{$product->image}">

{* 配列の書き方でマッパーを指定します *}
<img n:asset="[images, $product->image]">

n:asset<a> でも働き、そこでは href を埋めます。<link> でも働き、そこでは先読みのヒントを作ります。

<a n:asset="hero.jpg">画像をダウンロード</a>

<a><link> の形が働くのは、描ける種類のアセット(画像、スクリプトなど)だけで、PDF のような GenericAsset では働かないことに注意してください。

画像では width(か height)だけを設定すれば足り、もう一方の寸法は縦横の比を保つように自動的に計算されます。

{* height は縦横の比から補われます *}
<img n:asset="product.jpg" width="200">

asset()

最大限のしなやかさが欲しいなら、asset() の関数を使います。

{var $logo = asset('logo.png')}
<img src={$logo} width={$logo->width} height={$logo->height}>

{* あるいは直接 *}
<img src={asset('logo.png')} alt="Logo">

省略できるアセット

{asset?}n:asset?tryAsset() で、ないアセットを穏やかに扱えます。

{* 省略できるタグ - アセットがなければ何も描きません *}
{asset? 'optional-banner.jpg'}

{* 省略できる属性 - アセットがなければ飛ばします *}
<img n:asset?="user-avatar.jpg" alt="Avatar" class="avatar">

{* 代わりのものを用意します *}
{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')}
<img n:asset=$avatar alt="Avatar">

{preload}

ページの読み込みの速さを上げます。

{* <head> の節の中で *}
{preload 'critical.css'}
{preload 'important-font.woff2'}
{preload 'hero-image.jpg'}

ふさわしい先読みのリンクを作ります。

<link rel="preload" href="/assets/critical.css?v=123" as="style">
<link rel="preload" href="/assets/important-font.woff2?v=456" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/hero-image.jpg?v=789" as="image" type="image/jpeg">

レスポンスが nonce の付いた Content-Security-Policy のヘッダーを設定している場合、Nette は作られるすべての <script><link><style> の要素に、合う nonce の属性を自動的に足すので、ブラウザの安全の方針に遮られません。

進んだ機能

拡張子の自動の判別

複数の形式を自動的に扱います。

assets:
	mapping:
		images:
			path: img
			extension: [webp, jpg, png]  # この順に試します

これで拡張子なしで求められます。

{* logo.webp、logo.jpg、logo.png を自動的に見つけます *}
{asset 'images:logo'}

今どきの形式で段階的に良くしていくのにぴったりです。

賢いバージョン付け

ファイルは、変わった時刻をもとに自動的にバージョン付けされます。

{asset 'style.css'}
{* 出力: <link rel="stylesheet" href="/assets/style.css?v=1699123456"> *}

ファイルを更新するとタイムスタンプが変わり、ブラウザのキャッシュが更新されます。

アセットごとにバージョン付けを決められます。

// 特定のアセットのバージョン付けを切ります
$asset = $assets->getAsset('style.css', ['version' => false]);
{* Latte では *}
{asset 'style.css', version: false}

同じ reference, key: value の書き方で、n:asset{preload} にもオプションを渡せます。

<img n:asset="photo.jpg, version: false">
{preload 'style.css', version: false}

フォントのアセット

フォントは正しい CORS とともに特別に扱われます。

{* crossorigin を伴う正しい先読み *}
{preload 'fonts:OpenSans-Regular.woff2'}

{* CSS で使います *}
<style>
@font-face {
	font-family: 'Open Sans';
	src: url('{asset 'fonts:OpenSans-Regular.woff2'}') format('woff2');
	font-display: swap;
}
</style>

独自のマッパー

クラウドの保管場所や動的な生成のような特別な必要には、独自のマッパーを作ります。

use Nette\Assets\Mapper;
use Nette\Assets\Asset;
use Nette\Assets\Helpers;

class CloudStorageMapper implements Mapper
{
	public function __construct(
		private CloudClient $client,
		private string $bucket,
	) {}

	public function getAsset(string $reference, array $options = []): Asset
	{
		if (!$this->client->exists($this->bucket, $reference)) {
			throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found");
		}

		$url = $this->client->getPublicUrl($this->bucket, $reference);
		return Helpers::createAssetFromUrl($url);
	}
}

設定に登録します。

assets:
	mapping:
		cloud: CloudStorageMapper(@cloudClient, 'my-bucket')

ほかのマッパーと同じように使います。

{asset 'cloud:user-uploads/photo.jpg'}

Helpers::createAssetFromUrl() メソッドは、ファイルの拡張子をもとに正しいアセットの種類を自動的に作ります。

Nette\Assets\HtmlRenderable インターフェースを実装するアセットの種類(画像、スクリプト、スタイルなど)は、{asset} で完全な HTML の要素として描けます。そのほかのファイルの種類は GenericAsset(たとえば PDF)になり、HTML の要素としては描けませんが、URL(とそのほかのメタデータ)は得られます。そうしたアセットを HTML の要素として描こうとすると Nette\InvalidArgumentException が投げられます。属性の中でその URL を使うことはできます。

さらに読む

バージョン: 1.x