inertia.jsのLinkとファイルダウンロードについて
個人的に重要なところだけピックアップしつつ解説していく。
単純なページ遷移を試してみる
なお、ここでは全てreact
を使う。
以下のように2つのルートを作る
- /demo リンク元
- /demo/page リンク先
routes/web.php
use Inertia\Inertia;
Route::get('/demo', function () {
return Inertia::render('Demo/Index', [ ]);
})->name('demo.index');
Route::get('/demo/page', function () {
return Inertia::render('Demo/Page', [ ]);
})->name('demo.page');
このように単純に2ファイルレンダリングする準備をrouteの中のクロージャーで完結している。
以降それぞれ見ていこう。
Index.jsx
resources/js/Pages/Demo/Index.jsx
import { Link, Head } from '@inertiajs/react';
export default function Demo() {
return (
<>
<Head title="Demo" />
<div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center">
<h1 className="text-4xl font-extrabold text-gray-800 mb-6">Link Demo</h1>
<div className="flex space-x-4 mb-6">
<Link
href={route('demo.page')}
className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
Demo Page (Inertia.js Link)
</Link>
</div>
</div>
</>
);
}
tailwind cssのクラスがclassName
にもりもり付いてて若干うっとうしいけど、まあこんな感じでLinkを作成できる。以下のような見た目になるだろう。
見た目はともかく、Linkで遷移できる事を確認するために、Demo/Pageも作る
Page.jsx
import { Link, Head } from '@inertiajs/react';
export default function Page() {
return (
<>
<Head title="Demo Page" />
<div className="min-h-screen bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-600 flex flex-col items-center justify-center text-white">
<header className="text-center mb-8">
<h1 className="text-5xl font-bold drop-shadow-lg">Welcome to the Demo Page</h1>
<p className="mt-4 text-lg opacity-90">Exploring the possibilities of Inertia.js and Tailwind CSS</p>
</header>
<main className="w-full max-w-4xl p-6 bg-white bg-opacity-20 rounded-lg shadow-lg">
<section className="mb-6">
<h2 className="text-3xl font-semibold mb-4">Features</h2>
<ul className="space-y-3 list-disc list-inside">
<li className="opacity-90">Dynamic routing with <code>@inertiajs/react</code></li>
<li className="opacity-90">Modern UI powered by Tailwind CSS</li>
<li className="opacity-90">Seamless navigation</li>
</ul>
</section>
<section className="mb-6">
<h2 className="text-3xl font-semibold mb-4">Learn More</h2>
<p className="opacity-90">
Check out the official documentation for
<a href="https://inertiajs.com/" className="text-blue-300 underline hover:text-blue-400 ml-1">
Inertia.js
</a>
and
<a href="https://tailwindcss.com/" className="text-blue-300 underline hover:text-blue-400 ml-1">
Tailwind CSS
</a>.
</p>
</section>
<section className="text-center">
<Link
href={route('demo.index')}
className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
Back to Demo
</Link>
</section>
</main>
<footer className="mt-8 text-sm opacity-70">
<p>© 2024 Demo Project. All rights reserved.</p>
</footer>
</div>
</>
);
}
これはまあAIが作ったんだけど、とりあえずテンションの上がりそうなデモページができあがったんじゃないかな?
これでIndex → Page → Indexの遷移ができる事を確認しておく。
こんな感じにレンダリングされる。
さらに
<Link
href={route('demo.page')}
as="button" className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
Demo Page (Inertia.js Link)
</Link>
ここで as="button"
とすると、もはや見た目はcssでwrapされててようわかんけど
buttonでレンダリングされているる。遷移はうまいこと内部でやってるようだ
なお、ドキュメントに
POST
/PUT
/PATCH
/DELETE
の作成にaタグを使ってはいけないと書かれている。これはまあそうだよね。
メソッド指定
上記の注釈でそうはいったものの、Inertia リンクの HTTP リクエストメソッドを method
プロパティで指定することもできて、これを使用して POST
/PUT
/PATCH
/DELETE
のリクエストを送信できる。ここではPUT
を送信してみる。注釈の通りas="button"にしときますよ。
<Link
href={route('demo.put')}
method="put"
as="button"
className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
PUT method
</Link>
routes/web.phpに適当なエンドポイントを作って
Route::put('/demo/put', function () {
dd("called");
})->name('demo.put');
"called" と表示されて止める。これを確認しておく。
パラメーターの追加
さらにリクエストに対し、パラメーターも与える事ができる
<Link
href={route('demo.put')}
method="put"
as="button"
data={{ foo: "bar" }}
className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
PUT method
</Link>
とすれば
use Illuminate\Http\Request;
Route::put('/demo/put', function (Request $request) {
dd($request->all());
})->name('demo.put');
、となる。
Headerの追加
さらに
<Link
href={route('demo.put')}
method="put"
as="button"
headers={{ foo: "bar" }}
className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
PUT method
</Link>
としたらheaderを付けることができ、
Route::put('/demo/put', function (Request $request) {
dd($request->headers->all());
})->name('demo.put');
とすると
となる。
Partialリロード
only
プロパティを使用すると、ページの一部のプロパティ(データ)のみをサーバーから取得するよう指定できるってわけで、これがパーシャルリロードなんだけど
import { Link } from '@inertiajs/react';
<Link href="/users?active=true" only={['users']}>Show active</Link>
まあこれはちょっと概念がむずいのでいずれ別に解説ページを作るかもしれないし、作らないかもしれない。
Aタグとの比較
いよいよここからAタグと比較してみよう。まず
import { Link, Head} from '@inertiajs/react';
import { router } from '@inertiajs/react';
export default function Demo() {
return (
<>
<Head title="Demo" />
<div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center">
<h1 className="text-4xl font-extrabold text-gray-800 mb-6">Link Demo</h1>
<div className="flex space-x-4 mb-6">
<Link
href={route('demo.page')}
className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
Demo Page (Inertia.js Link)
</Link>
<a
href={route('demo.page')}
className="px-6 py-3 bg-gray-800 text-white rounded-lg shadow-md hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2 transition-all"
>
Demo Page (HTML Anchor)
</a>
</div>
</div>
</>
);
}
このようにほとんど同じ機能のものを2つ作る。すると
こうなるんだけど、開発者タブでネットワークをみてみてInertia.jsのLinkコンポーネントをクリックすると
これだけの読み込みしか行っていない。
これに対してAタグをクリックすると
このように、大量の読み込みが発生しているのがわかる。これが違い。
Linkコンポーネントの欠点
ほとんどのニーズに対応できるのだが、これはバイナリーダウンロードのリンクを発生させられない。これに関しては <A>タグを使うのが正解 なのだけど、たとえばこういう仕組みを考えてみたとき
import { Link, Head, useForm } from '@inertiajs/react';
import InputLabel from '@/Components/InputLabel';
import TextInput from '@/Components/TextInput';
export default function Demo() {
const { data, setData, post } = useForm({
password: '',
});
const submit = (e) => {
e.preventDefault();
post(route('download.readme'));
};
return (
<>
<Head title="Demo" />
<div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center">
<h1 className="text-4xl font-extrabold text-gray-800 mb-6">Link Demo</h1>
<div className="flex space-x-4 mb-6">
<Link
href={route('demo.page')}
className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
Demo Page (Inertia.js Link)
</Link>
<a
href={route('demo.page')}
className="px-6 py-3 bg-gray-800 text-white rounded-lg shadow-md hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2 transition-all"
>
Demo Page (HTML Anchor)
</a>
</div>
<div>
<form onSubmit={submit} className="flex flex-col space-y-4">
<InputLabel htmlFor="password" value="Password"/>
<TextInput
id="password"
type="password"
name="password"
className="mt-1 block w-full"
onChange={(e) => setData('password', e.target.value)}
/>
<button
type="submit"
className="px-6 py-3 bg-teal-600 text-white rounded-lg shadow-md hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 transition-all"
>
Download readme.zip (Inertia.js Form POST)
</button>
</form>
</div>
</div>
</>
);
}
でたとえばパスワードがmoge
のときだけダウンロードできるとかそういうのとかよくあるじゃないすか
// File download route
Route::match(['get', 'post'], '/download/readme', function (Request $request) {
$password = request()->input('password');
if ($password !== 'moge') {
abort(403, 'Unauthorized');
}
$path = storage_path('app/readme.zip');
if (!file_exists($path)) {
abort(404, 'File not found');
}
return response()->download($path, 'readme.zip');
})->name('download.readme');
なお、これは動作しない
で議論されているようにformにしないといけなかったりする
import { Link, Head, useForm } from '@inertiajs/react';
import { router } from '@inertiajs/react';
import InputLabel from '@/Components/InputLabel';
import TextInput from '@/Components/TextInput';
import SecondaryButton from '@/Components/SecondaryButton';
export default function Demo({ csrfToken }) {
const { data, setData, post, processing } = useForm({
password: '',
});
const submit = (e) => {
e.preventDefault();
post(route('download.readme'));
};
return (
<>
<Head title="Demo" />
<div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center">
<h1 className="text-4xl font-extrabold text-gray-800 mb-6">Secure Download Demo</h1>
<div className="flex space-x-4 mb-6">
<Link
href={route('demo.page')}
className="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all"
>
Demo Page (Inertia.js Link)
</Link>
<a
href={route('demo.page')}
className="px-6 py-3 bg-gray-800 text-white rounded-lg shadow-md hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2 transition-all"
>
Demo Page (HTML Anchor)
</a>
</div>
<div className="w-full max-w-md space-y-8">
{/* Classic Form with CSRF Token */}
<form action={route('download.readme')} method="post" className="bg-white p-6 shadow rounded-lg">
<input type="hidden" name="_token" value={csrfToken} />
<div className="mb-4">
<InputLabel htmlFor="password" value="Password" />
<TextInput
id="password"
type="password"
name="password"
className="mt-1 block w-full"
/>
</div>
<SecondaryButton type="submit" className="w-full">
Download (Classic Form)
</SecondaryButton>
</form>
{/* Inertia.js Form */}
<form onSubmit={submit} className="bg-white p-6 shadow rounded-lg">
<div className="mb-4">
<InputLabel htmlFor="password" value="Password" />
<TextInput
id="password"
type="password"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
className="mt-1 block w-full"
/>
</div>
<button
type="submit"
disabled={processing}
className="px-6 py-3 w-full bg-teal-600 text-white rounded-lg shadow-md hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 transition-all disabled:opacity-50"
>
{processing ? 'Processing...' : 'Inertia Form (NOT WORK)'}
</button>
</form>
</div>
</div>
</>
);
}
// File download route
Route::match(['get', 'post'], '/download/readme', function (Request $request) {
$password = request()->input('password');
if ($password !== 'moge') {
abort(403, 'Unauthorized');
}
$path = storage_path('app/readme.zip');
if (!file_exists($path)) {
abort(404, 'File not found');
}
return response()->download($path, 'readme.zip');
})->name('download.readme');
まあ、こんな感じでうまいこと対応する事。
このようになり、Classic Formは動作するけどinertiaのformは動作しない。これはまあそういうもんだと思って諦めないといけないので、設計するときにはこれを最初から理解して行う事が必要になるわけだ。
Discussion