前言
最近又负责了个uni小程序的开发,拉下代码一看,大为吃惊,vue2+js的老项目,底层几乎什么都没封装处理,连上传文件都没有,可把我烦躁坏了,没办法只能封装处理下,之前也封装过腾讯COS的上传,但是那是后台的,而且我感觉当初写的还是有点不太好,除此之外小程序配合vant的还是有点区别。
代码
/*
* @Author : 桔子
* @Date : 2024-04-25 17:11:11
* @LastEditors : 桔子
* @LastEditTime : 2024-04-26 09:26:03
* @Description : 文件上传
* @FilePath : /hooks/web/useUploadFile.js
*/
// 引入cos-wx-sdk-v5
import COS from "cos-wx-sdk-v5";
import dayjs from "dayjs";
import md5 from "@/config/md5.min"; // md5加密
const basic_url = "https://xxxxx.com/xx/xxx"; // 获取oss基本信息
const src = 'xxx'
let cos = null; // 存放 new 出的对象
let oss_token = null; // cos相关的token
export const useUploadFile = () => {
/**
* @Description: 初始化对应的cos对象
* @return {*}
*/
const initCos = async () => {
const result = await uni.$originalRequest({
url: basic_url,
data: { src },
method: "POST",
});
oss_token = result?.data?.data || null;
const {
oss_expired_time,
oss_session_token,
oss_start_time,
oss_tmp_secret_id,
oss_tmp_secret_key,
} = oss_token || {};
// new 一个COS对象
cos = new COS({
// 必选参数
SimpleUploadMethod: "putObject",
getAuthorization: (options, callback) => {
callback({
TmpSecretId: oss_tmp_secret_id,
TmpSecretKey: oss_tmp_secret_key,
XCosSecurityToken: oss_session_token,
StartTime: oss_start_time, // 时间戳,单位秒,如:1580000000
ExpiredTime: oss_expired_time, // 时间戳,单位秒,如:1580000900
ScopeLimit: true, // 细粒度控制权限需要设为 true,会限制密钥只在相同请求时重复使用--如果是要单次使用,即每次上传都需要获取一次签名时,那么就需要改为false
ProgressInterval: 0.001,
});
},
});
};
/**
* @Description: 处理文件名
* @param {*} file
* @param {*} now_time
* @return {*}
*/
const getFileName = (file, now_time) => {
const { type, url } = file || {};
const name_arr = url.split("."); // 转为数组
let file_name_type = name_arr[name_arr.length - 1]; // 最后一个是文件类型
file_name_type =
type === "video"
? `-video.${file_name_type}`
: `-img.${file_name_type}`;
name_arr.pop(); // 删除最后一项文件类型的
const deal_file_name = name_arr.join(""); // 转成字符串
let file_name = `${now_time}-${md5(deal_file_name)}${file_name_type}`;
return file_name;
};
/**
* @Description: 判断token是否过期
* @return {*}
*/
const judgeOssToken = async () => {
const now_time = +dayjs().unix();
const now_time_end = now_time + 60 * 5; // 这里之所以加五分钟的秒数,是担心不加的时候,现在可能满足条件,但是后面请求可能恰好到了过期时间,从而导致上传失败
const oss_expired_time_end = oss_token?.oss_expired_time || 0;
if (oss_expired_time_end < now_time_end) {
await initCos();
}
return {
now_time,
};
};
/**
* @Description: 上传文件
* @param {*} file
* @return {*}
*/
const uploadFile = async (file) => {
const { now_time } = await judgeOssToken();
const { type, url } = file || {};
const { oss_bucket, oss_region, oss_path, oss_cdn_domain } =
oss_token || {};
const file_name = getFileName(file, now_time);
const file_key = `${oss_path}${file_name}`;
const put_obj = {
Bucket: oss_bucket,
Region: oss_region,
Key: file_key,
FilePath: url,
Headers:
type === "video"
? null
: {
"Pic-Operations":
'{"is_pic_info": 1, "rules": [{"fileid": "' +
file_name +
'", "rule": "imageMogr2/thumbnail/600x/"}]}',
},
onProgress: (info) => {
console.log(JSON.stringify(info));
},
};
return new Promise((resolve, reject) => {
cos.putObject(put_obj, (err, data) => {
if (err) {
uni.showToast({
title: "网络错误,请重新上传图片!",
icon: "none",
});
resolve({ code: 0, data: err });
} else {
const reesult = `https://${oss_cdn_domain}${
data.Location.split(".com")[1]
}`;
resolve({ code: 1, data: reesult, file_key });
}
});
});
};
/**
* @Description: 删除文件
* @param {*} file_key
* @return {*}
*/
const deleteFile = async (file_key) => {
await judgeOssToken();
const { oss_bucket, oss_region } = oss_token || {};
const delete_obj = {
Bucket: oss_bucket,
Region: oss_region,
Key: file_key,
};
return new Promise((resolve, reject) => {
cos.deleteObject(delete_obj, (err, data) => {
if (err) {
resolve({ code: 0, data: err });
} else {
resolve({ code: 1, data: data });
}
});
});
};
return {
uploadFile,
deleteFile,
};
};
uni.$originalRequest其实就是uni.request哈,我不喜欢里面通过success或者fail返回结果哈,因为我感觉里面写太多逻辑,不易于阅读,就很简单的套了一层promise,如下
/**
* @Description: 原始的uni.request封一层
* @param {*} row
* @return {*}
*/
export const originalRequest = (row) => {
return new Promise((resolve, reject) => {
uni.request({
...row,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
},
});
});
};
至于上面的基础oss请求地址和那个参数找公司配置的人要哈。
组件使用
<!--
* @Author : 桔子
* @Date : 2024-04-25 13:41:56
* @LastEditors : 桔子
* @LastEditTime : 2024-04-26 10:10:44
* @Description : 上传文件组件
* @FilePath : /components/UploadFile/index.vue
-->
<template>
<view :class="{ basic_upload_wrap: defaultStyle }">
<van-uploader
:fileList="file_list"
:multiple="multiple"
:maxCount="maxCount"
:accept="accept"
:readonly="uploadFlag"
:deletable="deletable"
:use-before-read="true"
@before-read="beforeRead"
@after-read="afterRead"
@delete="deleteRead"
>
<slot></slot>
</van-uploader>
</view>
</template>
<script>
import { useUploadFile } from "@/hooks/web/useUploadFile";
import { dealListData } from "./format";
export default {
name: "UploadFile",
props: {
defaultStyle: {
type: Boolean,
default: true,
},
/**
* @Description: 格式['地址', ‘地址’]或者[{url: '地址', status: 'done', file_key: '文件key'}]
* @return {*}
*/
initList: {
type: Array,
default: () => [],
},
// 是否开启多选
multiple: {
type: Boolean,
default: false,
},
maxCount: {
type: Number,
default: 1,
},
accept: {
type: String,
default: "all",
},
deletable: {
type: Boolean,
default: true,
},
},
data() {
return {
file_list: [], // 图片列表
uploadFlag: false, // 是否可上传
};
},
created() {
this.autoInitList();
},
methods: {
/**
* @Description: 自动注入数据,但是只会执行一次
* @return {*}
*/
autoInitList() {
const unwatch = this.$watch(
"initList",
(val) => {
this.file_list = dealListData(val);
},
{
immediate: true,
deep: true,
}
);
unwatch && unwatch();
},
/**
* @Description: 手动注入数据
* @param {*} val
* @return {*}
*/
operationInitList(val) {
this.file_list = dealListData(val);
},
/**
* @Description: 文件上传前的钩子函数
* @param {*} file
* @param {*} detail
* @return {*}
*/
beforeRead(event) {
const { file, callback } = event.detail;
this.uploadFlag = true;
callback && callback(true);
},
/**
* @Description: 上传成功回调
* @param {*} file
* @param {*} detail
* @return {*}
*/
async afterRead(file, detail) {
this.uploadFlag = false;
const file_data = file.detail.file;
const { uploadFile } = useUploadFile();
const result = await uploadFile(file_data);
if (result.code === 1) {
this.file_list.push({
url: result.data,
status: "done",
file_key: result.file_key,
});
}
this.$emit("getUploadData", this.file_list);
},
/**
* @Description: 删除图片
* @param {*} event
* @return {*}
*/
async deleteRead(event) {
const { file = {} } = event.detail;
// const { deleteFile } = useUploadFile();
if (file.file_key) {
// 目前删除服务端没有给权限,删除不了
// await deleteFile(file.file_key);
}
this.file_list = this.file_list.filter((item) => item.url !== file.url);
this.$emit("getUploadData", this.file_list);
},
},
};
</script>
<style lang="scss" scoped>
.basic_upload_wrap {
background: #fff;
}
</style>
format文件
/*
* @Author : 桔子
* @Date : 2024-04-26 09:49:49
* @LastEditors : 桔子
* @LastEditTime : 2024-04-26 10:04:20
* @Description : 头部注释配置模板
* @FilePath : /silkworm-business-box-uni/components/UploadFile/format.js
*/
/**
* @Description: 初始化数据处理
* @param {*} val
* @return {*}
*/
export const dealListData = (val) => {
const arr = [];
if (Array.isArray(val) && val.length) {
val.forEach((item) => {
const data = { status: "done", file_key: "" };
if (typeof item === "string") {
data.url = item;
}
if (typeof item !== "string" && item?.url) {
data.url = item.url;
}
arr.push(data);
});
}
return arr;
};
以上我用的是"@vant/weapp": "^1.11.5"版本哈,注意点vant和vant/weapp有点区别的,仔细点
说明
autoInitList是用来初始化开始的数据哈,但是它只会执行一遍,开始本来用监听简单点写,但是我发现当触发getUploadData方法后,父组件获取的数据字段和initList如果是同一个,那么监听会再次执行一遍,其实是不需要的哈,initList传进来改变的也是file_list字段,而我上传好了数据,file_list字段就是最新的,没必要监听在执行一次,感觉消耗性能,所以这里我才用了命令式的监听,目的就是初始化一次后,取消监听不在执行了,但是我又担心总有些特例要手动更新下,为此再写了个operationInitList方法,目的就是通过$ref手动再次赋值。
最后
我就不过多讲解了,页面中直接引组件就好,根据业务需求调整即可,之所以重新封装一下,就是一方面我这里的写法不一样了,用hoos写的,更清晰明了,另一方面我这里不会每次去请求获取token了,而是根据过期时间适当请求,最后就是我增加了删除文件的方法,当然这个方法还需要配置是否能删除的权限哈,我这里没有配置,所以删除其实是不成功的,由于老项目,没用ts,需要ts的其实改下就好,加个类型即可