什么是UploadThing?
在Next.js中使用UploadThing前先介绍一下UploadThing。借用官方的一句话:UploadThing 是将文件上传添加到全栈 TypeScript 应用程序的最简单方法,UploadThing 是一个用于构建文件上传功能的平台,它是我们平时开发中进行文件托管的一种方案,可以更好的为我们管理文件,现在的工作原理主要通过缓存和回调来包装 S3来实现文件管理。
在所有权上,你可以将文件发布到UploadThing上管理,也可以将文件上传到你的服务器上的 /api/uploadthing 端点,它提供了一个易于使用的 API,不过你必须在自己的服务器上托管一个 UploadThing 端点,才能使用 UploadThing 的功能。
为什么需要托管端点
所有权和控制权: 托管端点意味着你拥有对文件上传的完全控制权,你可以决定如何处理上传的文件,例如存储位置、权限等。
带宽成本: 你无需将上传的文件通过 UploadThing 的服务器,可以避免额外的带宽成本。
正文开始
- 访问链接 :uploadthing.com/
Create a new app store
当我们登录网页后,就可以创建应用文件管理仓库了
创建完成后就可以看到一个 dashboard 面板
在练手的时候可以创建一个免费版的,容量只有2G但是学习基本够用了
设置秘钥
访问API Keys这个目录,将 API 密钥复制并添加项目.env文件作为环境变量
安装
bun add uploadthing @uploadthing/react
or
pnpm add uploadthing @uploadthing/react
or
npm install uploadthing @uploadthing/react
设置文件路由器
上传到 uploadthing 的所有文件都与 FileRoute 关联
- 设置支持上传的文件类型
["image", "video", ...]Max file size最大文件大小middleware用于验证和标记请求onUploadComplete上传完成时的回调
- app/api/uploadthing/core.ts
import { createUploadthing, type FileRouter } from 'uploadthing/next'
import { auth } from '@clerk/nextjs/server'
const f = createUploadthing()
const authenticateUser = () => {
const user = auth()
// If you throw, the user will not be able to upload
if (!user) throw new Error('Unauthorized')
// Whatever is returned here is accessible in onUploadComplete as `metadata`
return user
}
// FileRouter for your app, can contain multiple FileRoutes
export const ourFileRouter = {
// Define as many FileRoutes as you like, each with a unique routeSlug
subaccountLogo: f({ image: { maxFileSize: '4MB', maxFileCount: 1 } })
.middleware(authenticateUser)
.onUploadComplete(() => { }),
avatar: f({ image: { maxFileSize: '4MB', maxFileCount: 1 } })
.middleware(authenticateUser)
.onUploadComplete(() => { }),
agencyLogo: f({ image: { maxFileSize: '4MB', maxFileCount: 1 } })
.middleware(authenticateUser)
.onUploadComplete(() => { }),
media: f({ image: { maxFileSize: '4MB', maxFileCount: 1 } })
.middleware(authenticateUser)
.onUploadComplete(() => { }),
} satisfies FileRouter
export type OurFileRouter = typeof ourFileRouter
- app/api/uploadthing/route.ts
import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";
// Export routes for Next App Router
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
// Apply an (optional) custom config:
// config: { ... },
});
创建 UploadThing 组件
generateUploadButton函数用于生成用于与 UploadThing 交互的 UploadButton 组件。generateUploadDropzone函数用于生成用于与 UploadThing 交互的 UploadDropzone 组件。
生成组件允许将完全类型安全的组件绑定到文件路由器的类型
- lib/uploadthing.ts
import {
generateUploadButton,
generateUploadDropzone,
} from "@uploadthing/react";
import type { OurFileRouter } from "@/app/api/uploadthing/core";
export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
UploadThing SSR 插件[可选]
UploadThing 需要从您的服务器获取信息以获取权限信息。通常这意味着加载状态。我们构建了一个可选插件来防止这种情况
要添加 SSR 水合并避免该加载状态,只需在子级之前的根布局主体中渲染
<NextSSRPlugin /> 水合助手即可。
- app/layout.tsx
import { NextSSRPlugin } from "@uploadthing/react/next-ssr-plugin";
import { extractRouterConfig } from "uploadthing/server";
import { ourFileRouter } from "@/app/api/uploadthing/core";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<NextSSRPlugin
/**
* The `extractRouterConfig` will extract **only** the route configs
* from the router to prevent additional information from being
* leaked to the client. The data passed to the client is the same
* as if you were to fetch `/api/uploadthing` directly.
*/
routerConfig={extractRouterConfig(ourFileRouter)}
/>
{children}
</body>
</html>
);
}
项目演示
场景介绍:form 表单中需要上传文件
- app/agency/page.tsx
"use client"
import { Agency } from "@prisma/client";
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { AlertDialog } from "@/components/ui/alert-dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import FileUpload from "@/components/global/file-upload";
const formSchema = z.object({
agencyLogo: z.string().min(1)
})
interface AgencyDetailsProps {
data?: Partial<Agency>
}
const AgencyDetails = ({ data }: AgencyDetailsProps) => {
const form = useForm<z.infer<typeof formSchema>>({
mode: 'onChange',
resolver: zodResolver(formSchema),
defaultValues: {
agencyLogo: data?.agencyLogo || "",
}
})
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values)
}
const isLoading = form.formState.isSubmitting;
return (
<AlertDialog>
<Card className="w-full">
<CardHeader>
<CardTitle>Agency Information</CardTitle>
<CardDescription>
Lets create an agency for you business. You can edit agency settings
later from the agency settings tab.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
disabled={isLoading}
control={form.control}
name="agencyLogo"
render={({ field }) => (
<FormItem>
<FormLabel>Agency Logo</FormLabel>
<FormControl>
<FileUpload
apiEndpoint="agencyLogo"
onChange={field.onChange}
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</CardContent>
</Card>
</AlertDialog>
);
};
export default AgencyDetails;
- components/global/file-upload.tsx
"use client"
import { FileIcon, X } from 'lucide-react'
import Image from 'next/image'
import { Button } from '../ui/button'
import { UploadDropzone } from '@/lib/uploadthing'
interface FileUploadProps = {
apiEndpoint: 'agencyLogo' | 'avatar' | 'subaccountLogo'
onChange: (url?: string) => void
value?: string
}
const FileUpload = ({ apiEndpoint, onChange, value }: FileUploadProps) => {
const type = value?.split('.').pop()
if (value) {
return (
<div className="flex flex-col justify-center items-center">
{type !== 'pdf' ? (
<div className="relative w-40 h-40">
<Image
src={value}
alt="uploaded image"
className="object-contain"
fill
/>
</div>
) : (
<div className="relative flex items-center p-2 mt-2 rounded-md bg-background/10">
<FileIcon />
<a
href={value}
target="_blank"
rel="noopener_noreferrer"
className="ml-2 text-sm text-indigo-500 dark:text-indigo-400 hover:underline"
>
View PDF
</a>
</div>
)}
<Button
onClick={() => onChange('')}
variant="ghost"
type="button"
>
<X className="h-4 w-4" />
Remove Logo
</Button>
</div>
)
}
return (
<div className="w-full bg-muted/30">
<UploadDropzone
endpoint={apiEndpoint}
onClientUploadComplete={(res) => {
onChange(res?.[0].url)
}}
onUploadError={(error: Error) => {
console.log('Upload Error', error)
}}
/>
</div>
)
}
export default FileUpload
效果展示
其他
当我们点击上传是组件内部会自动调用 Uploadthing API
POST /api/uploadthing?slug=agencyLogo
对应的是我们app/api/uploadthing/route.ts中的agencyLogo路由
agencyLogo: f({ image: { maxFileSize: '4MB', maxFileCount: 1 } })
.middleware(authenticateUser)
.onUploadComplete(() => { }),
上传成功后会返回静态资源链接🔗 默认格式为:"utfs.io/f/*****.png"
参考项目地址(GitHub):github.com/jshuaishuai…
@aws-sdk/client-s3 上传实例参考
import { useState } from "react";
import {
ListObjectsCommandOutput,
S3Client,
PutObjectCommand,
} from "@aws-sdk/client-s3";
import "./App.css";
function App() {
const [filePath, setFilePath] = useState('');
const bucketName = 'file-ms';
const s3Client = new S3Client({
region: "us-west-1",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY as string,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY as string,
}
})
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
const newFilename = encodeURIComponent(`files/${file.name}`);
try {
const command = new PutObjectCommand({
Body: file,
ACL: 'public-read',
Bucket: bucketName,
Key: newFilename, // 设置文件在 S3 上的路径
ContentType: file.type,
});
await s3Client?.send(command);
const url = `https://${bucketName}.s3.amazonaws.com/${newFilename}`;
setFilePath(url);
// 更新文件列表
} catch (error) {
console.error('Error uploading file', error);
}
};
return (
<div className="App">
<input type="file" onChange={handleFileUpload} />
<p>{filePath}</p>
</div>
);
}
export default App;
end
感谢观看!!