Web標準を活かすRemix:フォーム送信の基礎からファイルアップロードまで
フォームからファイル送信をする。色んな方法があるかと思う。今回は最もベーシックなmultipart/form-data形式での送信でやってみよう。
そもそもフォーム送信を理解する
RemixはWeb標準に準拠しているのでその内容を理解することが重要。まずはその理解から。
form送信には一般的なx-www-form-urlencoded形式とmultipart/form-data形式の2つが存在する。
x-www-form-urlencoded形式
<form method="POST">
<input type="text" name="name" />
</form>
テキストをURLエンコードして一度にまとめて送信する。名前とそのコンテンツ、という1対1の情報しか持つことができない。つまりファイル送信ができない。
ファイルを送信した場合、フォームのフィールド名、ファイル名、ファイル種類、ファイルの内容といった4つの情報が送られる。が、1つの情報しか持てないのでファイル名だけが送信されることになる。
multipart/form-data形式
<form method="POST" type="multipart/form-data">
<input type="text" name="name" />
<input type="file" name="image" />
</form>
この形式だとname属性ごとに分割してデータ送信が行われる、と理解している。その代わり、ファイルの4つの情報がまとめて一回で送信することが可能となり、ファイル送信ができるのだ。
この通信形式の違いを把握すると、どのようなプログラムが必要かイメージが湧いてくる。おそらくmultipart/form-data形式のフォーム送信の場合はコールバック式などで都度通信を受け止めるのだな、と。
Remixでx-www-form-urlencoded形式のform
これは一番よく実装するパターン。Remixのドキュメントにもほぼこの方法が。Remixでapplication/jsonで通信すると逆に面倒なイメージがある。
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const name = formData.get("name");
console.log("name", name);
return json({ message: "Hello, World!" });
}
export default function Index() {
return (
<div>
<Form method="post">
<input name="name" type="text" />
<button type="submit">Submit</button>
</Form>
</div>
);
}
Remixでmultipart/form-data形式のform
続いてファイル送信。この解説がドキュメントになぜか無い。なぜなんだ。そしてメソッドがunstableなのは…それはさておき紹介する。
export async function action({ request }: ActionFunctionArgs) {
const uploadHandler = composeUploadHandlers(async ({ name, data }) => {
if (name !== "image") return undefined;
return "画像のURLよー";
}, createMemoryUploadHandler());
const formData = await parseMultipartFormData(request, uploadHandler);
const name = formData.get("name");
const image = formData.get("image");
console.log("name", name);
console.log("image", image);
return json({ message: "Hello, World!" });
}
export default function Index() {
return (
<div className="font-sans p-4 max-w-xl">
<Form
method="post"
encType="multipart/form-data"
className="flex flex-col space-y-5"
>
<h1 className="text-3xl mb-5">Cloudflare Images Sample.</h1>
<label>
<span>Name</span>
<input
type="text"
name="name"
className="block mt-1 w-full"
required
/>
</label>
<label>
<span>Image File</span>
<input
type="file"
name="image"
className="block mt-1 w-full"
accept="image/*"
/>
</label>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Submit
</button>
</Form>
</div>
);
}
メソッド名にunstableが付いていてわかりにくいので別名で利用する。
import {
unstable_composeUploadHandlers as composeUploadHandlers,
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
unstable_parseMultipartFormData as parseMultipartFormData,
} from "@remix-run/cloudflare";
Remixが用意してくれているメソッドを利用してデータを受信する。前述したようにname属性ごとにuploadHandlerが発火するイメージ。
const formData = await parseMultipartFormData(request, uploadHandler);
そのuploadHandlerの中身だ。
const uploadHandler = composeUploadHandlers(nameがimageの場合のハンドラ, その他の場合のハンドラ)
順番が大事。最初にname属性がimageの場合の処理を行う。違ってたらundefinedをreturn。すると、次のハンドラに渡される。
次のハンドラは createMemoryUploadHandler
で、これはRemixが用意してくれているハンドラ関数。他のハンドラのフォールバックとして利用するようだ。通常のテキスト情報をそのまま返してくれる。
return "画像のURLよー"
の箇所だが、ここでCloudflare Imagesなどに画像をアップロードし、取得したURLをreturnするイメージだ。Cloudflare Imagesにアップロードする処理の解説は後日。
const uploadHandler = composeUploadHandlers(async ({ name, data }) => {
if (name !== "image") return undefined;
return "画像のURLよー";
}, createMemoryUploadHandler());
あとは通常のフォームと同じようにデータを取得できる。
const name = formData.get("name");
const image = formData.get("image");
console.log("name", name);
console.log("image", image);
RemixはWeb標準の機能が生きるっていうのは、こういうことも含めてなのかもと実感。
Web標準を学ぶにはこの書籍をオススメする。当然この記事で話した内容ももっと詳しく書いてある。
Discussion