fenghuo/apps/web/docs/UPLOAD_HOOK_USAGE.md

288 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` - 获取上传状态