zoukankan      html  css  js  c++  java
  • 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,缓存数据了,刨析源码中...
    
    
  • 相关阅读:
    中介者模式
    Redis安装
    观察者模式
    第三天:创建型模式--建造者模式
    第二天:创建型模式--抽象工厂模式
    第一天:创建型模式--工厂方法模式
    17天17个Python设计模式--目录
    Python模拟登陆新版知乎
    Flask架站基础篇(八)--SQLAlchemy(2)
    Flask架站基础篇(七)--SQLAlchemy(1)
  • 原文地址:https://www.cnblogs.com/zhangliang91/p/11912991.html
Copyright © 2011-2022 走看看