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依赖第三方库 axios 和 js-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 方式上传:
- 避免 SDK 兼容性问题:无需引入复杂的第三方 SDK
- 减少依赖冲突:不依赖额外的 npm 包
- 提高稳定性:使用 uni-app 原生 API 实现
- 简化实现:代码逻辑更清晰
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 动态导入的价值
- 错误处理:可以优雅地处理模块加载失败
- 按需加载:只在需要时加载模块,减少初始包体积
- 平台适配:根据不同平台加载不同的模块实现
5.2 开发实践
- Promise 封装:提升异步代码的可读性和维护性
- 统一错误处理:使用 try/catch 统一处理异步错误
- 用户体验:提供进度反馈和明确的操作结果提示