288 lines
6.7 KiB
Markdown
288 lines
6.7 KiB
Markdown
![]() |
# TUS 上传 Hook 使用指南
|
|||
|
|
|||
|
## 概述
|
|||
|
|
|||
|
`useTusUpload` 是一个自定义 React Hook,提供了基于 TUS 协议的文件上传功能,支持大文件上传、断点续传、进度跟踪等特性。
|
|||
|
|
|||
|
## 环境变量配置
|
|||
|
|
|||
|
确保在 `.env` 文件中配置了以下环境变量:
|
|||
|
|
|||
|
```env
|
|||
|
NEXT_PUBLIC_SERVER_PORT=3000
|
|||
|
NEXT_PUBLIC_SERVER_IP=http://localhost
|
|||
|
```
|
|||
|
|
|||
|
**注意**:在 Next.js 中,客户端组件只能访问以 `NEXT_PUBLIC_` 开头的环境变量。
|
|||
|
|
|||
|
## Hook API
|
|||
|
|
|||
|
### 返回值
|
|||
|
|
|||
|
```typescript
|
|||
|
const {
|
|||
|
uploadProgress, // 上传进度 (0-100)
|
|||
|
isUploading, // 是否正在上传
|
|||
|
uploadError, // 上传错误信息
|
|||
|
handleFileUpload, // 文件上传函数
|
|||
|
getFileUrlByFileId, // 根据文件ID获取访问链接
|
|||
|
getFileInfo, // 获取文件详细信息
|
|||
|
getUploadStatus, // 获取上传状态
|
|||
|
serverUrl, // 服务器地址
|
|||
|
} = useTusUpload();
|
|||
|
```
|
|||
|
|
|||
|
### 主要方法
|
|||
|
|
|||
|
#### `handleFileUpload(file, onSuccess?, onError?)`
|
|||
|
|
|||
|
上传文件的主要方法。
|
|||
|
|
|||
|
**参数:**
|
|||
|
|
|||
|
- `file: File` - 要上传的文件对象
|
|||
|
- `onSuccess?: (result: UploadResult) => void` - 成功回调
|
|||
|
- `onError?: (error: string) => void` - 失败回调
|
|||
|
|
|||
|
**返回:** `Promise<UploadResult>`
|
|||
|
|
|||
|
**UploadResult 接口:**
|
|||
|
|
|||
|
```typescript
|
|||
|
interface UploadResult {
|
|||
|
compressedUrl: string; // 压缩版本URL(当前与原始URL相同)
|
|||
|
url: string; // 文件访问URL
|
|||
|
fileId: string; // 文件唯一标识
|
|||
|
fileName: string; // 文件名
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
#### `getFileUrlByFileId(fileId: string)`
|
|||
|
|
|||
|
根据文件ID生成访问链接。
|
|||
|
|
|||
|
**参数:**
|
|||
|
|
|||
|
- `fileId: string` - 文件唯一标识
|
|||
|
|
|||
|
**返回:** `string` - 文件访问URL
|
|||
|
|
|||
|
## 使用示例
|
|||
|
|
|||
|
### 基础使用
|
|||
|
|
|||
|
```tsx
|
|||
|
import React, { useState } from 'react';
|
|||
|
import { useTusUpload } from '../hooks/useTusUpload';
|
|||
|
|
|||
|
function UploadComponent() {
|
|||
|
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload();
|
|||
|
const [uploadedUrl, setUploadedUrl] = useState<string>('');
|
|||
|
|
|||
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|||
|
const file = e.target.files?.[0];
|
|||
|
if (!file) return;
|
|||
|
|
|||
|
try {
|
|||
|
const result = await handleFileUpload(
|
|||
|
file,
|
|||
|
(result) => {
|
|||
|
console.log('上传成功!', result);
|
|||
|
setUploadedUrl(result.url);
|
|||
|
},
|
|||
|
(error) => {
|
|||
|
console.error('上传失败:', error);
|
|||
|
},
|
|||
|
);
|
|||
|
} catch (error) {
|
|||
|
console.error('上传出错:', error);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
return (
|
|||
|
<div>
|
|||
|
<input type="file" onChange={handleFileChange} disabled={isUploading} />
|
|||
|
|
|||
|
{isUploading && (
|
|||
|
<div>
|
|||
|
<p>上传进度: {uploadProgress}%</p>
|
|||
|
<progress value={uploadProgress} max="100" />
|
|||
|
</div>
|
|||
|
)}
|
|||
|
|
|||
|
{uploadError && <p style={{ color: 'red' }}>{uploadError}</p>}
|
|||
|
|
|||
|
{uploadedUrl && (
|
|||
|
<a href={uploadedUrl} target="_blank" rel="noopener noreferrer">
|
|||
|
查看上传的文件
|
|||
|
</a>
|
|||
|
)}
|
|||
|
</div>
|
|||
|
);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### 拖拽上传
|
|||
|
|
|||
|
```tsx
|
|||
|
import React, { useCallback, useState } from 'react';
|
|||
|
import { useTusUpload } from '../hooks/useTusUpload';
|
|||
|
|
|||
|
function DragDropUpload() {
|
|||
|
const { handleFileUpload, isUploading, uploadProgress } = useTusUpload();
|
|||
|
const [dragOver, setDragOver] = useState(false);
|
|||
|
|
|||
|
const handleDrop = useCallback(
|
|||
|
async (e: React.DragEvent) => {
|
|||
|
e.preventDefault();
|
|||
|
setDragOver(false);
|
|||
|
|
|||
|
const files = e.dataTransfer.files;
|
|||
|
if (files.length > 0) {
|
|||
|
await handleFileUpload(files[0]);
|
|||
|
}
|
|||
|
},
|
|||
|
[handleFileUpload],
|
|||
|
);
|
|||
|
|
|||
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|||
|
e.preventDefault();
|
|||
|
setDragOver(true);
|
|||
|
}, []);
|
|||
|
|
|||
|
return (
|
|||
|
<div
|
|||
|
onDrop={handleDrop}
|
|||
|
onDragOver={handleDragOver}
|
|||
|
onDragLeave={() => setDragOver(false)}
|
|||
|
style={{
|
|||
|
border: dragOver ? '2px dashed #0070f3' : '2px dashed #ccc',
|
|||
|
padding: '20px',
|
|||
|
textAlign: 'center',
|
|||
|
backgroundColor: dragOver ? '#f0f8ff' : '#fafafa',
|
|||
|
}}
|
|||
|
>
|
|||
|
{isUploading ? <p>上传中... {uploadProgress}%</p> : <p>拖拽文件到这里上传</p>}
|
|||
|
</div>
|
|||
|
);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### 多文件上传
|
|||
|
|
|||
|
```tsx
|
|||
|
function MultiFileUpload() {
|
|||
|
const { handleFileUpload } = useTusUpload();
|
|||
|
const [uploadingFiles, setUploadingFiles] = useState<Map<string, number>>(new Map());
|
|||
|
|
|||
|
const handleFilesChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|||
|
const files = e.target.files;
|
|||
|
if (!files) return;
|
|||
|
|
|||
|
for (let i = 0; i < files.length; i++) {
|
|||
|
const file = files[i];
|
|||
|
const fileId = `${file.name}-${Date.now()}-${i}`;
|
|||
|
|
|||
|
setUploadingFiles((prev) => new Map(prev).set(fileId, 0));
|
|||
|
|
|||
|
try {
|
|||
|
await handleFileUpload(
|
|||
|
file,
|
|||
|
(result) => {
|
|||
|
console.log(`文件 ${file.name} 上传成功:`, result);
|
|||
|
setUploadingFiles((prev) => {
|
|||
|
const newMap = new Map(prev);
|
|||
|
newMap.delete(fileId);
|
|||
|
return newMap;
|
|||
|
});
|
|||
|
},
|
|||
|
(error) => {
|
|||
|
console.error(`文件 ${file.name} 上传失败:`, error);
|
|||
|
setUploadingFiles((prev) => {
|
|||
|
const newMap = new Map(prev);
|
|||
|
newMap.delete(fileId);
|
|||
|
return newMap;
|
|||
|
});
|
|||
|
},
|
|||
|
);
|
|||
|
} catch (error) {
|
|||
|
console.error(`文件 ${file.name} 上传出错:`, error);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
return (
|
|||
|
<div>
|
|||
|
<input type="file" multiple onChange={handleFilesChange} />
|
|||
|
|
|||
|
{uploadingFiles.size > 0 && (
|
|||
|
<div>
|
|||
|
<h4>正在上传的文件:</h4>
|
|||
|
{Array.from(uploadingFiles.entries()).map(([fileId, progress]) => (
|
|||
|
<div key={fileId}>
|
|||
|
{fileId}: {progress}%
|
|||
|
</div>
|
|||
|
))}
|
|||
|
</div>
|
|||
|
)}
|
|||
|
</div>
|
|||
|
);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
## 特性
|
|||
|
|
|||
|
### 1. 断点续传
|
|||
|
|
|||
|
TUS 协议支持断点续传,如果上传过程中断,可以从中断的地方继续上传。
|
|||
|
|
|||
|
### 2. 大文件支持
|
|||
|
|
|||
|
适合上传大文件,没有文件大小限制(取决于服务器配置)。
|
|||
|
|
|||
|
### 3. 进度跟踪
|
|||
|
|
|||
|
实时显示上传进度,提供良好的用户体验。
|
|||
|
|
|||
|
### 4. 错误处理
|
|||
|
|
|||
|
提供详细的错误信息和重试机制。
|
|||
|
|
|||
|
### 5. 自动重试
|
|||
|
|
|||
|
内置重试机制,网络异常时自动重试。
|
|||
|
|
|||
|
## 故障排除
|
|||
|
|
|||
|
### 1. 环境变量获取不到
|
|||
|
|
|||
|
确保环境变量以 `NEXT_PUBLIC_` 开头,并且 Next.js 应用已重启。
|
|||
|
|
|||
|
### 2. 上传失败
|
|||
|
|
|||
|
检查服务器是否正在运行,端口是否正确。
|
|||
|
|
|||
|
### 3. CORS 错误
|
|||
|
|
|||
|
确保后端服务器配置了正确的 CORS 设置。
|
|||
|
|
|||
|
### 4. 文件无法访问
|
|||
|
|
|||
|
确认文件上传成功后,检查返回的 URL 是否正确。
|
|||
|
|
|||
|
## 注意事项
|
|||
|
|
|||
|
1. **Next.js 环境变量**:客户端组件只能访问 `NEXT_PUBLIC_` 前缀的环境变量
|
|||
|
2. **服务器配置**:确保后端服务器支持 TUS 协议
|
|||
|
3. **文件大小**:虽然支持大文件,但要注意服务器和客户端的内存限制
|
|||
|
4. **网络环境**:在网络不稳定的环境下,断点续传功能特别有用
|
|||
|
|
|||
|
## API 路由
|
|||
|
|
|||
|
Hook 会访问以下 API 路由:
|
|||
|
|
|||
|
- `POST /upload` - TUS 上传端点
|
|||
|
- `GET /download/:fileId` - 文件下载/访问
|
|||
|
- `GET /api/storage/resource/:fileId` - 获取文件信息
|
|||
|
- `HEAD /upload/:fileId` - 获取上传状态
|