檔案上傳
本頁面內容

檔案上傳

在您的 React Router 應用程式中處理檔案上傳。本指南使用 Remix The Web 專案中的一些套件,讓檔案上傳更輕鬆。

感謝 David Adams 撰寫原始指南,本文件即以此為基礎。您可以參考該指南以獲得更多範例。

基本檔案上傳

1. 設定一些路由

您可以隨意設定路由。此範例使用以下結構

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;

2. 新增表單資料解析器

form-data-parserrequest.formData() 的包裝器,提供串流支援以處理檔案上傳。

npm i @mjackson/form-data-parser

請參閱 form-data-parser 文件以取得更多資訊

3. 建立具有上傳動作的路由

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>
  );
}

本地儲存實作

1. 新增儲存套件

file-storage 是用於在 JavaScript 中儲存 File 物件的鍵/值介面。類似於 localStorage 允許您在瀏覽器中儲存字串的鍵/值對,file-storage 允許您在伺服器上儲存檔案的鍵/值對。

npm i @mjackson/file-storage

請參閱 file-storage 文件以取得更多資訊

2. 建立儲存設定

建立一個檔案,匯出一個 LocalFileStorage 實例,供不同的路由使用。

import { LocalFileStorage } from "@mjackson/file-storage/local";

export const fileStorage = new LocalFileStorage(
  "./uploads/avatars"
);

export function getStorageKey(userId: string) {
  return `user-${userId}-avatar`;
}

3. 實作上傳處理器

更新表單的 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>
  );
}

4. 新增路由以提供上傳的檔案

建立一個 資源路由,將檔案作為回應串流。

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}`,
    },
  });
}
文件與範例 CC 4.0