前端直传阿里云OSS

阿里云对象存储服务(Object Storage Service,简称OSS),是阿里云对外提供的海量、安全、低成本、高可靠的云存储服务。

目前通过Web端直传文件(Object)到OSS,有两种方案:

一、利用OSS Browser.js SDK将文件上传到OSS。该方案通过OSS Browser.js SDK直传数据到OSS,支持断点续传,支持各种主流浏览器,可以将File对象、Blob数据以及OSS Buffer上传OSS,该方案还支持下载和删除

二、利用OSS提供的PostObject接口来实现表单上传,不支持断点续传,支持h5,小程序,支持uniapp的uni.uploadFile接口

方案一:使用阿里云SDK上传

由于前端环境不安全,为避免暴露阿里云账号访问密钥(AccessKey ID和AccessKey Secret),该方案需要搭建STS服务获取临时访问密钥(AccessKey ID和AccessKey Secret)和安全令牌(SecurityToken),需要先开通STS服务,参考官方文档

后端

后端需要导入aliyun-sdk-oss包,用于获取前端需要的key和secret

JDK版本:jdk8

xml 复制代码
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>2.0.7</version>
    <!--<version>3.15.0</version>最新-->
</dependency>

如果是java9及以上版本,则需要添加jaxb相关依赖。添加jaxb相关依赖示例代码如下:

xml 复制代码
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

搭建STS服务(部分代码)

javascript 复制代码
	// STS接入地址,例如sts.cn-hangzhou.aliyuncs.com。
  	@Value("${ramEndpoint}")
    private String ramEndpoint;
    // 访问密钥AccessKey ID和AccessKey Secret
    @Value("${ramAccessKeyId}")
    private String ramAccessKeyId;
    @Value("${ramAccessKeySecret}")
    private String ramAccessKeySecret;
    // 角色ARN
    @Value("${ramRoleArn}")
    // 自定义角色会话名称,用来区分不同的令牌,例如可填写为SessionTest
    private String ramRoleArn;
    @Value("${ramRoleSessionName}")
    private String ramRoleSessionName;

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * 通过RAM子账号获取stsToken,作为临时凭据
     */
    @RequestMapping(value = "getStsToken", method = {RequestMethod.GET, RequestMethod.POST})
    public Object getStsToken() {
        ResponseVo responseVo = new ResponseVo();
        AssumeRoleResponse response = null;
        Object redisToken = null;
        try {
            redisToken = redisTemplate.opsForValue().get("stsToken");
        } catch (Exception e) {
            logger.error(e.getMessage());
            responseVo.setError(GlobalErrorCode.SYS_RUN_ERROR.getCode());
            return responseVo;
        }
        if (redisToken != null) {
            response = JSONObject.parseObject(redisToken.toString(), AssumeRoleResponse.class);
            responseVo.setSuccess(response);
            return responseVo;
        } else {
            String policy = "{\n" +
                    "    \"Version\": \"1\", \n" +
                    "    \"Statement\": [\n" +
                    "        {\n" +
                    "            \"Action\": [\n" +
                    "                \"oss:*\"\n" +
                    "            ], \n" +
                    "            \"Resource\": [\n" +
                    "                \"acs:oss:*:*:*\" \n" +
                    "            ], \n" +
                    "            \"Effect\": \"Allow\"\n" +
                    "        }\n" +
                    "    ]\n" +
                    "}";
            try {
                DefaultProfile.addEndpoint("", "", "Sts", ramEndpoint);
                // 构造default profile(参数留空,无需添加region ID)
                IClientProfile profile = DefaultProfile.getProfile("", ramAccessKeyId, ramAccessKeySecret);
                // 用profile构造client
                DefaultAcsClient client = new DefaultAcsClient(profile);
                final AssumeRoleRequest request = new AssumeRoleRequest();
                request.setMethod(MethodType.POST);
                request.setRoleArn(ramRoleArn);
                request.setRoleSessionName(ramRoleSessionName);
                request.setPolicy(policy); // 若policy为空,则用户将获得该角色下所有权限
                request.setDurationSeconds(20 * 60L); // 设置凭证有效时间,单位秒
                //获取凭证
                response = client.getAcsResponse(request);
                /*
                 * 缓存该凭证,凭证失效后才从OSS再次获取凭证
                 * 凭证有效时间为20分钟,Redis里只缓存10分钟
                 */
                redisTemplate.opsForValue().set("stsToken", JSONObject.toJSONString(response), 10 * 60, TimeUnit.SECONDS);
                responseVo.setSuccess(response);
                return responseVo;
            } catch (ClientException e) {
                logger.error(e.getErrMsg());
                responseVo.setError(GlobalErrorCode.SYS_RUN_ERROR.getCode());
                return responseVo;
            }
        }

    }

参考阿里云文档

前端

安装

复制代码
$ npm install ali-oss --save

部分代码

javascript 复制代码
onLoad() {
	this.getStsToken()
},

methods

javascript 复制代码
/**
* @param {String} pathAndName Object完整路径。Object完整路径中不能包含Bucket名称("exampledir/exampleobject.txt")
* @param {Object} data (file对象、Blob数据或者OSS Buffer)
*/
async putObject(pathAndName, data) {
    try {
        // 您可以通过自定义文件名(例如exampleobject.txt)或文件完整路径(例如exampledir/exampleobject.txt)的形式实现将数据上传到当前Bucket或Bucket中的指定目录。
        const result = await this.getClient().put(
            pathAndName,
            data
        );
        console.log('result:', result);
    } catch (e) {
        console.log(e);
    }
},
    getClient() {
        if (this.client) {
            return this.client
        }
        const OSS = require('ali-oss');

        const client = new OSS({
            // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
            region: 'oss-cn-qingdao',
            // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
            accessKeyId: this.stsToken.credentials.accessKeyId,
            accessKeySecret: this.stsToken.credentials.accessKeySecret,
            // 从STS服务获取的安全令牌(SecurityToken)。
            stsToken: this.stsToken.credentials.securityToken,
            refreshSTSToken: async () => {
                // 向您搭建的STS服务获取临时访问凭证。
                let info = await this.$post(GET_STS_TOKEN)
                info = info.data
                console.log('-----------refresh--token')

                return {
                    accessKeyId: info.credentials.accessKeyId,
                    accessKeySecret: info.credentials.accessKeySecret,
                    stsToken: info.credentials.securityToken
                }
            },
            // 刷新临时访问凭证的时间间隔,单位为毫秒。每隔一段时间定时器会自动掉后台接口刷新token
            refreshSTSTokenInterval: 600000,
            // 填写Bucket名称。
            bucket: 'zxxxxth-bucket'
        });

        this.client = client
        return this.client
    },
        getStsToken() {
            //从后台获取stsToken(改成自己的前端请求接口)
            this.$post(GET_STS_TOKEN).then(rsp => {
                if (rsp.success) {
                    this.stsToken = rsp.data;
                    // 初始化一下client让定时任务启动,自动刷新token(避免过期)
                    this.getClient()
                    console.log('this.stsToken:', this.stsToken)
                } else {
                    uni.showToast({
                        title: rsp.message,
                        duration: 2000
                    });
                }
            })
        },              
                

refreshSTSToken参数说明:当初始化new OSS()时,定时器会启动,当时间到了refreshSTSTokenInterval所设置的值时,并不会立即调用后台接口获取token,只有手动触发put()接口时,才会调用后台接口获取token

参考阿里云文档

开通STS服务步骤

方案二:使用PostObject接口来实现表单上传

这个方案支持小程序上传,uniapp上传。无需开通STS服务

后端

获取postObject接口需要的policy,OSSAccessKeyId,signature 参考官方文档

这里签名使用后端签名,所以不需要申请开通STS服务

简化版,无回调
java 复制代码
/**
     * 利用OSS提供的PostObject接口,通过表单上传的方式将文件上传到OSS。
     * 该方案兼容大部分浏览器,但在网络状况不好的时候,如果单个文件上传失败,
     * 只能重试上传。上传的Object大小不能超过5 GB。
     * @return ResponseVo{success:true,message:'',data:{},code:200}
     */
    @RequestMapping(value = "getPostObjectParams", method = {RequestMethod.GET, RequestMethod.POST})
    public Object getPostObjectParams() {
        ResponseVo responseVo = new ResponseVo();
        responseVo.setSuccess(OSSServer.getPostObjectParams());
        return responseVo;
    }

OSSServer.class

java 复制代码
	public static OSSClient getOSSClient() {
		if (null == ossClient) {
			ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
		}
		return ossClient;
	}	

		/**
	 * 获取表单上传的方式的PostObject参数
	 * @return
	 */
	public static Map<String, Object> getPostObjectParams() {
		Map<String, Object> respMap = new LinkedHashMap();
		// 限制参数的生效时间,单位为分钟,默认值为20。
		int expireTime = 20;
		// 限制上传文件的大小,单位为MB,默认值为100。
		int maxSize = 100;
		// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
		// 如果值为"test"那么前端的key参数必须以"test"开头,如test/*、test1.jpg、test/comment/11.jpg
		String dir = "";

		// 创建OSSClient实例。
		OSS ossClient = getOSSClient();
		try {
			long expireEndTime = System.currentTimeMillis() + expireTime * 1000 * 60;
			Date expiration = new Date(expireEndTime);
			PolicyConditions policyConds = new PolicyConditions();
			policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, maxSize * 1024 * 1024);
			policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

			String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
			byte[] binaryData = postPolicy.getBytes("utf-8");
			String encodedPolicy = BinaryUtil.toBase64String(binaryData);
			String postSignature = ossClient.calculatePostSignature(postPolicy);


			respMap.put("accessKeyId", accessKeyId);
			respMap.put("policy", encodedPolicy);
			respMap.put("signature", postSignature);

			respMap.put("expire", expireEndTime / 1000);
		} catch (Exception e) {
			log.error("getPostObjectParams", e);
		}

		return respMap;
	}

大多数情况下,用户上传文件后,应用服务器需要知道用户上传了哪些文件以及文件名;如果上传了图片,还需要知道图片的大小等,为此OSS提供了上传回调方案。

流程图:

当用户要上传一个文件到OSS,而且希望将上传的结果返回给应用服务器时,需要设置一个回调函数,将请求告知应用服务器。用户上传完文件后,不会直接得到返回结果,而是先通知应用服务器,再把结果转达给用户。

有回调的PostObject参数
java 复制代码
/**
	 * 获取表单上传的方式的PostObject参数【有回调】
	 * @return
	 */
	public static Map<String, Object> getPostObjectParams() {
		Map<String, Object> respMap = new LinkedHashMap();
		// 限制参数的生效时间,单位为分钟,默认值为20。
		int expireTime = 20;
		// 限制上传文件的大小,单位为MB,默认值为10。
		int maxSize = 10;
		// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
		// 如果值为"test"那么前端的key参数必须以"test"开头,如test/*、test1.jpg、test/comment/11.jpg
		// 可以让用户没有办法上传到其他的目录,从而保证了数据的安全性
		String dir = "";
		// 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
		String callbackUrl = "https://jmt.xxx.cn/common/postObjectCallBack/";

		// 创建OSSClient实例。
		OSS ossClient = getOSSClient();
		try {
			long expireEndTime = System.currentTimeMillis() + expireTime * 1000 * 60;
			Date expiration = new Date(expireEndTime);
			PolicyConditions policyConds = new PolicyConditions();
			policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, maxSize * 1024 * 1024);
			policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

			String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
			byte[] binaryData = postPolicy.getBytes("utf-8");
			String encodedPolicy = BinaryUtil.toBase64String(binaryData);
			String postSignature = ossClient.calculatePostSignature(postPolicy);


			respMap.put("accessKeyId", accessKeyId);
			respMap.put("policy", encodedPolicy);
			respMap.put("signature", postSignature);
			respMap.put("expire", expireEndTime / 1000);
			// 配置回调地址
			JSONObject jasonCallback = new JSONObject();
			jasonCallback.put("callbackUrl", callbackUrl);
//			jasonCallback.put("callbackBody",
//					"filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");

			jasonCallback.put("callbackBody", "{\"filename\":${object},\"mimeType\":${mimeType}}");

			jasonCallback.put("callbackBodyType", "application/json");//application/x-www-form-urlencoded
			String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
			respMap.put("callback", base64CallbackBody);

		} catch (Exception e) {
			log.error("getPostObjectParams", e);
		}

		return respMap;
	}
给oss回调的接口
java 复制代码
    @RequestMapping(value = "postObjectCallBack", method = RequestMethod.POST)
    public Object postObjectCallBack(HttpServletRequest request, @RequestBody Object callbackBody) throws IOException {

        log.info("---callbackBody={}",callbackBody);

//        "{"filename":"test/comment/tt1.jpg","mimeType":"image/png"}"
         return callbackBody;

    }

前端

小程序
javascript 复制代码
const host = '<host>'; //"https://examplebucket.oss-cn-hangzhou.aliyuncs.com"
const signature = '<signatureString>';
const ossAccessKeyId = '<accessKey>';
const policy = '<policyBase64Str>';
const key = '<object name>';
const securityToken = '<x-oss-security-token>'; 
const filePath = '<filePath>'; // 待上传文件的文件路径。
wx.uploadFile({
  url: host, // 这个是阿里云bucket的根地址,使用这个地址拼接路径可以访问已上传的文件。如果是自己服务器则是开发者服务器的URL。
  filePath: filePath,// 本地文件路径,小程序chooseImage方法返回的路径
  name: 'file', // 必须填file。
  formData: {
    key,
    policy,
    OSSAccessKeyId: ossAccessKeyId,
    signature,
    // 'x-oss-security-token': securityToken // 使用STS签名时必传。
  },
  success: (res) => {
    if (res.statusCode === 204) {
      console.log('上传成功');
    }
  },
  fail: err => {
    console.log(err);
  }
});
uniapp、uView的Upload组件
javascript 复制代码
uni.uploadFile({
    url: 'https://res.xxx.cn', //这个是阿里云bucket的根地址,使用这个地址拼接路径可以访问已上传的文件
    filePath: url,// 本地文件路径,小程序chooseImage方法返回的路径
    name: 'file',// 必须填file
    formData: {
        key: 'test/comment/tt1.jpg',//会把tt1.jpg图片上传至bucket(上方url所指向)的test/comment目录
        policy: this.postObject.policy,
        OSSAccessKeyId: this.postObject.accessKeyId,
        signature: this.postObject.signature,
        // callback: this.postObject.callback
    },
    success: (res) => {
        console.log('uni.uploadFile success:', res)
        if(res.statusCode===204){
            // 上传成功
            console.log('-------------success------------')
        }else if(res.statusCode===403){
            // Policy expired.
            uni.showToast({
                title: '网络超时',
                duration: 2000
            });
            // 续期
            this.getPostObjectParams()
        }else{
            console.log('上传失败')
        }
        // setTimeout(() => {
        resolve(res)
        // }, 1000)
    },
    fail(err) {
        console.error('uni.uploadFile: fail', err)
    }
});
javascript 复制代码
onLoad() {
	this.getPostObjectParams()
},
...........
getPostObjectParams() {
    //从后台获取stsToken
    this.$post(GET_POST_OBJECT_PARAMS).then(rsp => {
        if (rsp.success) {
            this.postObject = rsp.data;
            console.log('this.postObject:', this.postObject)
        } else {
            uni.showToast({
                title: rsp.message,
                duration: 2000,
                icon:'none'
            });
        }
    })
},

参考官方文档

举一个uniapp例子

UI库:uView

javascript 复制代码
<template>
	<!-- 发表评论 -->
	<view class="create-comment">
		<view class="star comment-common">
			<view class="title">评分</view>
			<view class="control">
				<text class="name">游玩体验</text>
				<u-rate :count="5" v-model="starCount" :touchable="false" active-color="#E65526" size="24"></u-rate>
			</view>

		</view>

		<view class="content comment-common">
			<view class="title">评价内容</view>
			<textarea v-model="resourceComment.content" maxlength="200"
				placeholder="游玩的满意吗?大家都想了解这里值得去吗?有什么亮点?期待你精彩的点评!">
			</textarea>
		</view>

		<view class="picture comment-common">
			<view class="title">图片</view>
			<!-- name=1对应fileList1 -->
			<u-upload :fileList="fileList1" @afterRead="afterRead" @delete="deletePic" name="1" multiple :maxCount="15"
				:maxSize="maxSize">
			</u-upload>
		</view>

		<view class="picture comment-common">
			<view class="title">视频</view>
			<!-- name=2对应fileList2 -->
			<u-upload :fileList="fileList2" @afterRead="afterRead" @delete="deletePic" name="2" multiple :maxCount="1"
				:maxSize="maxSize" accept="video" uploadIcon="movie"></u-upload>
		</view>

		<button @click="submit()" type="warn" class="submit" :loading="loading" :disabled="loading">发布</button>
	</view>
</template>

<script>
	import {
		GET_POST_OBJECT_PARAMS
	} from '../../api/api.js'

	export default {
		data() {
			return {
				starCount: 0,
				resourceComment: {
					content: ''
				},
				fileList1: [],
				fileList2: [],
				loading: false,
				postObject: {
					expire: 0
				},
				maxSize: 100 * 1024 * 1024
			}
		},
		onLoad() {
		},
		methods: {
			// -----upload start
			// 新增图片
			async afterRead(event) {
				console.log('event:', event)

				// 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
				let lists = [].concat(event.file)
				// console.log('lists:', lists)
				let fileListLen = this[`fileList${event.name}`].length
				// console.log('fileListLen:', fileListLen)
				lists.map((item) => {
					this[`fileList${event.name}`].push({
						...item,
						status: 'uploading',
						message: '上传中'
					})
				})

				let time = new Date().getTime() / 1000
				// console.log('time:', time)
				if (time > this.postObject.expire) {
					// policy过期,续期
					await this.getPostObjectParams()
				}

				for (let i = 0; i < lists.length; i++) {
					const result = await this.uploadFilePromise(lists[i].url)
					let item = this[`fileList${event.name}`][fileListLen]
					this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
						status: result ? 'success' : 'failed',
						message: '',
						url: result
					}))
					fileListLen++
				}
			},
			compressJpgImage(src) {
				return new Promise((resolve, reject) => {
					// uni.compressImage({
					//   src: src,
					//   quality: 80,
					//   success: res => {
					//     console.log(res.tempFilePath)
					//   }
					// })
				})
			},
			uploadFilePromise(url) {
				return new Promise((resolve, reject) => {
					let a = uni.uploadFile({
						url: 'https://res.xxxx.cn',
						filePath: url,
						name: 'file', // 必须填file
						formData: {
							key: 'test/comment/tt3.jpg',
							policy: this.postObject.policy,
							OSSAccessKeyId: this.postObject.accessKeyId,
							signature: this.postObject.signature,
							callback: this.postObject.callback
						},
						success: (res) => {
							// 未配置回调 上传成功返回 {date:"",errMsg:"uploadFile:ok",statusCode:204},如果配置了回调data参数才会有值
							// 配置了回调 上传成功返回 {{data:{"filename":"test/comment/tt1.jpg","mimeType":"image/png"},errMsg:"uploadFile:ok",statusCode:200}
							console.log('uni.uploadFile success():', res)
							if (res.statusCode === 204 || res.statusCode === 200) {
								// 上传成功
								console.log('-------------uploaded success')
								resolve(url)
							} else {
								console.log('-------------uploaded failed')
								uni.showToast({
									title: '上传失败',
									duration: 2000,
									icon: 'error'
								});
								resolve()
							}
						},
						fail(err) {
							console.error('uni.uploadFile: fail():', err)
						}
					});
				})
			},
			// 删除图片
			deletePic(event) {
				this[`fileList${event.name}`].splice(event.index, 1)
			},
			// -----upload end

			getPostObjectParams() {
				//从后台获取postObject
				return this.$post(GET_POST_OBJECT_PARAMS,{folderType:'comment'}).then(rsp => {
					// this.postObject = rsp
					if (rsp.success) {
						this.postObject = rsp.data;
						console.log('this.postObject:', this.postObject)
					} else {
						console.error('getPostObjectParams:', rsp.message || '系统错误')
					}
				})
			}
		}
	}
</script>

<style lang="scss">
	.create-comment {
		padding: 12px;

		.comment-common {
			margin-bottom: 10px;
			padding: 15px 10px;
			background-color: white;
			border-radius: 10px;
		}

		.title {
			margin-bottom: 10px;
			font-size: 16px;
			font-weight: bold;
		}

		.star {
			.control {
				display: flex;
				align-items: center;

				.name {
					margin-right: 10px;
				}
			}
		}

		.content {
			textarea {
				font-size: 14px;
				width: 100%;
			}
		}

		.picture {}

		.submit {
			margin-top: 20px;
			width: 80%;
			font-size: 15px;
			color: white;
			background-color: #e65526;
		}
	}
</style>

附:根据blob链接获取blob对象

javascript 复制代码
			/**
			 * 根据blob链接获取blob对象
			 * @param {Object} url "blob:http://localhost:8085/d688ce4f-0f5d-418c-85ad-62bcb3f38dee"
			 * @returns Blob(31846) {size: 31846, type: "image/jpeg"}
			 */
			getBlobByUrl(url) {
				return uni.request({
					url: url,
					// 合法值:text、arraybuffer
					responseType: 'arraybuffer'
				}).then(data=>{
					const [error, rsp]  = data;
					if(error){
						console.error(`post-error:${error}, url:${url}`)
						return {message: error.errMsg}
					}else{
						let buffer = rsp.data
						// ArrayBuffer(185) {}
						console.log('buffer:', buffer)
						return new Blob([buffer])
					}
				})
				
				// return new Promise((resolve, reject) => {
					
				// 	let xhr = new XMLHttpRequest()
				// 	xhr.open('GET', url, true)
				// 	xhr.responseType = 'blob'
				// 	xhr.onload = function(e) {
				// 		if (this.status == 200) {
				// 			let myBlob = this.response

				// 			// let file = new window.File(
				// 			// 	[myBlob],
				// 			// 	'myfile.jpg', {
				// 			// 		type: myBlob.type
				// 			// 	}
				// 			// )
				// 			// console.log("files:", file)
				// 			resolve(myBlob)
				// 		} else {
				// 			reject(false)
				// 		}
				// 	}
				// 	xhr.send()
				// })
			},

blobUrl、blob、base64、file相互转化:https://www.cnblogs.com/jing-zhe/p/15402775.html

uniapp选择file

javascript 复制代码
<button @click="submit()" type="warn" class="submit" :loading="loading" :disabled="loading">发布</button>

submit() {
    let utils = new Utils()
    uni.chooseImage({
        count: 6, //默认9
        sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
        sourceType: ['album'], //从相册选择
        success: function(res) {
            console.log(JSON.stringify(res.tempFilePaths));
            console.log(res.tempFiles)

            utils.getFileMD5(res.tempFiles[0], function(md5) {
                console.log('md5:', md5)
            })
        }
    });
   
},
相关推荐
fruge1 分钟前
前端正则表达式实战合集:表单验证与字符串处理高频场景
前端·正则表达式
baozj6 分钟前
🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器
前端·javascript·vue.js
用户40993225021214 分钟前
为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?
前端·ai编程·trae
海云前端115 分钟前
Vue首屏加速秘籍 组件按需加载真能省一半时间
前端
蛋仔聊测试17 分钟前
Playwright 中route 方法模拟测试数据(Mocking)详解
前端·python·测试
零号机28 分钟前
使用TRAE 30分钟极速开发一款划词中英互译浏览器插件
前端·人工智能
疯狂踩坑人1 小时前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
Mintopia1 小时前
🚀 共绩算力:3分钟拥有自己的文生图AI服务-容器化部署 StableDiffusion1.5-WebUI 应用
前端·人工智能·aigc
街尾杂货店&1 小时前
CSS - transition 过渡属性及使用方法(示例代码)
前端·css
CH_X_M1 小时前
为什么在AI对话中选择用sse而不是web socket?
前端