flask


阅读目录

一、上传文件

二、下载文件

引言

本案例前端采用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,缓存数据了,刨析源码中...

原文地址:https://www.cnblogs.com/zhangliang91/p/11912991.html