前端实现上传华为云obs文件直传

11 阅读2分钟

官方文档BrowserJS_SDK参考_对象存储服务 OBS-华为云

技术架构概览

  • 核心依赖: 使用华为云官方SDK esdk-obs-browserjs 实现对象存储功能
  • 异步配置获取:通过getOBSConfig函数并行请求AK、SK、bucketName和endpoint等必要配置项
  • 动态配置更新:确保在上传前配置已正确加载

关键功能实现

1. 动态客户端初始化

  • 通过 createObsClient 工厂函数动态创建OBS客户端实例
  • 支持AK/SK认证信息验证,防止无效配置导致上传失败
  • 超时时间设置为1秒(timeout: 1000)

2. 客户端实例管理

  • 单例模式:通过全局变量obsClientInstance实现ObsClient单例管理
  • 懒加载机制:采用createObsClient工厂函数延迟创建客户端实例
  • 容错处理:在上传前检查并重新初始化客户端实例

3. 文件命名与校验策略

  • 实现 getSimpleFileNameWithoutExt 函数提取文件名(不含扩展名)
  • 文件名长度限制为20个字符,超过则抛出错误提示
  • 采用 原始文件名_随机UUID.扩展名 的命名规则,避免文件冲突

核心特性

1. 进度监控支持

2. 统一响应格式

  • 成功时返回 { status: 200, data: { url: string } }
  • 失败时返回 { status: 500, data: { message: string } }
  • 文件访问URL格式:${obsEndpoint.value}/${obsBucket.value}/${fileName}

代码片段

//引入华为云sdk
import ObsClient from "esdk-obs-browserjs";
import { generateUUID } from "@/utils";
import { reactive } from "vue";
import { getConfigKey } from "@/api/login/login";

let obsClientInstance = null;

const obsConfig = reactive({
  ak: null,
  sk: null,
  bucketName: null,
  endpoint: null
});

// 同步获取OBS配置
export const getOBSConfig = async () => {
  try {
    // 并行请求所有配置项
    const [skRes, endpointRes, bucketNameRes, akRes] = await Promise.all([
      getConfigKey("sys.obs.secret.key"),
      getConfigKey("sys.obs.endpoint"),
      getConfigKey("sys.obs.bucket.name"),
      getConfigKey("sys.obs.access.key")
    ]);
    
    obsConfig.sk = skRes.data;
    obsConfig.endpoint = endpointRes.data;
    obsConfig.bucketName = bucketNameRes.data;
    obsConfig.ak = akRes.data;
    
    console.log('OBS配置已获取:', obsConfig);
  } catch (error) {
    console.error('获取OBS配置失败:', error);
    throw new Error('获取OBS配置失败');
  }
};

// 创建ObsClient的工厂函数
const createObsClient = () => {
  if (!obsConfig.ak || !obsConfig.sk) {
    console.log('OBS配置未完全加载,无法创建客户端');
    return null;
  }

  let a = new ObsClient({
    access_key_id: obsConfig.ak,
    secret_access_key: obsConfig.sk,
    server: obsConfig.endpoint,
    timeout: 10000, // 增加超时时间
  });
  console.log('obsClientInstance',a)
  return a
};

// 初始化或更新ObsClient
const initOrUpdateObsClient = () => {
  obsClientInstance = createObsClient();
  console.log('obsClientInstance',obsClientInstance)
};

export const obsUploadFile = async (option,progressCallback?: (transferredAmount: number, totalAmount: number, totalSeconds: number) => void) => {
  // 检查配置是否已加载
  if (!obsConfig.ak || !obsConfig.sk || !obsConfig.bucketName || !obsConfig.endpoint) {
    console.log('OBS配置未加载,正在获取...');
    await getOBSConfig();
    
    // 确保配置已加载后再初始化客户端
    initOrUpdateObsClient();
    
    if (!obsClientInstance) {
      uni.showToast({
        title: '上传配置未加载,请重试',
        icon: 'error',
      });
      throw new Error('OBS配置未初始化,请刷新重试或重新上传文件');
    }
  }
  
  // 如果客户端实例仍然为空,尝试重新创建
  if (!obsClientInstance) {
	console.log('开始初始化配置')
    initOrUpdateObsClient();
    if (!obsClientInstance) {
      uni.showToast({
        title: '上传配置未加载,请重试',
        icon: 'error',
      });
      throw new Error('OBS客户端初始化失败');
    }
  }
  const file = option.file; //上传的文件
  const extensionName = option.name.substr(option.name.indexOf(".")); // 文件扩展名
  const fristName = getSimpleFileNameWithoutExt(option.name);
  console.log('file',file)
  if(fristName.length > 20){
    throw new Error('文件名过长,请重新上传文件');
  }
  
  const fileName = getSimpleFileNameWithoutExt(option.name) + '_' + generateUUID() + extensionName; //自定义文件名
  let blob;
    
  // 根据不同的源类型进行转换
  if (typeof file === 'string') {
    if (file.startsWith('data:')) {
      // 处理Base64字符串
      blob = base64ToBlob(file);
    } else if (file.startsWith('blob:')) {
      // 处理Blob URL
      blob = await blobUrlToBlob(file);
    } else {
      throw new Error('不支持的文件源格式');
    }
  } else if (file instanceof Blob || file instanceof File) {
    // 已经是Blob或File对象,直接使用
    blob = file;
  } else {
    throw new Error('文件源必须是Base64字符串、Blob URL、Blob或File对象');
  }
  
  if (!blob) {
    throw new Error('转换为Blob对象失败');
  }
  
  const putFileObj = async () => {
    try {
    
    const internalProgressCallback = (transferredAmount: number, totalAmount: number, totalSeconds: number) => {
        // 如果提供了外部回调函数,则执行它
        if (progressCallback && typeof progressCallback === 'function') {
          progressCallback(transferredAmount, totalAmount, totalSeconds);
        }
      };
      const result = await obsClientInstance.putObject({
        Bucket: obsConfig.bucketName, // 使用本地配置
        Key: fileName, //文件名
        SourceFile: blob, //流文件 其必须是File对象或者Blob对象
        ProgressCallback: internalProgressCallback 
      });
      
      if (result.CommonMsg.Status == 200) {
        return {
          data: {
            url: `${obsConfig.endpoint}/${obsConfig.bucketName}/${fileName}`, // 使用本地配置
          },
          status: 200,
        };
      } else {
        return {
          status: 500,
          data: {
            message: '上传失败,请重试',
          },
        };
      }
    } catch (e) {
      console.log('catch', e);
      return {
        status: 500,
        data: {
          message: '上传失败,请重试',
        },
      };
    }
  };
  
  return putFileObj();
};

// 1. 将Base64转换为Blob对象
function base64ToBlob(base64String) {
  // 分离Base64数据和类型信息
  const arr = base64String.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  
  return new Blob([u8arr], { type: mime });
}

// 2. 通过Blob URL获取Blob对象
async function blobUrlToBlob(blobUrl) {
  try {
    const response = await fetch(blobUrl);
    const blob = await response.blob();
    return blob;
  } catch (error) {
    console.error('获取Blob对象失败:', error);
    return null;
  }
}

function getSimpleFileNameWithoutExt(fileName) {
  if (!fileName) return '';
  const lastDotIndex = fileName.lastIndexOf('.');
  return lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
}

使用参考

el-upload覆盖默认上传

import { obsUploadFile } from "@/utils/uploadObsFile.js"

<el-upload ref="uploadRef" 
    class="upload-demo" 
    'http-request': uploadAction,
    headers: item.uploadHeaders || {
        Authorization: 'Bearer ' + getToken(),
    },
    :auto-upload="false" > 
    <template #trigger>
      <el-button type="primary">select file</el-button> 
    </template> 
    <el-button class="ml-3" type="success" @click="submitUpload"> upload to server </el-button> <template #tip> 
      <div class="el-upload__tip"> jpg/png files with a size less than 500kb </div> 
    </template> 
</el-upload>

const uploadAction = async (option) =>{
      const result = await obsUploadFile(option,(transferredAmount, totalAmount, totalSeconds) => {
        // 计算上传速率和进度
        const rate = transferredAmount * 1.0 / totalSeconds / 1024 / 1024;
        const percentage = transferredAmount * 100.0 / totalAmount;
        
        loadingProgressText.value = `上传进度: ${percentage.toFixed(2)}% | 上传速率: ${rate.toFixed(2)} M/s`
        // 在这里可以更新UI显示进度
        console.log(`上传进度: ${percentage.toFixed(2)}%`);
        console.log(`上传速率: ${rate.toFixed(2)} M/s`);
        // 更新进度条
        // updateProgressBar(percentage);
      });
      if(result.status != 200){
        ElMessage.error('上传失败')
      }
      return result;
    }