一、上传功能的整体流程
整个上传流程可以分为前端选择文件、服务端获取 OSS 配置、服务端上传文件、返回文件地址四个步骤。
简单流程如下:
用户选择文件
-> 前端生成本地预览
-> 点击上传
-> 调用服务端 uploadToOSS 方法
-> 服务端调用 getOss 获取 OSS 临时凭证
-> 服务端使用 ali-oss 上传文件
-> 上传成功后返回 OSS 文件访问地址
-> 前端展示上传结果
代码在最下面!代码在最下面!代码在最下面!
二、为什么使用 Server Action 上传
在 Next.js 中,如果上传方法文件顶部添加:
javascript
'use server'
说明这个文件中的方法会在服务端执行。
前端组件虽然可以调用这个方法,但实际上传逻辑不会运行在浏览器中,而是由 Next.js 服务端处理。
这样做的好处是:
-
OSS 密钥不会暴露到浏览器
-
上传逻辑集中在服务端,安全性更高
-
可以在服务端统一校验文件类型和大小
-
可以在服务端统一处理错误和日志
-
前端代码更简单,只需要关心文件选择和结果展示
三、前端上传页面主要负责什么
前端页面一般只负责用户交互,不直接持有 OSS 密钥。
主要职责包括:
-
选择文件 :通过
input type="file"获取用户选择的图片或视频 -
文件校验:判断文件类型是否符合要求,例如只允许图片或视频
-
本地预览 :使用
URL.createObjectURL(file)生成临时预览地址 -
上传触发:点击按钮后调用服务端上传方法
-
状态展示:显示上传中、上传成功、上传失败等状态
-
结果展示:上传成功后展示 OSS 文件地址
前端不会直接操作 OSS 永久密钥,这样可以避免敏感信息泄露。
四、服务端上传方法主要负责什么
服务端上传方法负责真正的 OSS 上传逻辑。
主要职责包括:
-
接收前端传来的 File 文件对象
-
校验文件是否为空
-
校验文件类型是否合法
-
调用接口获取 OSS 临时凭证
-
校验 OSS 配置是否完整
-
创建 OSS 客户端
-
生成唯一文件名
-
拼接 OSS 存储路径
-
将 File 转换为 Buffer
-
调用 OSS SDK 上传
-
返回最终文件访问地址
服务端上传成功后,会返回类似这样的结果:url就是上传到阿里云的文件地址
javascript
{
success: true,
url: "https://xxx.oss-cn-xxx.aliyuncs.com/path/file.png"
}
五、OSS 临时凭证的作用
上传前,服务端会先调用
getOss 获取 OSS 配置。
通常配置中会包含:
-
region:OSS 区域
-
bucket:OSS 存储桶名称
-
AccessKeyId:临时访问 Key
-
AccessKeySecret:临时访问 Secret
-
SecurityToken:STS 临时 Token
-
keyPrefix:上传目录前缀
-
url:文件访问域名
这些信息用于创建 OSS 客户端,并完成文件上传。
这里推荐使用 STS 临时凭证,而不是长期有效的 AccessKey。
原因是:
-
临时凭证有效期短
-
权限可以限制到指定 bucket 或目录
-
安全性更高
-
即使泄露,风险也更可控
六、文件名为什么要重新生成
上传文件时,不建议直接使用用户原始文件名。
一般会重新生成文件名,例如:时间戳_随机字符串.扩展名
这样做有几个好处:
-
避免文件名重复导致覆盖
-
避免中文、空格、特殊字符造成路径问题
-
便于按时间排查上传记录
-
减少用户原始文件名泄露风险
最终 OSS 路径通常是:keyPrefix + fileName
例如:uploads/20260523_abc123.png
七、为什么要把 File 转成 Buffer
在服务端使用 ali-oss 上传时,一般需要传入可被 Node.js 处理的数据类型。前端传来的 File 对象可以通过:file.arrayBuffer()转换成 ArrayBuffer,再通过:Buffer.from()
转换成 Node.js 的 Buffer。
最终上传给 OSS 的就是这个 Buffer。
八、服务端上传和客户端直传的区别
服务端上传
服务端上传的流程是:浏览器 -> Next.js 服务端 -> 阿里云 OSS
优点:
-
安全性高
-
密钥不暴露
-
逻辑统一
-
适合管理后台、头像、Logo、二维码等中小文件上传
缺点:
-
文件会经过自己的服务器
-
服务器带宽和内存压力更大
-
大文件可能受到 Server Action 上传大小限制
客户端直传
客户端直传的流程是:浏览器 -> 阿里云 OSS
前端先向后端请求临时 STS 凭证,然后浏览器使用临时凭证直接上传 OSS。
优点:
-
文件不经过业务服务器
-
上传速度更快
-
服务器压力更小
-
更适合大视频、大文件上传
缺点:
-
前端会拿到临时凭证
-
必须严格限制 STS 权限
-
需要控制凭证过期时间和上传目录
-
前端逻辑相对复杂
九、什么时候需要配置 bodySizeLimit
如果使用的是 Next.js Server Action 上传文件,大文件会先传到 Next.js 服务端。
Next.js 对 Server Action 的请求体大小有限制。
如果上传大图片或视频,可能出现类似:Body exceeded 1 MB limit
这时需要在 next.config.ts 中配置:experimental.serverActions.bodySizeLimit
例如设置为:200mb
适用场景:
-
上传视频
-
上传大图
-
文件超过默认 Server Action 限制
-
调用 Server Action 时直接传 File 对象
如果只是上传 Logo、二维码、小图标,一般文件较小,可能不需要额外配置。
示例代码
1、page.tsx,我这里定义的是客户端组件,在界面最顶部加上了'use client'
javascript
'use client'
import React, { useEffect, useState } from 'react';
import styles from '@/app/[lang]/[domain]/modelRun/styles/VideoGenerator.module.css';
import { uploadToOSS } from '@/utils/aliyOssUpload';
import { useToast } from '@/components/Toast/ToastContainer';
const VideoGenerator: React.FC = () => {
// 选择的文件对象,支持图片或视频
const [file, setFile] = useState<File | null>(null);
// 文件本地预览地址,用于上传前预览
const [previewUrl, setPreviewUrl] = useState('');
// 上传成功后 OSS 返回的文件访问地址
const [ossUrl, setOssUrl] = useState('');
// 当前文件类型,用于决定预览时显示 img 还是 video
const [fileType, setFileType] = useState<'image' | 'video' | null>(null);
// 上传中状态,用于禁用按钮和展示上传中
const [isUploading, setIsUploading] = useState(false);
// Toast 提示
const toast = useToast();
/**
* 处理文件选择
*
* 功能:
* 1. 校验文件是否存在
* 2. 校验文件类型是否为图片或视频
* 3. 清理旧的本地预览 URL
* 4. 保存新的文件对象
* 5. 创建新的本地预览 URL
*/
const handleFileChange = (selectedFile: File | null) => {
if (!selectedFile) {
return;
}
const isImage = selectedFile.type.startsWith('image/');
const isVideo = selectedFile.type.startsWith('video/');
if (!isImage && !isVideo) {
toast.error('只支持图片或视频文件');
return;
}
if (previewUrl && previewUrl.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl);
}
setFile(selectedFile);
setPreviewUrl(URL.createObjectURL(selectedFile));
setOssUrl('');
setFileType(isImage ? 'image' : 'video');
};
/**
* 清除当前选择的文件
*
* 功能:
* 1. 释放本地预览 URL
* 2. 清空文件对象
* 3. 清空 OSS 地址
* 4. 清空文件类型
* 5. 重置 input value,保证可以重复选择同一个文件
*/
const handleClearFile = () => {
if (previewUrl && previewUrl.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl);
}
setFile(null);
setPreviewUrl('');
setOssUrl('');
setFileType(null);
const input = document.getElementById('oss-upload-input') as HTMLInputElement;
if (input) {
input.value = '';
}
};
/**
* 上传文件到 OSS
*
* 功能:
* 1. 判断是否已经选择文件
* 2. 调用 uploadToOSS Server Action
* 3. 上传成功后保存 OSS 地址
* 4. 上传失败时提示错误信息
*/
const handleUpload = async () => {
if (!file) {
toast.error('请先选择文件');
return;
}
setIsUploading(true);
try {
const result = await uploadToOSS(file);
if (result.success && result.url) {
setOssUrl(result.url);
toast.success('上传成功');
} else {
toast.error(result.error || '上传失败');
}
} catch (error) {
console.error('上传异常:', error);
toast.error(error instanceof Error ? error.message : '上传失败');
} finally {
setIsUploading(false);
}
};
/**
* 组件卸载时释放本地预览 URL
*
* 作用:
* - URL.createObjectURL 会占用浏览器内存
* - 组件卸载时需要 revoke,避免内存泄漏
*/
useEffect(() => {
return () => {
if (previewUrl && previewUrl.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl);
}
};
}, [previewUrl]);
return (
<div className={styles.container}>
<div className={styles.inputSection}>
<div className={styles.sectionHeader}>
<h3 className={styles.sectionTitle}>OSS 上传测试</h3>
</div>
<div className={styles.formContent}>
<div className={styles.formGroup}>
<label className={styles.label}>选择图片或视频</label>
<div className={styles.uploadSection}>
{file && previewUrl ? (
<div className={styles.imagePreviewContainer}>
{fileType === 'image' ? (
<img
src={previewUrl}
alt="文件预览"
className={styles.imagePreview}
/>
) : (
<video
src={previewUrl}
className={styles.imagePreview}
controls
style={{
maxHeight: '200px',
width: '100%',
objectFit: 'contain',
}}
/>
)}
<div className={styles.imageActions}>
<span className={styles.fileName}>
{file.name}
</span>
<button
type="button"
className={styles.clearButton}
onClick={handleClearFile}
title="清除文件"
>
✕
</button>
</div>
</div>
) : (
<div className={styles.uploadBox}>
<input
id="oss-upload-input"
type="file"
accept="image/*,video/*"
className={styles.fileInput}
onChange={(e) => handleFileChange(e.target.files?.[0] || null)}
/>
<label htmlFor="oss-upload-input" className={styles.uploadLabel}>
<svg
className={styles.uploadIconSvg}
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 4L12 16M12 4L8 8M12 4L16 8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 17V19C4 20.1046 4.89543 21 6 21H18C19.1046 21 20 20.1046 20 19V17"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
<span className={styles.uploadMainText}>
点击选择文件
</span>
<span className={styles.uploadSubText}>
支持图片或视频
</span>
</label>
</div>
)}
</div>
</div>
<button
className={styles.submitButton}
onClick={handleUpload}
disabled={isUploading || !file}
>
<span className={styles.buttonIcon}>▶</span>
{isUploading ? '上传中...' : '上传到 OSS'}
</button>
{ossUrl && (
<div className={styles.formGroup}>
<label className={styles.label}>OSS 文件地址</label>
<textarea
className={styles.textarea}
value={ossUrl}
readOnly
/>
<a
href={ossUrl}
target="_blank"
rel="noopener noreferrer"
style={{
color: '#635bff',
marginTop: '8px',
display: 'inline-block',
}}
>
打开文件
</a>
</div>
)}
</div>
</div>
</div>
);
};
export default VideoGenerator;
2、上传实现js文件:aliOssUpload.ts
javascript
'use server'
import { getOss } from '@/api/common';
import OSS from 'ali-oss';
export interface UploadResult {
// 是否上传成功
success: boolean;
// 上传成功后返回的 OSS 文件访问地址
url?: string;
// 上传失败时返回的错误信息
error?: string;
}
/**
* 上传单个文件到阿里云 OSS
*
* 执行环境:
* - 由于文件顶部声明了 'use server'
* - 所以该方法会作为 Server Action 在服务端执行
* - OSS 临时密钥不会暴露到浏览器端
*
* 上传流程:
* 1. 校验文件是否有效
* 2. 校验文件类型是否为图片或视频
* 3. 调用 getOss 获取 OSS 临时上传配置
* 4. 校验 OSS 配置是否完整
* 5. 创建 ali-oss 客户端
* 6. 生成唯一文件名和 OSS 存储路径
* 7. 将 File 转为 Buffer
* 8. 上传到 OSS
* 9. 拼接并返回文件访问地址
*
* @param file 要上传的文件对象
* @param userId 用户 ID,可选,当前方法中暂未使用
* @param sourceType 资源类型,可选,当前方法中暂未使用
* @returns 上传结果,成功时包含 url,失败时包含 error
*/
export async function uploadToOSS(
file: File,
userId?: string,
sourceType?: string
): Promise<UploadResult> {
try {
// 校验文件对象是否存在,以及文件大小是否有效
if (!file || file.size === 0) {
return { success: false, error: '无效的文件对象' };
}
// 限制只允许上传图片或视频文件
if (!file.type.startsWith('image/') && !file.type.startsWith('video/')) {
return { success: false, error: '只支持图片或视频文件' };
}
// 开始获取 OSS 上传所需的临时配置
console.log('[OSS] 准备调用 getOss');
// 调用后端接口获取 OSS 临时凭证、bucket、region、上传目录和访问域名
const response = await getOss();
// 打印 OSS 配置接口返回的关键信息,方便排查配置问题
console.log('[OSS] getOss response:', {
code: response?.code,
msg: response?.msg,
bucket: response?.data?.bucket,
});
// 判断 OSS 配置接口是否返回成功
if (response.code !== 0 || !response.data) {
console.error('[OSS] 获取配置失败:', response);
return {
success: false,
error: response.msg || '获取 OSS 配置失败',
};
}
// OSS 配置信息,包含 region、bucket、临时密钥、上传目录、访问域名等
const ossConfig = response.data as any;
// 校验 OSS 上传必须字段是否完整
const missing = ['region', 'bucket', 'AccessKeyId', 'AccessKeySecret', 'SecurityToken', 'keyPrefix', 'url']
.find(k => !ossConfig[k]);
// 如果缺少必要字段,直接返回错误
if (missing) {
console.error(`[OSS] 配置缺失: ${missing}`);
return { success: false, error: `OSS ${missing} 未设置` };
}
// 创建 OSS 客户端实例,使用的是 STS 临时凭证
const client = new OSS({
region: ossConfig.region,
accessKeyId: ossConfig.AccessKeyId,
accessKeySecret: ossConfig.AccessKeySecret,
stsToken: ossConfig.SecurityToken,
bucket: ossConfig.bucket,
secure: true,
});
// 获取原文件扩展名,例如 .png、.jpg、.mp4
const ext = file.name.includes('.')
? file.name.substring(file.name.lastIndexOf('.'))
: '';
// 生成唯一文件名,避免覆盖 OSS 上已有文件
const fileName = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
// 拼接 OSS 存储路径,keyPrefix 是后端返回的上传目录前缀
const filePath = `${ossConfig.keyPrefix || ''}${fileName}`;
// 将浏览器传来的 File 对象转换为 Node.js Buffer,供 ali-oss 服务端上传使用
const buffer = Buffer.from(await file.arrayBuffer());
// 上传文件到 OSS,并设置文件 Content-Type
const result = await client.put(filePath, buffer, {
headers: { 'Content-Type': file.type },
});
// 打印上传成功后的 OSS 对象名称
console.log('[OSS] 上传成功:', result.name);
// 去掉访问域名末尾的 /,避免最终 URL 出现双斜杠
const baseUrl = String(ossConfig.url).replace(/\/$/, '');
// 返回完整文件访问地址
return {
success: true,
url: `${baseUrl}/${filePath}`,
};
} catch (error) {
// 捕获上传过程中的异常,例如 OSS SDK 报错、网络异常等
console.error('[OSS] 上传异常:', error);
return {
success: false,
error: error instanceof Error ? error.message : '上传失败',
};
}
}
/**
* 批量上传文件到阿里云 OSS
*
* 上传方式:
* - 内部复用 uploadToOSS
* - 使用 Promise.all 并发上传多个文件
*
* @param files 要上传的文件数组
* @param userId 用户 ID,可选,会继续传递给 uploadToOSS
* @param sourceType 资源类型,可选,会继续传递给 uploadToOSS
* @returns 每个文件对应的上传结果数组
*/
export async function uploadMultipleToOSS(
files: File[],
userId?: string,
sourceType?: string
): Promise<UploadResult[]> {
// 并发上传所有文件,并返回每个文件的上传结果
return Promise.all(files.map(f => uploadToOSS(f, userId, sourceType)));
}
3、如果需要在nextjs的服务端上传大文件,则需要在根目录文件next.config.ts内加入以下配置:
javascript
// 上传阿里云文件限制大小
experimental: {
serverActions: {
bodySizeLimit: '200mb',
},
},
注意:如果在aliOssUpload.js中的顶部加入:'use server'则上传调用在服务端进行上传,否则在客户端上传
如果使用 use server,上传逻辑会运行在服务端,安全性更高,适合中小文件上传。如果需要上传大文件,可以通过调整 bodySizeLimit 临时解决,但从架构上看,客户端直传 OSS 会更合适。