文章目录

  • 前言(可不看)
  • 1. 简介
  • 2. 必要了解项
    • 2.1 资源术语
    • 2.2 常用SDK
  • 3. 准备工作
    • 3.1 创建bucket
    • 3.2 设置跨域规则
    • 3.3 创建RAM子账户及配置权限
    • 3.4 创建RAM角色
  • 4. 后端配置及提供token操作
    • 4.1 配置文件
    • 4.2 OssGetToken
  • 5. 前端(html)
  • 6. 总结后记
    • 参考文章:

前言(可不看)

最近一段时间领导让我跟踪研究一下云服务系统的文件上传功能。问题的背景是,①文件一旦超过100M以后上传耗时就变得很长;②超过500M以后出错的几率大大增加,用户体验极其不友好。
已知旧版本在这一块使用的ota上传功能采用的是阿里的upload.js + plupload插件实现的。
本篇优化使用STS,web端从后台获取临时授权token,调有用aliyun-oss-sdk api来实现。

<dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-sts</artifactId><version>2.1.6</version>
</dependency>
<dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>3.2.10</version>
</dependency>

1. 简介

  • 什么是oss ?
    阿里云对象存储服务(Object Storage Service,简称OSS),是阿里云对外提供的海量、安全、低成本、高可靠的云存储服务。我们在上传文件这一功能方面,总不可避免会遇到文件太大上传太慢乃至失败问题,oss就是阿里对该问题提供的解决方案。

  • 更详细介绍
    参考阿里云链接:oss产品简介

2. 必要了解项

2.1 资源术语

中文 英文 说明
存储空间 bucket 存储空间是您用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。对应到阿里云上就是你的bucket桶空间,用来存放文件等资源。
对象/文件 Object 对象是 OSS 存储数据的基本单元,也被称为OSS的文件。对象由元信息(Object Meta)、用户数据(Data)和文件名(Key)组成。对象由存储空间内部唯一的Key来标识。
地域 Region 地域表示 OSS 的数据中心所在物理位置。您可以根据费用、请求来源等综合选择数据存储的地域。详情请查看OSS已经开通的Region。
访问域名 Endpoint Endpoint 表示OSS对外服务的访问域名。OSS以HTTP RESTful API的形式对外提供服务,当访问不同地域的时候,需要不同的域名。通过内网和外网访问同一个地域所需要的域名也是不同的。具体的内容请参见各个Region对应的Endpoint
访问密钥 AccessKey AccessKey,简称 AK,指的是访问身份验证中用到的AccessKeyId 和AccessKeySecret。OSS通过使用AccessKeyId 和AccessKeySecret对称加密的方法来验证某个请求的发送者身份。AccessKeyId用于标识用户,AccessKeySecret是用户用于加密签名字符串和OSS用来验证签名字符串的密钥,其中AccessKeySecret 必须保密

(PS: 我们本篇介绍的上传方式就要用到上述术语)

2.2 常用SDK

有很多,比如 Java、Node.js、Browser.js、等等。
重点介绍一下与本篇相关的sdk。

  • Browser.js
  1. 使用前提:存储空间已配置跨域共享(CORS)规则(后面会详细给出)。
  2. 需要后端读取配置文件然后生成临时STSToken 返回给前端,前端才可以继续调用接口实现上传。

3. 准备工作

3.1 创建bucket

(我是优化的,所以我司服务器已有一堆bucket,这里贴出来方法)
登录云账户,搜索框输入 对象存储OSS跳转进入页面,点击右侧,创建bucket。

3.2 设置跨域规则

点击新建/已有的 bucket名称进入,设置跨域规则。

这里,照着操作就OK:

3.3 创建RAM子账户及配置权限

!!! AccessKey Secret 只在第一次创建时可见!记不住丢了后面可别找我要哦!!!

进入RAM访问控制——>用户——>创建用户。勾选 api调用,名称随意(假设命名为STS)。

创建好后,要给它分配权限:

点击名称进入详情——>查看权限管理——>新增授权,我们给它加一个支持调用阿里STS服务 AssumeRole接口的权限,这样后台才可以凭借该用户身份分配STS token给前端。

点击认证管理,我们要牢牢记住该用户的 AccessKeyId 和 AccessKey Secret ,尤其secret,只在创建用户时可见,之后忘了是无法找到的。

3.4 创建RAM角色

我们还需要在RAM控制页面 新建一个角色并分配权限。
作用:使得前端 有权限调用aliyun-oss-sdk进行文件上传(写入桶空间)。
点击创建角色,这里我命名为:AliyunRAM-OSSRole,你们随意。



角色即我们上面的角色名。创建完成。

然后点击名称进入,我们还得分配权限,什么权限呢?

当然是以上两个针对OSS管理/访问的系统策略咯~

注:还可以自定义权限策略并分配给该角色,我偷懒了,没有,在这里贴出来别人的一份自定义策略,只涉及上传相关的权限,可用性未知。

{"Version": "1","Statement": [{"Effect": "Allow","Action": ["oss:PutObject","oss:InitiateMultipartUpload","oss:UploadPart","oss:UploadPartCopy","oss:CompleteMultipartUpload","oss:AbortMultipartUpload","oss:ListMultipartUploads","oss:ListParts"],"Resource": ["acs:oss:*:*:mudontire-test","acs:oss:*:*:mudontire-test/*"]}]
}

哈哈,到这里我们的一堆配置就OK了,接下来就是我们喜闻乐见的代码环节了!

4. 后端配置及提供token操作

4.1 配置文件

在你的config.properties(名字按你的来)中,配置如下:

oss.access_id=xxx
oss.access_secret=xxx
oss.bucket_name=xxx
oss.region=xxx

你看这里就用到专业术语了吧,不会去复习~

4.2 OssGetToken

解释:
① MDMProperies 是我的配置类,读取配置信息。
② 关于 roleArn,你在上面创建的角色基本信息里就可以看到。

③关于roleSessionName ,好像可以随意命名,记不清了,你这里跟我一样得了。(貌似为空不行)
④ 临时token有效时间 900s <= token <= 3600s
⑤ 注意 AssumeRoleRequest,jar版本不同,调用也不同。我这里是

就是我前言放的配置!

@Component
public class OssGetStsToken {private static final Logger logger = LoggerFactory.getLogger(OssGetStsToken.class);private static String accessKeyId = MDMProperies.ossBigAccessId;private static String accessKeySecret = MDMProperies.ossBigAccessSecret;private static final String roleArn = "acs:ram::xxx/aliyunram-ossrole";private static final String roleSessionName = "alice";private static final String bucketName = MDMProperies.ossBigBucketName;private static final String region = MDMProperies.ossBigRegion;/***  临时授权有效时间900-3600s,token失效时间, 单位秒*/private static final Long durationSeconds = 900L;
//    private static final String ENDPOINT = MDMProperies.ossEndpoint;private static final String ENDPOINT = "sts.aliyuncs.com";/*** 获取STStoken接口*/public static StsTokenVO getStsToken() {StsTokenVO tokenVO = new StsTokenVO();try {// 添加endpoint(直接使用STS endpoint,前两个参数留空,无需添加region ID)DefaultProfile.addEndpoint("", "", "Sts", ENDPOINT);// 进行角色授权 构造default profile(参数留空,无需添加region ID)IClientProfile profile = DefaultProfile.getProfile("", accessKeyId, accessKeySecret);// 用profile构造clientDefaultAcsClient client = new DefaultAcsClient(profile);final AssumeRoleRequest request = new AssumeRoleRequest();request.setMethod(MethodType.POST);request.setRoleArn(roleArn);  // role-Arnrequest.setRoleSessionName(roleSessionName);request.setDurationSeconds(durationSeconds);  // 3600s// 针对该临时权限可以根据该属性赋予规则,格式为json,没有特殊要求,默认为空// request.setPolicy(policy); // Optionalfinal AssumeRoleResponse response = client.getAcsResponse(request);AssumeRoleResponse.Credentials credentials = response.getCredentials();tokenVO.setAccessKeyId(credentials.getAccessKeyId());tokenVO.setAccessKeySecret(credentials.getAccessKeySecret());tokenVO.setSecurityToken(credentials.getSecurityToken());tokenVO.setBucketName(bucketName);tokenVO.setRegion(region);tokenVO.setExpiration(durationSeconds);logger.info("tokenVO——> :"+tokenVO);return tokenVO;} catch (ClientException e) {logger.error("获取阿里云STS临时授权权限失败,错误信息:"+e);throw new RuntimeException("获取阿里云STS临时授权权限失败,错误信息:" +e);}}}

StsTokenVO:

@Data
public class StsTokenVO implements Serializable {/*** 访问密钥标识*/private String accessKeyId;/*** 访问密钥*/private String accessKeySecret;/*** 安全令牌*/private String securityToken;/*** oss-bucket  桶名称*/private String bucketName;/*** token过期时间*/private Long expiration;/*** 桶所在的区域*/private String region;
}

然后剩余的 service啦、控制层接口之类的这里就省略了。因项目而异。

5. 前端(html)

后端传给前端的就是 上面的StsTokenVO的json数据。

那么我们来看前端:

<div class="row"><div id="up_wrap" style="margin-left: 420px;width: 800px;"></div><div class="col-sm-offset-5 col-sm-10"><button type="button" class="btn btn-sm btn-warning" id="pause"><i class="fa fa-close"> </i> <label class="language_label">Pause</label></button>&nbsp;<button type="button" class="btn btn-sm btn-success" id="resume"><i class="fa fa-cloud-upload"> </i> <label class="language_label">Resume upload</label></button>&nbsp;<button type="button" class="btn btn-sm btn-primary" id="submit"><i class="fa fa-check"></i><label class="language_label">Save</label></button>&nbsp;<button type="button" class="btn btn-sm btn-danger" id="closeSubmit" onclick="closeItem()"><i class="fa fa-reply-all"></i><label class="language_label">Close</label> </button></div></div>

样式如下(关闭按钮按照你们自己的逻辑来):

哦对了,还有个文件上传选择器:

<div id="firmware" class="input-group"><input type="file" class="form-control" id="fileName" multiple="true">
</div>

关于这个type=file 原生的太丑的问题,我另开一篇介绍美化。

Script引入:

<script type="text/javascript"src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.16.0.min.js"></script>
<script type="text/javascript">

剩余代码:

 var prefix = ctx + "biz/firmware_ota";var getStsTokenURL = ctx + 'biz/sts_token!token.action';var bucket = ""; // bucket 桶名称 (初始化client时获取)var region = '';  // oss bucket所在地域名称let ossClient = null;  // 定义 oss客户端实例变量let credentials = null;  // 变量接收stsTokenlet tempCheckPoint = null; //数组,记录了已经完成上传的分片及其对应的etagconst checkpoints = {};// 定义中断点//let abortCheckPoint;// 获取上传DOM。const submit = document.getElementById("submit");// 获取中断domconst pause = document.getElementById("pause");// 获取续传domconst resume = document.getElementById("resume");// 获取STS Tokenfunction getCredential() {return fetch(getStsTokenURL).then(res => {return res.json()}).then(res => {console.log(JSON.stringify(res));  // 转为json字符串credentials = res.value;console.log("credentials:"+credentials);}).catch(err => {console.error(err);});}// 客户端初始化
async function initOSSClient() {const { accessKeyId, accessKeySecret, securityToken, bucketName } = credentials;bucket = credentials["bucketName"];region = credentials["region"];//console.log("bucket = "+bucket);ossClient = new OSS({accessKeyId: accessKeyId,accessKeySecret: accessKeySecret,stsToken: securityToken,//secure:true,bucket: bucketName,region});
}const headers = {// 指定该Object被下载时的网页缓存行为。"Cache-Control": "no-cache",// 指定该Object被下载时的名称。//"Content-Disposition": "example.txt",// 指定该Object被下载时的内容编码格式。"Content-Encoding": "utf-8",// 指定过期时间,单位为毫秒。//Expires: "1000","Access-Control-Allow-Origin": "*",// 指定Object的存储类型。//"x-oss-storage-class": "Standard",// 指定Object标签,可同时设置多个标签。"x-oss-tagging": "Tag1=1&Tag2=2",// 指定初始化分片上传时是否覆盖同名Object。此处设置为true,表示禁止覆盖同名Object;存在相同会报错"x-oss-forbid-overwrite": "true","Content-Type": 'application/x-www-form-urlencoded'};const options = {// 获取分片上传进度、断点和返回值。progress: (p, cpt, res) => {tempCheckPoint = cpt;console.log(p);//console.log(tempCheckPoint);  // 测试输出part和etag},// 设置并发上传的分片数量。parallel: 4,// 设置分片大小。默认值为1 MB,最小值为100 KB。partSize: 1024 * 1024 * 1,  // 设为1MBheaders,// 自定义元数据,通过HeadObject接口可以获取Object的元数据。//meta: { year: 2020, people: "test" },mime: "text/plain",timeout: 120000  // 设置超时时间
};// 普通上传async function commonUpload(file) {if (!ossClient) {await initOSSClient();}const fileName = file.name;$("#fileLength").val(file.size);return ossClient.put(fileName, file, {headers: { 'x-oss-forbid-overwrite': true }}).then(result => {console.log(`Common upload ${file.name} succeeded, result === `, result)const url = `https://${bucket}.${region}.aliyuncs.com/${fileName}`;// 获取下载文件的url$("#firmwareUrl").val(url);$.operate.saveTab(prefix + "!doAddInfo.action", $('#form-info-add').serialize());  // 保存上传记录到后台数据库console.log("save success...");}).catch(err => {console.log(`Common upload ${file.name} failed === `, err);});}// 分片上传let retryCount = 0;let retryCountMax = 3;const uploadFile = function uploadFile(client) {if (!ossClient || Object.keys(ossClient).length === 0) {ossClient = client;}}async function multipartUpload(file) {if (!ossClient) {await initOSSClient();}const fileName = file.name;$("#fileLength").val(file.size);return ossClient.multipartUpload(fileName, file, {parallel: options.parallel,partSize: options.partSize,progress: onMultipartUploadProgress,headers,timeout: 120000  // 设置超时时间2min}).then( result => {console.log("upload success: ", result);const url = `https://${bucket}.${region}.aliyuncs.com/${fileName}`;console.log(`Multipart upload file ${file.name} success, url = `, url);$("#firmwareUrl").val(url);  // 赋值url$.operate.saveTab(prefix + "!doAddInfo.action", $('#form-info-add').serialize());  // 保存上传记录到后台数据库,我自己的需求。这一步看你们需求,只上传就没必要console.log("save success...");ossClient = null;}).catch(err => {if (ossClient && ossClient.isCancel()) {console.log("stop-upload!");} else {console.log(checkpoints);console.log(`Multipart upload ${file.name} failed === `, err);// retry 重试机制 3次if (retryCount < retryCountMax) {retryCount++;console.error("retryCount: " + retryCount);uploadFile('');}}})}var oldDate = null;// 分片上传进度改变回调async function onMultipartUploadProgress(progress, checkpoint) {//console.log(`${checkpoint.file.name} 上传进度 ${progress}`);checkpoints[checkpoint.uploadId] = checkpoint;let ps = parseInt((progress.toFixed(2)) * 100);//console.log("上传进度:", ps + '%');//console.log("cpt:", checkpoint);let html = '';html = '<dl><dt></dt><dd><div class="progress"><div  class="progress-bar"  role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:' + ps + '%"><span>' + ps + '%</span></div></div></dd></dl>';$("#up_wrap").html(html);// 判断STS Token是否将要过期,过期则重新获取const { expiration } = credentials;//console.log("token 过期时间:"+expiration);//console.log("oldDate:"+oldDate);if (oldDate === null) {oldDate = new Date().getTime() + (expiration * 1000);  //localStorage.setItem('tokenTime', oldDate);//console.log("tokenTime:"+oldDate);}let tokenTime = localStorage.getItem("tokenTime");//console.log("localStorage tokenTime:"+tokenTime);if (tokenTime !== null){const tempTime = 60 * 1000;if ((tokenTime - new Date().getTime()) <= tempTime) {  // 距离token过期小于1min时暂停上传重新获取token后再续传console.log(`STS token will expire in ${tempTime/(60*1000)} minutes,uploading will pause and resume after getting new STS token`);if (ossClient) {ossClient.cancel();}await getCredential();await resumeMultipartUpload();}}// 断点续传async function resumeMultipartUpload() {Object.values(checkpoints).forEach((checkpoint) => {const { uploadId, file, name } = checkpoint;console.log("uploadId:"+uploadId);console.log("file:"+file);console.log("name:"+name);ossClient.multipartUpload(uploadId, file, {parallel: options.parallel,partSize: options.partSize,progress: onMultipartUploadProgress,checkpoint}).then(result => {console.log('before delete checkpoints === ', checkpoints);delete checkpoints[checkpoint.uploadId];console.log('after delete checkpoints === ', checkpoints);const url = `https://${bucket}.${region}.aliyuncs.com/${name}`;console.log(`Resume multipart upload ${file.name} succeeded, url === `, url)$("#firmwareUrl").val(url);$.operate.saveTab(prefix + "!doAddInfo.action", $('#form-info-add').serialize());  // 保存上传记录到后台数据库console.log("save success...");}).catch(err => {console.log('Resume multipart upload failed === ', err);});});}submit.addEventListener("click", async () => {try {const file = document.getElementById("fileName").files[0];//console.log("data=" +data);//采用时间戳重命名var last=file.name.substr(file.name.lastIndexOf("."),file.name.length)var fileName=Date.parse(new Date()) + last;console.log("file name:"+file.name);console.log("submit filename="+fileName);console.log("file Size: "+file.size);console.log("file Type: "+file.type);// 获取STS Tokenawait getCredential();await initOSSClient();// 如果文件大小小于分片大小,使用普通上传,否则使用分片上传if (file.size < options.partSize) {console.log("普通上传...");await commonUpload(file);} else {console.log("大于100M大文件分片上传...");await multipartUpload(file);}} catch (err) {console.log(err);}
});pause.addEventListener("click", () => {// 暂停上传if (ossClient) ossClient.cancel();console.log("暂停上传......");
})// 监听续传按钮,单击“恢复上传”后继续上传
resume.addEventListener("click", async () => {console.log("断点续传中......");await resumeMultipartUpload();
})

6. 总结后记

  1. 强烈建议不要直接将 AccessKey 密钥信息暴露在前端,安全性问题,老老实实使用后端提供STS Token的方法来保证。
  2. 关于Java SDK,新建ossClient有多种方式,使用OSS域名、使用自定义域名、专有云或专有域环境、使用IP和使用STS。
  3. 关于Browser.js,使用它的前提限制必须是 STS,涉及到RAM子账户和角色。即,如果你想用阿里云主账户的密钥,抱歉,不行。
  4. 有关实践过程遇到的问题,可以参考我这篇记录:错误记录
  5. 更新1条,web删除上传记录,同步删除存储空间的文件 这个操作,采用的是后端new ossClient删除的,经测试,初始化client参数获取云账户 主账户的accesskeyId、accessKeySecret就可以同步删除(这里不是必须RAM子账户的密钥)
  6. 更新1条,分片上传规定分片大小 100kb ~ 5GB,分片数量 <=10000,最大上传文件<= 48.8TB。自己测试当上传文件超出 最大分片数量1W时,并没有报错抛出异常,而是会自动重新设置分片大小来保证在1W分片数量内,使得成功上传。例子如下:

2022.11.10 再次更新1条:

  1. 最近有地方遇到网络特别差的情况下,上传文件用时超过了我在后台获取stsToken设置的有效期15min。然而按照我上述给出的代码,即:
if ((tokenTime - new Date().getTime()) <= tempTime) {  // 距离token过期小于1min时暂停上传重新获取token后再续传console.log(`STS token will expire in ${tempTime/(60*1000)} minutes,uploading will pause and resume after getting new STS token`);if (ossClient) {ossClient.cancel();}await getCredential();await resumeMultipartUpload();
}

异步获取后台新的stsToken失败,出现错误,调试web发现并没有获取到新的token。修改无果,最终决定改掉这个逻辑,发现ali-oss有更简便的方式来实现自动重新获取stsToken 。(PS:我上面的方式也是参考网上资料的,是通过获取后台token携带的有效期在web端判断1min内过期然后人为再次调用获取token的getCredential方法。但是实际上行不通,应该是异步方法并没有真正的异步执行吧,然后因为是在进度改变回调方法里,感觉不伦不类的。(doge))

!!!那么重点来了,以我上述程序为基础,修改部分如下:
① 去掉onMultipartUploadProgress方法里的判断过期重获token这一段代码:

// 判断STS Token是否将要过期,过期则重新获取const { expiration } = credentials;//console.log("token 过期时间:"+expiration);//console.log("oldDate:"+oldDate);if (oldDate === null) {oldDate = new Date().getTime() + (expiration * 1000);  //localStorage.setItem('tokenTime', oldDate);//console.log("tokenTime:"+oldDate);}let tokenTime = localStorage.getItem("tokenTime");//console.log("localStorage tokenTime:"+tokenTime);if (tokenTime !== null){const tempTime = 60 * 1000;if ((tokenTime - new Date().getTime()) <= tempTime) {  // 距离token过期小于1min时暂停上传重新获取token后再续传console.log(`STS token will expire in ${tempTime/(60*1000)} minutes,uploading will pause and resume after getting new STS token`);if (ossClient) {ossClient.cancel();}await getCredential();await resumeMultipartUpload();}}

② 修改初始化 oss客户端的方法:

// 初始化oss客户端增加token过期重新获取
async function initOSSClient() {const { accessKeyId, accessKeySecret, securityToken, bucketName } = credentials;bucket = credentials["bucketName"];region = credentials["region"];//console.log("bucket = "+bucket);ossClient = new OSS({accessKeyId: accessKeyId,accessKeySecret: accessKeySecret,stsToken: securityToken,secure:true,bucket: bucketName,region,refreshSTSToken: async () => {await getCredential();//console.log("credentials = "+JSON.stringify(credentials))return {accessKeyId: credentials["accessKeyId"],accessKeySecret: credentials["accessKeySecret"],stsToken: credentials["securityToken"],}},refreshSTSTokenInterval: 900 * 1000   // 单位ms,这里设置15min自动获取新的token});
}

这两个参数是sdk提供的,所以根本不需要我们自己主动重新获取,只要设置好refreshSTSTokenInterval,然后就不用管了。这才是简便易用的打开方式!已经过实测,超过15min会获取新的token,各位实测的时候取消console注释,上传过程中断网,超过有效期再联网恢复上传,web控制台会有输出console内容。文件顺利续传成功。

  1. 好奇如何可以超过最长1小时有效期,提了个工单得到的答复如下,记录一下:

DurationSeconds 默认最大是 3600s 也就是一小时,如果要超过一小时,需要设置 CreateRole或UpdateRole 接口中MaxSessionDuration 的时间

===================================================================================

参考文章:

  1. ali-oss简单上传和分片上传
  2. 阿里云oss 前端通过stsToken,普通上传、分片上传、断点续传(单文件和批量上传)
  3. ali-oss官方文档
  4. 大文件上传阿里oss
  5. 前端oss上传vue完整版
  6. 阿里云oss文件分片断点续传
  7. 阿里云oss前后端实现

完整记录一下Web前端直传阿里OSS大文件+采用后端临时授权传stsToken的方式相关推荐

  1. 【vue】 前端 基于 vue-simple-uploader 实现大文件断点续传和分片上传

    文章目录 一.前言 二.后端部分 新建Maven 项目 后端 pom.xml 配置文件 application.yml HttpStatus.java AjaxResult.java CommonCo ...

  2. web直传阿里OSS

    web直传阿里OSS 推荐观看官方文档: 安装:https://help.aliyun.com/document_detail/64041.html OSS鉴权详解:https://help.aliy ...

  3. 阿里OSS对象存储,实现图片上传进度显示ProgressListener;

    想了解阿里OSS对象存储,实现图片上传的内容的可看我的另一篇博客,博客中有完整代码,这篇博客是以上一篇阿里OSS对象存储博客为基础,只写一些与进度有关的内容,细心往下看js代码中有需要注意的地方! 实 ...

  4. Web前端开发技术课程大作业,期末考试

    Web前端开发技术课程大作业,期末考试 作业要求 最终界面 部分代码呈现 index.html login.html index.css login.css swithpic.js 完整代码素材下载 ...

  5. 0基础怎么学web前端?新手到大神的进阶路线在这!

    结合个人经历总结的前端入门方法,总结从零基础到具备前端基本技能的道路.学习方法.资料.由于能力有限,不能保证面面俱到,只是作为入门参考,面向初学者,让初学者少走弯路. 互联网的快速发展和激烈竞争,用户 ...

  6. 利用阿里云OSS对文件进行存储,上传等操作

    --pom.xml加入阿里OSS存储依赖 <!--阿里云OSS存储--> <dependency><groupId>com.aliyun.oss</group ...

  7. 阿里OSS对象存储,实现图片上传代码;

    一.注册阿里云账号,购买OSS服务 获取 : 连接区域地址endpoint :需要存储的bucketName:图片保存路径picLocation :连接keyId:accessKeyId :连接秘钥a ...

  8. 初学者应该怎么学习前端?web前端的学习路线大剖析

    最近总是会看到后很多人会问,我现在想学习Web前端开发,该如何下手,学习路线是怎样的?作为一个过来人,为了让新手程序员少走点弯路,这里就分享一些快速学习前端开发的经验以及我自己对前端学习的理解,教你如 ...

  9. web前端的进阶路线大剖析!初学者如何迅速“升级”!

    优秀的Web前端开发工程师要在知识体系上既要有广度和深度!应该具备快速学习能力. 前端开发工程师不仅要掌握基本的Web前端开发技术,网站性能优化.SEO和服务器端的基础知识,而且要学会运用各种工具进行 ...

最新文章

  1. [专题总结]AC自动机
  2. python处理excel的工具-基于Python的Excel处理工具
  3. 计算机二级考试基础知识总结,全国计算机等级考试二级公共基础知识总结
  4. sqlserver实验心得体会_sqlserver 关于DBCC CHECKDB的总结
  5. 域名解析是否生效实时检测(阿里云DNS检测)
  6. UVa 297 四分树
  7. Pycharm设置utf-8自动显示
  8. Linux杂碎2/SHELL
  9. 自定义Stack接口
  10. 刚刚,Google 官方发布了 2 份编程指南,干货十足!
  11. 【项目管理】项目管理发展的新阶段——PRINCE2项目管理方法
  12. adb下载安装教程(已安装Android studio)
  13. QT使用AES加密解密
  14. 9.mysql SQL面试题
  15. QMap QList的安全删除操作
  16. SPRING IN ACTION 第4版笔记-第八章Advanced Spring MVC-003-Pizza例子的基本流程
  17. linux系统mysql报err1055_MySQL Err 1055的解决
  18. java 查看内存_java 内存查看工具
  19. 浅谈单片机、ARM和DSP的异同
  20. 【nginx详解】nginx配置文件详细解析以及模板

热门文章

  1. 小白项目初尝试——全民飞机大战初期
  2. 国产 ETL工具 ETL产品 数据交换系统
  3. 蓝桥杯练习题 <42点问题> 枚举法
  4. 量化标杆文艺复兴公司正在抛弃这种交易策略
  5. Mapv 偏移后的图标导致点击错位的解决方法
  6. php mysql iconv_freeBSD 安装php扩展:iconv
  7. python直接获得文件夹下子目录的文件名
  8. cmd中编辑文本文件
  9. 易语言 取无标题的窗口中编辑框句柄
  10. 某大型保险集团在线财险业务系统数据库存储架构由集中式向分布式转型实践