在您的 React Router 應用程式中處理檔案上傳。本指南使用 Remix The Web 專案中的一些套件,讓檔案上傳更輕鬆。
感謝 David Adams 撰寫原始指南,本文件即以此為基礎。您可以參考該指南以獲得更多範例。
您可以隨意設定路由。此範例使用以下結構
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
// ... other routes
route("user/:id", "pages/user-profile.tsx", [
route("avatar", "api/avatar.tsx"),
]),
] satisfies RouteConfig;
form-data-parser
是 request.formData()
的包裝器,提供串流支援以處理檔案上傳。
npm i @mjackson/form-data-parser
請參閱 form-data-parser
文件以取得更多資訊
parseFormData
函式接受一個 uploadHandler
函式作為引數。對於表單中的每個檔案上傳,都會呼叫此函式。
您必須將表單的 enctype
設定為 multipart/form-data
,檔案上傳才能運作。
import {
type FileUpload,
parseFormData,
} from "@mjackson/form-data-parser";
export async function action({
request,
}: ActionFunctionArgs) {
const uploadHandler = async (fileUpload: FileUpload) => {
if (fileUpload.fieldName === "avatar") {
// process the upload and return a File
}
};
const formData = await parseFormData(
request,
uploadHandler
);
// 'avatar' has already been processed at this point
const file = formData.get("avatar");
}
export default function Component() {
return (
<form method="post" encType="multipart/form-data">
<input type="file" name="avatar" />
<button>Submit</button>
</form>
);
}
file-storage
是用於在 JavaScript 中儲存 File 物件的鍵/值介面。類似於 localStorage
允許您在瀏覽器中儲存字串的鍵/值對,file-storage 允許您在伺服器上儲存檔案的鍵/值對。
npm i @mjackson/file-storage
建立一個檔案,匯出一個 LocalFileStorage
實例,供不同的路由使用。
import { LocalFileStorage } from "@mjackson/file-storage/local";
export const fileStorage = new LocalFileStorage(
"./uploads/avatars"
);
export function getStorageKey(userId: string) {
return `user-${userId}-avatar`;
}
更新表單的 action
,以將檔案儲存在 fileStorage
實例中。
import {
type FileUpload,
parseFormData,
} from "@mjackson/form-data-parser";
import {
fileStorage,
getStorageKey,
} from "~/avatar-storage.server";
import type { Route } from "./+types/user-profile";
export async function action({
request,
params,
}: Route.ActionArgs) {
async function uploadHandler(fileUpload: FileUpload) {
if (
fileUpload.fieldName === "avatar" &&
fileUpload.type.startsWith("image/")
) {
let storageKey = getStorageKey(params.id);
// FileUpload objects are not meant to stick around for very long (they are
// streaming data from the request.body); store them as soon as possible.
await fileStorage.set(storageKey, fileUpload);
// Return a File for the FormData object. This is a LazyFile that knows how
// to access the file's content if needed (using e.g. file.stream()) but
// waits until it is requested to actually read anything.
return fileStorage.get(storageKey);
}
}
const formData = await parseFormData(
request,
uploadHandler
);
}
export default function UserPage({
actionData,
params,
}: Route.ComponentProps) {
return (
<div>
<h1>User {params.id}</h1>
<form
method="post"
// The form's enctype must be set to "multipart/form-data" for file uploads
encType="multipart/form-data"
>
<input type="file" name="avatar" accept="image/*" />
<button>Submit</button>
</form>
<img
src={`/user/${params.id}/avatar`}
alt="user avatar"
/>
</div>
);
}
建立一個 資源路由,將檔案作為回應串流。
import {
fileStorage,
getStorageKey,
} from "~/avatar-storage.server";
import type { Route } from "./+types/avatar";
export async function loader({ params }: Route.LoaderArgs) {
const storageKey = getStorageKey(params.id);
const file = await fileStorage.get(storageKey);
if (!file) {
throw new Response("User avatar not found", {
status: 404,
});
}
return new Response(file.stream(), {
headers: {
"Content-Type": file.type,
"Content-Disposition": `attachment; filename=${file.name}`,
},
});
}