Skip to content

OSS 上传工具

OSS 上传工具是一个用于处理阿里云对象存储服务(OSS)文件上传的实用工具类。它提供了文件上传、URL 签名和凭证管理等功能。

功能特点

  • ✅ 文件上传进度跟踪
  • ✅ 自动凭证管理和刷新
  • ✅ 签名 URL 生成
  • ✅ 自定义 OSS 错误处理
  • 多配置 Client 缓存(支持不同 endpoint/region/bucket)
  • 分片上传(支持大文件上传)
  • 智能缓存管理(15分钟自动过期)

安装

OSS 上传工具已包含在 utils 包中,无需额外安装。

使用方法

基础文件上传

typescript
import { uploadFile } from '@/utils/oss-uploader'
import { apiClient } from '@/api'
import { ossUrls } from '@/api/urls'

const handleUpload = async (file: File) => {
  try {
    const result = await uploadFile({
      file,
      getCredentials: async () => {
        const { res, error } = await apiClient(ossUrls.getSts, {
          file_type: 'document',
          filename: file.name,
          auto_create_file: true,
          knowledge_base_id: 'kb_123',
        })
        if (error) {
          throw new OssError(error.msg)
        }
        
        // 构建回调变量
        const callbackVars = {
          'x:callback_id': res.callback_id,
          'x:file_type': res.file_type
        }

        // 构建 headers(处理 callback 可能为空的情况)
        const headers: Record<string, string> = {}
        if (res.callback) {
          headers['x-oss-callback'] = res.callback
          headers['x-oss-callback-var'] = btoa(JSON.stringify(callbackVars))
        }

        return {
          headers,
          path: res.object_key || undefined,
          bucket: res.bucket || undefined,
          endpoint: res.endpoint,
          region: res.region,
          accessKeyId: res.access_key_id,
          accessKeySecret: res.access_key_secret,
          securityToken: res.security_token,
        }
      },
      onProgress: (progress) => {
        console.log(`上传进度: ${progress.percent}%`)
      }
    })
    console.log('上传成功:', result)
  } catch (error) {
    console.error('上传失败:', error)
  }
}

使用自定义 Endpoint(CDN 加速)

typescript
import { uploadFile } from '@/utils/oss-uploader'

const handleUploadWithCDN = async (file: File) => {
  try {
    const result = await uploadFile({
      file,
      getCredentials: async () => {
        const response = await fetch('/api/oss/sts')
        const data = await response.json()
        
        return {
          accessKeyId: data.access_key_id,
          accessKeySecret: data.access_key_secret,
          securityToken: data.security_token,
          endpoint: 'https://cdn.example.com', // 自定义 CDN 域名
          bucket: 'my-bucket',
          cname: true, // 告诉 OSS SDK 这是 CNAME 域名
          path: 'images',
        }
      },
      onProgress: ({ percent }) => {
        console.log(`上传进度: ${percent}%`)
      }
    })
    console.log('上传成功:', result)
  } catch (error) {
    console.error('上传失败:', error)
  }
}

覆盖后端返回的配置

typescript
import { uploadFile } from '@/utils/oss-uploader'

const handleUpload = async (file: File) => {
  const result = await uploadFile({
    file,
    // 这里的 path 和 headers 会覆盖 getCredentials 返回的配置
    path: 'custom/path',
    headers: {
      'x-oss-callback': customCallback,
    },
    getCredentials: async () => {
      const data = await fetchOssConfig()
      return {
        accessKeyId: data.access_key_id,
        accessKeySecret: data.access_key_secret,
        securityToken: data.security_token,
        // 这些配置会被上面的参数覆盖
        path: data.object_key,
        headers: data.headers,
      }
    }
  })
}

生成签名 URL

typescript
import { generateSignedUrl } from '@/utils/oss-uploader'

const getFileUrl = async (objectKey: string) => {
  try {
    const url = await generateSignedUrl({
      objectKey,
      expires: 3600, // 可选,默认 3600 秒
      getCredentials: async () => {
        const response = await fetch('/api/oss/sts')
        const data = await response.json()
        
        return {
          accessKeyId: data.access_key_id,
          accessKeySecret: data.access_key_secret,
          securityToken: data.security_token,
          endpoint: data.endpoint, // 可选
          bucket: data.bucket,
          region: data.region,
        }
      }
    })
    return url
  } catch (error) {
    console.error('生成签名 URL 失败:', error)
    throw error
  }
}

缓存管理

typescript
import { OssUploader } from '@/utils/oss-uploader'

// 清理过期的 client 缓存(可选,系统会自动管理)
OssUploader.clearExpiredClients()

// 清空所有缓存(例如用户登出时)
OssUploader.clearAllClients()

API 参考

uploadFile(options: UploadFileOptions)

使用指定配置上传文件到 OSS。

参数

  • options: UploadFileOptions
    • file: File - 要上传的文件(必填)
    • getCredentials: () => Promise<OssCredentialsConfig | null> - 获取 OSS 凭证和配置的函数(必填)
      • 返回值包含:
        • accessKeyId: string - 访问密钥 ID(必填)
        • accessKeySecret: string - 访问密钥(必填)
        • securityToken: string - 安全令牌(必填)
        • bucket?: string - OSS bucket 名称(可选)
        • region?: string - OSS 区域(可选)
        • endpoint?: string - 自定义 OSS endpoint,支持 CDN 域名(可选)
        • cname?: boolean - 是否为 CNAME 域名(可选)
        • secure?: boolean - 是否使用 HTTPS(可选)
        • timeout?: number - 请求超时时间(可选)
        • path?: string - 上传路径/对象键(可选)
        • headers?: Record<string, string> - 自定义 headers(可选)
    • path?: string - 自定义上传路径,会覆盖 getCredentials 返回的 path(可选)
    • headers?: Record<string, string> - 自定义 headers,会覆盖 getCredentials 返回的 headers(可选)
    • onProgress?: (progress: { percent: number }) => void - 进度回调函数(可选)

返回值

  • Promise<OSS.MultipartUploadResult> - OSS 上传结果

示例

typescript
// 基础上传(所有配置由后端返回)
await uploadFile({
  file: myFile,
  getCredentials: async () => {
    const res = await fetch('/api/oss/sts')
    const data = await res.json()
    return {
      accessKeyId: data.access_key_id,
      accessKeySecret: data.access_key_secret,
      securityToken: data.security_token,
      bucket: data.bucket,
      region: data.region,
      endpoint: data.endpoint,
      cname: true,
      path: data.object_key,
      headers: data.headers,
    }
  }
})

// 覆盖后端配置
await uploadFile({
  file: myFile,
  path: 'custom/path', // 覆盖后端返回的 path
  getCredentials: async () => ({ /* ... */ })
})

generateSignedUrl(options: GenerateSignedUrlOptions)

生成用于访问 OSS 对象的签名 URL。

参数

  • options: GenerateSignedUrlOptions
    • objectKey: string - OSS 中的对象键(必填)
    • getCredentials: () => Promise<OssCredentialsConfig | null> - 获取 OSS 凭证和配置的函数(必填)
    • expires?: number - URL 过期时间(秒),默认 3600(可选)

返回值

  • Promise<string> - 签名 URL

示例

typescript
// 基础用法
const url = await generateSignedUrl({
  objectKey: 'path/to/file.jpg',
  getCredentials: async () => {
    const res = await fetch('/api/oss/sts')
    const data = await res.json()
    return {
      accessKeyId: data.access_key_id,
      accessKeySecret: data.access_key_secret,
      securityToken: data.security_token,
      bucket: data.bucket,
      region: data.region,
      endpoint: data.endpoint,
    }
  }
})

// 自定义过期时间
const url = await generateSignedUrl({
  objectKey: 'path/to/file.jpg',
  expires: 7200, // 2 小时
  getCredentials: async () => ({ /* ... */ })
})

OssUploader.clearExpiredClients()

清理过期的 client 缓存(过期时间为 15 分钟)。

返回值

  • void

示例

typescript
import { OssUploader } from '@/utils/oss-uploader'

// 定期清理过期缓存
setInterval(() => {
  OssUploader.clearExpiredClients()
}, 5 * 60 * 1000) // 每 5 分钟清理一次

OssUploader.clearAllClients()

清空所有 client 缓存。

返回值

  • void

示例

typescript
import { OssUploader } from '@/utils/oss-uploader'

// 用户登出时清空缓存
const handleLogout = () => {
  OssUploader.clearAllClients()
  // 其他登出逻辑...
}

错误处理

该工具包含自定义的 OssError 类用于错误处理。所有操作都会将错误包装在此类中以实现一致的错误处理:

typescript
try {
  await uploadFile(/* ... */)
} catch (error) {
  if (error instanceof OssError) {
    // 处理 OSS 特定错误
    console.error('OSS 操作失败:', error.message)
  } else {
    // 处理其他错误
    console.error('意外错误:', error)
  }
}

配置说明

默认配置包括:

  • 地域:oss-cn-hangzhou
  • Bucket:yao-file-daily
  • 安全传输:启用
  • 超时时间:60000ms
  • 已启用 CORS 并配置相应的请求头

安全注意事项

  1. 永远不要在应用程序中硬编码凭证
  2. 确保实现适当的凭证轮换机制
  3. 使用最小必要的 OSS 权限
  4. Token 默认过期时间为 15 分钟

高级用法

多配置场景

工具会自动为不同的配置组合创建独立的 client 实例:

typescript
// 这三个上传会使用不同的 client 实例
await uploadFile({
  file: file1,
  endpoint: 'https://cdn1.example.com',
  bucket: 'bucket-1',
  getCredentials: getCredentials1
})

await uploadFile({
  file: file2,
  endpoint: 'https://cdn2.example.com',
  bucket: 'bucket-2',
  getCredentials: getCredentials2
})

await uploadFile({
  file: file3,
  region: 'oss-cn-beijing',
  bucket: 'bucket-3',
  getCredentials: getCredentials3
})

与 STS 服务集成

推荐的后端 STS 接口实现:

typescript
import { apiClient } from '@/api'
import { ossUrls } from '@/api/urls'
import { OssError } from '@/utils/oss-uploader'

// 前端调用
const getCredentials = async () => {
  const { res, error } = await apiClient(ossUrls.getSts, {
    file_type: 'image',
    filename: file.name,
    auto_create_file: true,
    knowledge_base_id: 'kb_123',
  })
  
  if (error) {
    throw new OssError(error.msg)
  }
  
  // 构建回调变量
  const callbackVars = {
    'x:callback_id': res.callback_id,
    'x:file_type': res.file_type
  }

  // 构建 headers(处理 callback 可能为空的情况)
  const headers: Record<string, string> = {}
  if (res.callback) {
    headers['x-oss-callback'] = res.callback
    headers['x-oss-callback-var'] = btoa(JSON.stringify(callbackVars))
  }

  return {
    headers,
    path: res.object_key || undefined,
    bucket: res.bucket || undefined,
    endpoint: res.endpoint,
    region: res.region,
    accessKeyId: res.access_key_id,
    accessKeySecret: res.access_key_secret,
    securityToken: res.security_token,
  }
}

// 使用
await uploadFile({
  file,
  getCredentials
})

大文件上传优化

工具使用分片上传(multipartUpload),自动处理大文件:

typescript
// 上传大文件(如视频)
await uploadFile({
  file: largeVideoFile, // 例如 500MB
  path: 'videos',
  timeout: 300000, // 增加超时时间
  getCredentials: async () => ({ /* ... */ }),
  onProgress: ({ percent }) => {
    // 实时显示进度
    updateProgressBar(percent)
  }
})

常见问题

Q: 为什么我的 endpoint 配置没有生效?

A: 在 v2.0 之前,工具使用单例模式缓存 client,导致后续的配置被忽略。现在已修复,每个不同的配置组合都会创建独立的 client 实例。

Q: 如何判断使用哪个 client?

A: 工具根据 endpoint-region-bucket 组合生成缓存 key:

  • 如果指定了 endpoint,使用 endpoint
  • 否则使用 region
  • 不同的 bucket 也会创建不同的 client

Q: client 缓存会占用太多内存吗?

A: 不会。缓存有以下机制:

  • 每个 client 15 分钟后自动过期
  • 可以手动调用 clearExpiredClients() 清理
  • 用户登出时调用 clearAllClients() 清空

Q: 支持断点续传吗?

A: 支持。工具使用 multipartUpload 方法,在网络中断后可以继续上传未完成的分片。

Q: 如何处理上传失败?

A: 建议实现重试机制:

typescript
async function uploadWithRetry(file: File, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await uploadFile({
        file,
        getCredentials: async () => ({ /* ... */ })
      })
    } catch (error) {
      if (i === maxRetries - 1) throw error
      console.log(`上传失败,重试 ${i + 1}/${maxRetries}`)
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
    }
  }
}

Q: 可以同时上传多个文件吗?

A: 可以。每个上传都是独立的:

typescript
const files = [file1, file2, file3]

// 并行上传
const results = await Promise.all(
  files.map(file => uploadFile({
    file,
    getCredentials: async () => ({ /* ... */ })
  }))
)

// 或串行上传
for (const file of files) {
  await uploadFile({
    file,
    getCredentials: async () => ({ /* ... */ })
  })
}

最佳实践

  1. 使用 STS 临时凭证:永远不要在前端硬编码 AccessKey
  2. 处理上传进度:提供 onProgress 回调以改善用户体验
  3. 实现错误处理:捕获并处理 OssError,必要时实现重试机制
  4. 合理设置路径:使用有意义的路径前缀便于文件管理
  5. CDN 加速:生产环境建议使用 CDN 域名作为 endpoint
  6. 清理缓存:用户登出时调用 clearAllClients() 清空敏感信息
  7. 监控凭证过期:确保 getCredentials 函数能正确获取新凭证
  8. 大文件优化:上传大文件时适当增加 timeout

高级特性

自定义 Headers(OSS 回调支持)

工具支持在 getCredentials 中返回自定义 headers,用于 OSS 回调等场景:

typescript
import { apiClient } from '@/api'
import { ossUrls } from '@/api/urls'
import { OssError } from '@/utils/oss-uploader'

// 推荐方式:在 getCredentials 中返回 headers
await uploadFile({
  file: myFile,
  getCredentials: async () => {
    const { res, error } = await apiClient(ossUrls.getSts, {
      file_type: 'document',
      filename: myFile.name,
      auto_create_file: true,
    })
    
    if (error) {
      throw new OssError(error.msg)
    }
    
    // 构建回调变量
    const callbackVars = {
      'x:callback_id': res.callback_id,
      'x:file_type': res.file_type
    }

    // 构建 headers(处理 callback 可能为空的情况)
    const headers: Record<string, string> = {}
    if (res.callback) {
      headers['x-oss-callback'] = res.callback
      headers['x-oss-callback-var'] = btoa(JSON.stringify(callbackVars))
    }

    return {
      headers,
      path: res.object_key,
      bucket: res.bucket,
      endpoint: res.endpoint,
      region: res.region,
      accessKeyId: res.access_key_id,
      accessKeySecret: res.access_key_secret,
      securityToken: res.security_token,
    }
  }
})

智能路径判断

工具会根据以下优先级确定上传路径:

  1. 带后缀的 path 参数(最高优先级)

    • 如果 path 参数包含文件后缀(如 .pdf.jpg),直接使用
    • 例如:path: 'documents/report-2024.pdf'
  2. 目录 + 文件名组合(默认方式)

    • 如果 path 是目录路径,会自动拼接文件名
    • 例如:path: 'documents'documents/filename.pdf
typescript
// 场景1:path 是完整路径(带后缀)
await uploadFile({
  file: myFile,
  path: 'documents/report-2024.pdf', // ✅ 直接使用这个完整路径
  getCredentials: async () => ({ ...credentials })
})

// 场景2:path 是目录(传统方式)
await uploadFile({
  file: myFile, // 假设文件名是 invoice.pdf
  path: 'documents', // ✅ 会拼接成 documents/invoice.pdf
  getCredentials: async () => ({ ...credentials })
})

接口说明

typescript
// OSS 凭证和配置接口(由 getCredentials 返回)
interface OssCredentialsConfig {
  // 必需的凭证
  accessKeyId: string        // 必填:访问密钥 ID
  accessKeySecret: string    // 必填:访问密钥
  securityToken: string      // 必填:安全令牌(STS)

  // OSS 配置
  bucket?: string            // 可选:OSS bucket 名称
  region?: string            // 可选:OSS 区域
  endpoint?: string          // 可选:自定义 OSS endpoint(支持 CDN 域名)
  cname?: boolean            // 可选:是否为 CNAME 域名
  secure?: boolean           // 可选:是否使用 HTTPS
  timeout?: number           // 可选:请求超时时间(毫秒)

  // 上传相关配置
  path?: string              // 可选:上传路径/对象键
  headers?: Record<string, string>  // 可选:自定义 headers(如 OSS 回调)
}

// uploadFile 的参数接口
interface UploadFileOptions {
  file: File                 // 必填:要上传的文件
  getCredentials: () => Promise<OssCredentialsConfig | null>  // 必填:获取凭证和配置的函数
  onProgress?: (p: { percent: number }) => void  // 可选:进度回调

  // 以下参数可以覆盖 getCredentials 返回的配置
  path?: string              // 可选:上传路径(会覆盖 getCredentials 返回的 path)
  headers?: Record<string, string>  // 可选:自定义 headers(会覆盖 getCredentials 返回的 headers)
}

// generateSignedUrl 的参数接口
interface GenerateSignedUrlOptions {
  objectKey: string          // 必填:OSS 中的对象键
  getCredentials: () => Promise<OssCredentialsConfig | null>  // 必填:获取凭证和配置的函数
  expires?: number           // 可选:URL 过期时间(秒),默认 3600
}

更新日志

v3.0.0 (2025-12-16)

  • 🔥 重大变更:重新设计 API,getCredentials 现在返回完整的 OSS 配置
  • ✨ 新增 OssCredentialsConfig 接口,支持在 getCredentials 中返回所有 OSS 配置
  • ✨ 新增参数优先级机制:uploadFile 的直接参数可覆盖 getCredentials 返回的配置
  • ✨ 优化 generateSignedUrl API,改为对象参数形式
  • 📝 完善文档和使用示例,更符合实际使用场景
  • 🎯 更好地支持后端统一返回 OSS 配置的场景

v2.1.0 (2025-12-16)

  • ✨ 新增 OSS 回调 headers 支持
  • ✨ 新增智能路径判断(支持后端返回 object_key)
  • ✨ 支持 path 参数带文件后缀时直接使用
  • 📝 完善 OssCredentials 接口文档

v2.0.0 (2025-12-16)

  • ✨ 新增多配置 client 缓存机制
  • 🐛 修复自定义 endpoint 不生效的问题
  • ✨ 新增 clearExpiredClients()clearAllClients() 方法
  • 📝 完善文档和使用示例
  • ⚡️ 优化缓存管理策略

v1.0.0

  • 🎉 初始版本发布
  • ✨ 支持文件上传和进度跟踪
  • ✨ 支持签名 URL 生成
  • ✨ 自动凭证管理和刷新

Released under the MIT License.