阅读目录
一、上传文件
二、下载文件
引言
本案例前端采用Vue2.0 后端Flask1.0.2,主要实现上传/下载文件功能
1 上传文件
功能需求:支持用户上传头像的功能
1.1 前端-上传功能
1.1.1 html
<div id="app">
<div class="input-box">
<input style="display: none" type="file" id="file" @change="uploadChange" />
<label class="upload-icon" for="file" v-if="!avatarUrl">+</label>
<img
v-if="avatarUrl"
class="avatar"
:src="avatarUrl"
@click="clickImg"
alt
/>
<div class="input-box-button">
<input type="button" @click="uploadImg" value="上 传" />
</div>
</div>
</div>
1.1.2 JS代码
<script>
let SIZE_NUM = 2; // 图片大小数值
const MAX_SIZE = SIZE_NUM * 1024 * 1024; // 图片最大为 5M
var app = new Vue({
el: '#app',
data: {
avatarUrl: ''
},
methods:{
uploadImg(){
let file_obj = document.getElementById("file").files[0];
// 无文件时返回
if (!file_obj) {
alert("请选择上传图片");
return false;
}
var formData = new FormData();
formData.append("username", sessionStorage.username); // 可以添加其它数据
formData.append("file", file_obj); //加入文件对象
let url = '127.0.0.1:8080' + "/user/updateAvatar";
let configs = {
headers:{'Content-Type':'multipart/form-data', 'token':''}
};
axios.post(url, formData, configs).then(function (response) {
if(response.data.code != 200){
alert("文件上传失败,无效上传文件!")
}else{
alert("上传成功!")
}
})
},
// 头像改变
uploadChange(){
let file = document.getElementById("file").files[0];
// 无文件时返回
if (!file) {
return;
}
// 判断文件类型 是否为图片
let type = file.type;
if (type.indexOf("image") === -1) {
alert("只能上传图片");
return;
}
// 判断文件大小是否超过限制
let size = file.size;
if (size > MAX_SIZE) {
alert(`只能上传 ${SIZE_NUM}M 以内的图片`);
return;
}
/*
* 可以根据需求设置怎么存放,此处提供两种方法
* 1.存在后端服务器
* 2.存在阿里oss服务器 - 推荐
* */
//1.存在后端服务器,当选择照片改变的时候只需要img src指向改变就可以
let _this = this;
var reader=new FileReader(); //调用FileReader
reader.onload=function(evt){ //读取操作完成时触发。.
_this.avatarUrl = evt.target.result; //数据驱动 img-scr改变
// document.getElementById("avatar").setAttribute('src',evt.target.result);
};
reader.readAsDataURL(file); //将文件读取为 DataURL(base64)
//2.存放在oss服务器
// 生成上传的文件名
let index = file.name.lastIndexOf(".");
let fileType = file.name.substr(index + 1).toLowerCase();
let fileName = uuid.v1() + "." + fileType;
// 执行上传 oss ,oss上传配置文件就不展示在这边了
let _this = this;
client
.multipartUpload(fileName, file)
.then(function(result) {
if (result.res.status === 200) {
console.log(result);
let imgUrl = result.res.requestUrls[0];
_this.form.avatarUrl = imgUrl.replace(
"http:",
"https:"
);
_this.form.avatarUrl = _this.form.avatarUrl.split(
"?"
)[0];
} else {
}
})
.catch(function(err) {
console.log(err);
});
console.log(file);
},
// 点击头像
clickImg() {
document.getElementById("file").click();
},
}
})
</script>
1.1.2 CSS
<style>
.input-box{
margin-left: 20%;
}
.input-box-button{
margin-top: 15px;
}
.upload-icon {
display: inline-block;
80px;
height: 80px;
border-radius: 4px;
border: 1px solid #ddd;
text-align: center;
line-height: 80px;
color: #ddd;
font-size: 30px;
font-weight: 100;
cursor: pointer;
}
.avatar {
display: inline-block;
80px;
height: 80px;
border-radius: 4px;
cursor: pointer;
}
</style>
1.2 后端-上传
def upload_avatar(self):
"""上传头像"""
"""
后端存放实现这里提供三种选择:
1.存放在数据库
2.存放在硬盘
3.如果是oss地址,直接存放数据库就行
"""
# 第一种,存放在数据服
username = request.values.get("username")
# 此处是直接把文件流经过base64编码 ,data:image/jpg;base64,用来标识为base64编码/ 这里就需要数据库字段avatar必须足够大
avatar_base64 = "data:image/jpg;base64,".encode() + b64encode(request.files['file'].stream.read())
f"update user_table set avatar={avatar_base64} where username={username}"
# 第二种存放在硬盘
file = request.files.get("file")
if not username:
return False
if file and allowed_file(file.filename):
# 用户名作为文件名称存储
filename = secure_filename(username) + "." + file.filename.split('.')[1]
# flask-config 配置 UPLOAD_DIR存放地址
file.save(os.path.join(current_app.config.get("UPLOAD_DIR"), 'avatar', filename))
return True
return False
# 以下接口服务
res = upload_avatar()
if not res:
return response.error("你上传的是个啥呀!")
else:
return repose.success("ok啦...")
1.3 后端-展示
# 这里还是区分数据库存储还是本地存储,当然avtar的信息也可以和用户的其它信息写一个接口
def show_avatar(self, username):
if not username:
return None, "未检测到登录用户名,请先登录"
dirpath = file_obj.join_path(current_app.config['UPLOAD_DIR'], 'avatar')
filename = file_obj.get_file_fill_name(dirpath, username) # 在upload目录下获取用户名称的存储文件
if not filename:
return None, "未检测到登录用户名,请先登录"
filepath = file_obj.join_path(dirpath, filename)
if not file_obj.is_exist(filepath):
return None, "不存在上传头像信息" //这里可以给默认的头像,当然前端给更好了
f = open(filepath, 'rb') # 此处也可以设置读取限制,比如每次读取多少
base64_str = b64encode(f.read())
return "data:image/jpg;base64,".encode() + base64_str, None
# 以下接口服务
file, err = show_avatar(request.get("username"))
if err:
return response.error(err)
else:
return repose.success(data=file)
1.4 前端-展示
前端只需要调用show_avatar接口替换img:scr指向就可以了
<img :src="avatarUrl" alt="">
data: {
avatarUrl: ''
},
methods:{
let params = {username: 'admin'}
let avatarUrl= config.baseUrl + "/user/showAvatar";
let configs = {
headers:{'Content-Type':'content-type', 'token':config.headers.token}
};
let _this = this;
axios.post(avatarUrl, params, configs).then(function (response) {
if(response.data.code != 200){
this.avatarUrl = response.data.data; //因为后端最终返回的不是base64结果就是oss结果,所以可以直接替换
}else{
alert(response.data.data)
}
})
}
1.5 总结
1.上面很多伪代码,但是基本要点都有体现,可以根据自己的实际项目修改
2.前端avatar最好能封装为组件形式,当用户登录以后获取后端用户信息,包含权限,头像等信息,保存在vuex,sesssioStore中
3.后端用Flask原生的也可以实现返回数据流等,方法send_from_directory(dirpath, filename, as_attachment=True)或者send_file()等前后不分离很好使,但是前后分离项目实验不出效果,郁闷~~
2 下载文件
功能需求:前端点击按钮,后端生成文件后返回前端下载
2.1 前端
2.1.1 Blob实现
exportConfig(){
# 后端接口
exportConfig(this.ids).then(res => { # res为make_response()返回结果
if(res.status === 200){
const blob = new Blob([res.data],{type:"application/zip"}); #初始化Blob都西昂
const fileName = 'execute_file.zip'; # 文件名称
if ('download' in document.createElement('a')) { // 非IE下载
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // 释放URL 对象
document.body.removeChild(elink)
} else { // IE10+下载
navigator.msSaveBlob(blob, fileName)
}
this.$message({
message: "导出成功",
type: "success"
});
}else{
this.$message.error(res.data.data)
}
})
}
2.1.1 原生a标签实现
exportConfig(){
// 采用a标签的href指定的方式
const elink = document.createElement('a');
elink.download = 'execute_file.zip';
elink.style.display = 'none';
// elink.target = '_blank';
elink.href = config.baseUrl +'/接口路径/?ids='+ JSON.stringify(this.ids[0]);
document.body.appendChild(elink);
elink.click();
URL.revokeObjectURL(elink.href); // 释放URL 对象
document.body.removeChild(elink)
}
2.2 后端
2.2.1 服务 - Services层
def services_export_file():
"""导出文件"""
obj = ConfigXxx()
res, err = obj.export()
if err:
return response.failed(data=err)
return res
2.2.2 分模块处理 - Modules层
class ConfigXxx:
def export(self):
"""导出"""
p_id = request.values.get('ids')
if not str(p_id).isdigit():
return None, f"不支持的导出参数【{p_id}】"
p_info, err = self.structure_config_data(p_id)
if err:
return None, err
file_handle = File()
# 生成文件
dirpath, err = file_handle.generate(p_info)
if err:
return None, err
export_data, err = file_handle.export_zip_file(dirpath)
if err:
return None, err
# 移除文件
file_handle.remove(dirpath)
# 核心->把生成的数据交给交给make_response处理
res = make_response(send_file(export_data, attachment_filename='execute_file.zip', as_attachment=True))
return res, None
2.2.3 文件处理
class File:
def export_zip_files(self, dirpath):
"""查询导出文件"""
import os
import zipfile
from io import BytesIO
try:
memory_file = BytesIO()
dsplit = dirpath.split('\')
dname = None
if len(dsplit) >= 2:
dname = dsplit[-2]
with zipfile.ZipFile(memory_file, "w", zipfile.ZIP_DEFLATED) as zf:
for path, dirnames, filenames in os.walk(dirpath):
if dname:
hr = path.split(dname, 1)
for filename in filenames:
zf.write(os.path.join(path, filename), os.path.join(*hr[1].split('\'), filename))
else:
for filename in filenames:
zf.write(os.path.join(path, filename))
# zf.setpassword("kk-123456".encode('utf-8'))
memory_file.seek(0)
return memory_file, None
except Exception as e:
return None, str(e)
2.3 总结
1.前端用Blob实现导出失败,具体原因暂时不详,猜测flask.make_response()生成的文件的流Blob不支持,有知道原因的大神可以回复我一下!
2.原始a标签实现可以对接flask.make_response()下载文件
3.原始a标签实现下载有个隐藏的坑,上述实现方式在后端会有缓存,前端再次访问已经下载过的文件不会触发Services层,目前原因不祥,猜测是flask.make_response()内部注册session,缓存数据了,刨析源码中...