uni-app VOD 与 COS 选型、开发笔记

1. 项目背景与目标

在 uni-app 开发过程中,需要实现媒体文件(图片、视频)的上传功能,主要涉及腾讯云 VOD(视频点播)和 COS(对象存储)两种方案。

2. 技术选型与尝试过程

2.1 第一阶段:VOD SDK 尝试

2.1.1 初始方案:vod-js-sdk-v6

最初选择腾讯云 VOD SDK vod-js-sdk-v6 进行视频上传:

javascript 复制代码
import TcVod from 'vod-js-sdk-v6'

const tcVod = new TcVod({
  getSignature: () => {
    return new Promise((resolve, reject) => {
      uni.request({
        url: "后端签名接口",
        method: "POST",
        success: function (res) {
          const sig = res.data.data.signature;
          if (sig) {
            resolve(sig);
          } else {
            reject(new Error('签名不存在'));
          }
        }
      });
    });
  }
});
2.1.2 遇到的第一个错误
复制代码
TypeError: Cannot read property 'userAgent' of undefined

问题分析vod-js-sdk-v6在初始化时会访问 navigator.userAgent,但在 uni-app 的某些运行环境中该对象不存在。

尝试解决

javascript 复制代码
// 尝试模拟浏览器环境
const globalObj = (typeof window !== 'undefined') ? window : (typeof global !== 'undefined' ? global : this);
globalObj.navigator = globalObj.navigator || {};
globalObj.navigator.userAgent = globalObj.navigator.userAgent || 'app-plus';
2.1.3 遇到的第二个错误
复制代码
文件查找失败:'axios' at node_modules\.store\vod-js-sdk-v6@1.7.1-beta.1\node_modules\vod-js-sdk-v6\lib\src\uploader.js:161
文件查找失败:'js-sha1' at node_modules\.store\vod-js-sdk-v6@1.7.1-beta.1\node_modules\vod-js-sdk-v6\lib\src\uploader.js:158

问题分析vod-js-sdk-v6依赖第三方库 axiosjs-sha1,但在 uni-app 环境中这些依赖无法正确加载。

尝试解决

bash 复制代码
npm install axios js-sha1
# 但仍存在兼容性问题

2.2 第二阶段:COS SDK 尝试

2.2.1 尝试 cos-js-sdk-v5

由于 VOD SDK 问题无法解决(后查找资料得知,vod只支持网页端),转向腾讯云 COS JavaScript SDK:

javascript 复制代码
import COS from 'cos-js-sdk-v5'

export default {
  data() {
    return {
      cos: null
    }
  },
  
  mounted() {
    this.initCOS();
  },
  
  methods: {
    initCOS() {
      this.cos = new COS({
        SecretId: 'your-secret-id',
        SecretKey: 'your-secret-key'
      });
    },
    
    async uploadToCOSDirect(file) {
      try {
        const result = await new Promise((resolve, reject) => {
          this.cos.putObject({
            Bucket: 'your-bucket-name-123456789',
            Region: 'your-region',
            Key: `uploads/${Date.now()}_${this.fileName}`,
            Body: file,  // 需要file类型的文件
            onProgress: (progressData) => {
              this.uploadProgress = progressData.percent;
            }
          }, (err, data) => {
            if (err) {
              reject(err);
            } else {
              resolve(data);
            }
          });
        });
      } catch (error) {
        console.error('上传失败:', error);
      }
    }
  }
}
2.2.2 遇到的错误
复制代码
warning: cos-js-sdk-v5 不支持 nodejs 环境使用,请改用 cos-nodejs-sdk-v5

问题分析 :在 uni-app App 端运行时,cos-js-sdk-v5 检测到 Node.js 环境而报错(可能uniapp打包过程是在node环境进行的)。

2.2.3 尝试 cos-nodejs-sdk-v5(静态导入方式)
bash 复制代码
npm uninstall cos-js-sdk-v5
npm install cos-nodejs-sdk-v5

修改导入方式:

javascript 复制代码
import COS from 'cos-nodejs-sdk-v5'  // 静态导入方式

export default {
  methods: {
    initCOS() {
      this.cos = new COS({
        SecretId: 'your-secret-id',
        SecretKey: 'your-secret-key'
      });
    }
  }
}
2.2.4 新的依赖错误
复制代码
文件查找失败:'fast-xml-parser' at node_modules\.store\cos-nodejs-sdk-v5@2.15.4\node_modules\cos-nodejs-sdk-v5\sdk\util.js:9

问题分析cos-nodejs-sdk-v5依赖 ast-xml-parser,但在 uni-app 环境中无法正确加载。

2.3 第三阶段:COS SDK 动态导入方案

2.3.1 为什么要换成动态导入?

原因1:解决依赖加载问题

javascript 复制代码
// 静态导入在编译时就会尝试加载所有依赖
import COS from 'cos-nodejs-sdk-v5'  // 编译时就可能报错

// 动态导入在运行时才加载,可以更好地处理错误
let COS;
try {
  const cosModule = await import('cos-nodejs-sdk-v5');
  COS = cosModule.default || cosModule;
} catch (e) {
  console.error('加载失败:', e);
}

原因2:平台兼容性处理

javascript 复制代码
// 不同平台可能需要不同的 SDK
async initCOS() {
  try {
    // 优先尝试 Node.js 版本
    const cosModule = await import('cos-nodejs-sdk-v5');
    COS = cosModule.default || cosModule;
  } catch (eImport) {
    try {
      // 备选 JavaScript 版本
      const cosModule = await import('cos-js-sdk-v5');
      COS = cosModule.default || cosModule;
    } catch (eRequire) {
      console.error('两个版本都加载失败');
    }
  }
  
  if (COS) {
    this.cos = new COS({
      SecretId: 'your-secret-id',
      SecretKey: 'your-secret-key'
    });
  }
}

原因3:按需加载,减少包体积

javascript 复制代码
// 只在需要时才加载 SDK
methods: {
  async uploadToCOSDirect(file) {
    // 确保 COS SDK 已加载
    if (!this.cos) {
      await this.initCOS();
    }
    
    if (this.cos) {
      // 执行上传逻辑
    }
  }
}
2.3.2 动态导入实现方案
javascript 复制代码
export default {
  data() {
    return {
      cos: null
    }
  },
  
  methods: {
    // 动态导入 COS SDK
    async initCOS() {
      let COS;
      
      try {
        // 优先尝试动态 import(支持打包器)
        const mod = await import('cos-nodejs-sdk-v5');
        COS = mod && (mod.default || mod);
      } catch (eImport) {
        try {
          // fallback:require(某些编译环境可用)
          const req = require && require('cos-nodejs-sdk-v5');
          COS = req && (req.default || req);
        } catch (eRequire) {
          console.error('加载 cos-nodejs-sdk-v5 失败', eImport, eRequire);
          return;
        }
      }
      
      if (COS) {
        this.cos = new COS({
          SecretId: 'your-secret-id',
          SecretKey: 'your-secret-key'
        });
      }
    },
    
    // 使用动态导入的上传方法
    async uploadToCOSDirect(file) {
      try {
        // 确保 SDK 已加载
        if (!this.cos) {
          await this.initCOS();
        }
        
        if (!this.cos) {
          throw new Error('COS SDK 加载失败');
        }
        
        const result = await new Promise((resolve, reject) => {
          this.cos.putObject({
            Bucket: 'your-bucket',
            Region: 'ap-guangzhou',
            Key: `uploads/${Date.now()}_${file.name}`,
            Body: file,
            onProgress: (progressData) => {
              this.uploadProgress = progressData.percent;
            }
          }, (err, data) => {
            if (err) {
              reject(err);
            } else {
              resolve(data);
            }
          });
        });
        
        return result;
      } catch (error) {
        console.error('上传失败:', error);
        throw error;
      }
    }
  }
}

2.4 第四阶段:最终方案 - COS 预签名 URL

2.4.1 决策转变原因

基于以上多次尝试遇到的问题,决定转向更简单的 COS 预签名 URL 方式上传:

  1. 避免 SDK 兼容性问题:无需引入复杂的第三方 SDK
  2. 减少依赖冲突:不依赖额外的 npm 包
  3. 提高稳定性:使用 uni-app 原生 API 实现
  4. 简化实现:代码逻辑更清晰
2.4.2 最终实现方案
javascript 复制代码
// 1. 获取预签名信息
const signatureRes = await new Promise((resolve, reject) => {
  uni.request({
    url: 'http://后端接口/uniAppDirectSign',
    method: 'POST',
    success: resolve,
    fail: reject
  });
});

// 2. 使用 uni.uploadFile 上传
uni.uploadFile({
  url: 'https://' + signatureData.cosHost,
  filePath: filePath,
  name: 'file',
  formData: {
    'key': signatureData.cosKey,
    'policy': signatureData.policy,
    'q-sign-algorithm': signatureData.qSignAlgorithm,
    // ... 其他签名参数
  }
});

3. 最终实现详解

3.1 文件选择与上传流程

javascript 复制代码
// 拍照功能
takePhoto() {
  uni.chooseImage({
    count: 1,
    sourceType: ['camera', 'album'],
    success: (res) => {
      const file = res.tempFiles[0];
      this.uploadToCOS(file); // 使用预签名URL上传
    }
  });
}

3.2 核心上传方法

1. 封装工具类,多处方便使用
javascript 复制代码
class CosUploader {
  constructor() {
    this.baseUrl = 'your-address';
  }

  /**
   * 上传文件到腾讯云 COS
   * @param {Object} file - 要上传的文件对象
   * @param {Function} onProgress - 进度回调函数
   * @returns {Promise<string>} 返回文件访问 URL
   */
  async uploadToCOS(file, onProgress) {
    if (!file) {
      throw new Error('请先选择文件');
    }

    try {
      const fileName = file.path.split('/').pop();
      const lastIndex = fileName.lastIndexOf('.');
      const extName = lastIndex > -1 ? fileName.slice(lastIndex + 1) : '';
      const filePath = file.path;

      // 1. 先从后端获取上传凭证
      const signatureRes = await this.getUploadSignature(extName);

      const signatureData = signatureRes.data.data;
      console.log('获取上传签名:', signatureData);

      // 2. 使用 uni.uploadFile 直接上传到 COS
      const fileUrl = await this.uploadFileToCos(filePath, signatureData, onProgress);
      
      return fileUrl;
    } catch (error) {
      console.error('上传失败:', error);
      throw new Error('上传失败: ' + (error.message || '网络错误'));
    }
  }

  /**
   * 获取上传凭证
   * @param {string} extName - 文件扩展名
   * @returns {Promise<Object>} 签名数据
   */
  getUploadSignature(extName) {
    return new Promise((resolve, reject) => {
      uni.request({
        url: `${this.baseUrl}/uniAppDirectSign?ext=${extName}`,
        method: 'POST',
        success: (res) => resolve(res),
        fail: (err) => reject(err)
      });
    });
  }

  /**
   * 上传文件到 COS
   * @param {string} filePath - 文件路径
   * @param {Object} signatureData - 签名数据
   * @param {Function} onProgress - 进度回调
   * @returns {Promise<string>} 文件访问 URL
   */
  uploadFileToCos(filePath, signatureData, onProgress) {
    return new Promise((resolve, reject) => {
      uni.uploadFile({
        url: 'https://' + signatureData.cosHost,
        filePath: filePath,
        name: 'file',
        formData: {
          'key': signatureData.cosKey,
          'policy': signatureData.policy,
          'success_action_status': 200,
          'q-sign-algorithm': signatureData.qSignAlgorithm,
          'q-ak': signatureData.qAk,
          'q-key-time': signatureData.qKeyTime,
          'q-signature': signatureData.qSignature,
          'x-cos-security-token': signatureData.securityToken || ''
        },
        success: (uploadRes) => {
          if (uploadRes.statusCode === 200) {
            const fileUrl = 'https://' + signatureData.cosHost + '/' + signatureData.cosKey;
            resolve(fileUrl);
          } else {
            reject(new Error('上传失败'));
          }
        },
        fail: (err) => {
          reject(err);
        },
        progress: (progress) => {
          if (onProgress) {
            onProgress(progress.progress);
          }
        }
      });
    });
  }
}

// 导出单例实例
export default new CosUploader();
3.2. 页面中使用
javascript 复制代码
// 在页面中引入
import cosUploader from '@/utils/cosUploader.js'
// ....  其他代码

// 上传到 COS (预签名URL方式)
async uploadToCOS(file) {
  try {
    this.uploading = true;
    this.uploadProgress = '开始上传...';

    const fileUrl = await cosUploader.uploadToCOS(file, (progress) => {
      this.uploadProgress = `上传进度: ${progress}%`;
    });

    this.fileUrl = fileUrl;
    this.uploadProgress = '上传完成';
    uni.showToast({ title: '上传成功', icon: 'success' });
    console.log('文件上传成功,访问URL:', this.fileUrl);
  } catch (error) {
    this.uploadProgress = '上传失败';
    console.error('上传失败:', error);
    uni.showToast({ title: error.message, icon: 'none' });
  } finally {
    this.uploading = false;
  }
},

4. 关键技术要点

4.1 动态导入的优势

javascript 复制代码
// 动态导入 vs 静态导入
// 静态导入 - 编译时加载
import COS from 'cos-nodejs-sdk-v5';  // 可能编译时报错

// 动态导入 - 运行时加载
async function loadCOS() {
  try {
    const cosModule = await import('cos-nodejs-sdk-v5');
    return cosModule.default;
  } catch (error) {
    console.error('加载失败:', error);
    return null;
  }
}

4.2 Promise 封装的必要性

使用 Promise 封装异步操作,配合 async/await 简化代码:

javascript 复制代码
const signatureRes = await new Promise((resolve, reject) => {
  uni.request({
    url: '获取签名',
    success: resolve,
    fail: reject
  });
});

4.3 文件路径处理

javascript 复制代码
// 从文件路径提取扩展名
const fileName = file.path.split('/').pop();
const lastIndex = fileName.lastIndexOf('.');
const extName = lastIndex > -1 ? fileName.slice(lastIndex + 1) : '';

4.4 上传进度监控

javascript 复制代码
uni.uploadFile({
  progress: (progress) => {
    this.uploadProgress = progress.progress / 100;
  }
});

5. 学习收获与经验总结

5.1 动态导入的价值

  1. 错误处理:可以优雅地处理模块加载失败
  2. 按需加载:只在需要时加载模块,减少初始包体积
  3. 平台适配:根据不同平台加载不同的模块实现

5.2 开发实践

  1. Promise 封装:提升异步代码的可读性和维护性
  2. 统一错误处理:使用 try/catch 统一处理异步错误
  3. 用户体验:提供进度反馈和明确的操作结果提示
相关推荐
我狸才不是赔钱货8 小时前
前端技术栈全景图:从HTML到现代框架的演进之路
前端·html
百花~9 小时前
前端三剑客之一 HTML~
前端·html
lang201509289 小时前
Spring远程调用与Web服务全解析
java·前端·spring
崎岖Qiu10 小时前
【设计模式笔记06】:单一职责原则
java·笔记·设计模式·单一职责原则
im_AMBER11 小时前
数据结构 09 二叉树作业
数据结构·笔记·学习
listhi52011 小时前
利用React Hooks简化状态管理
前端·javascript·react.js
一点一木12 小时前
🚀 2025 年 10 月 GitHub 十大热门项目排行榜 🔥
前端·人工智能·github
华仔啊12 小时前
这个Vue3旋转菜单组件让项目颜值提升200%!支持多种主题,拿来即用
前端·javascript·css
非凡ghost12 小时前
Adobe Lightroom安卓版(手机调色软件)绿色版
前端·windows·adobe·智能手机·软件需求