mongodb + indexdb + Element el-upload图片上传总结

440 阅读3分钟

1.存储结构设计##

1.1后端mongodb中存储结构

由于mongodb数据库中每条记录存储不得超过16M,所以不能以工程Id存储一条记录,否则工程中上传的图片超过16M之后数据库就崩溃。

到mongodb的另一个存储机制 ---- 无返回码. 在 < mongodb 权威指南> 一书中, 作者称之为离弦之箭. 什么意思呢, 就是mongodb的插入,删除等操作, 客户端向数据库发出请求之后, 是不需要等待数据库返回操作是否成功的返回结果. 这也是mongodb插入,更新等操作速度快的原因. 这就导致, 当单个文件超过16M之后, 程序并不会报错, 但此时, 数据已经无法插入数据库了.

所以我们确定为一张图片作为一个文档,文档中包括 的字段"_id", "image" : {"contenType" : "image/png", "description" , "content":{} , "originName"},"bqItemID" , "id" , "userID", "projectID"

其中"image"对应的也是一个文档,在后端可以理解为一个JSON对象(图片对象),content存储的为图片转化为二进制后的内容,"_id"为mongo自动生成的,"id"在后端生成,(UUID生成的sId),由于我们设计的图片上传功能是一条清单可上传多张图片,所以为每张图片生成一个Id后将其放到数组中(idArray.add(sID);)并返回给前端idArray。

1.1.1后端上传代码如下,

 public String submitReferFile(MultipartFile[] files, String userID, String projectID, String bqItemID) {
        JSONObject ret = new JSONObject();
        try {
            // 构造提交对象数组
            JSONArray arrayItems = new JSONArray();
            JSONArray idArray = new JSONArray();
            for (int j = 0; j < files.length; j++) {
                JSONObject item = new JSONObject();
                item.put(EBQEditOtherConst.pfnUserID, userID);
                item.put(EBQEditOtherConst.pfnProjectID, projectID);
                item.put(EBQEditOtherConst.pfnBqItemID, bqItemID);
                String sID = UUID.randomUUID().toString();
                item.put(EBQEditOtherConst.pfnID, sID);
                idArray.add(sID);

                JSONObject image = new JSONObject();
                image.put(EBQEditOtherConst.pfnDescription, files[j].getName());
                image.put(EBQEditOtherConst.pfnOriginName, files[j].getOriginalFilename());
                image.put(EBQEditOtherConst.pfnContentType, files[j].getContentType());
                image.put(EBQEditOtherConst.pfnContent, new Binary(files[j].getBytes()));
                item.put(EBQEditOtherConst.pfnImage, image);
                arrayItems.add(item);
            }

            boolean bRet =  mongoDao.insertMany(EBQEditOtherConst.ptnBqItemReferFile, arrayItems);
            if(bRet){
                return EBQApiRetBuilder.buildResult(EBQHttpStatus.OK, true, idArray,"提交成功!");
            } else {
                return EBQApiRetBuilder.buildResult(EBQHttpStatus.FORBIDDEN, false,"提交失败,请重试!!");
            }
        }
        catch (IOException e) {
            return EBQApiRetBuilder.buildResult(EBQHttpStatus.INTERNAL_SERVER_ERROR, false, "提交失败,请重试!");

        }
    }

上面代码注意图片转化为二进制的方式(new Binary)

image.put(EBQEditOtherConst.pfnContent, new Binary(files[j].getBytes()));​

存储之后调试代码发现"content"字段对应的也是一个对象其中有两个属性“data”和“type”,data里面存储的是真正的图片二进制数据

1.1.2 后端返回图片二进制代码

本文将图片二进制放在HttpServletResponse的响应正文里

@ResponseBody
    @GetMapping(value = "referFileData/{id}")
    public void getReferFile(@PathVariable("id") String id, HttpServletRequest request, HttpServletResponse response) {
        referFileService.getReferFile(response, id);
    }
    
    
    referFileService中的方法
    @Override
    public void getReferFile(HttpServletResponse response, String sID) {
        Document document = mongoDao.findByID(EBQEditOtherConst.ptnBqItemReferFile, sID);
        JSONObject item = JSONObject.parseObject(document.toJson());
        JSONObject image =  item.getJSONObject(EBQEditOtherConst.pfnImage);

        JSONObject imageContent = image.getJSONObject(EBQEditOtherConst.pfnContent);
        byte[] imageBytes = "".getBytes();
        if (imageContent.containsKey(EBQEditOtherConst.pfnBianry)) {
           imageBytes =  imageContent.getBytes(EBQEditOtherConst.pfnBianry);
        } else if (imageContent.containsKey(EBQEditOtherConst.pfnData)) {
            imageBytes =  imageContent.getBytes(EBQEditOtherConst.pfnData);
        }
        response.setContentType(image.getString(EBQEditOtherConst.pfnContentType));  //设置图片格式
        boolean bRet = false;
        try {
            OutputStream out = response.getOutputStream(); // 打开输出流
            out.write(imageBytes);  //输出图片
            out.flush();	//输出
            out.close();  //关闭输出
        }
        catch (IOException e) {

        }
    }

1.2前端indexdb存储结构

由于本项目中一个前端工程对应indexdb数据库中一个对象仓库(project)的一条记录(indexdb学习请移步www.ruanyifeng.com/blog/2018/0… ,并且大部分数据保存在data中,所以我们将工程所属的图片存储在data里面的bqItem_referFile对应的数组中,数组中的每一个元素是一个json数组结构,每个json对象有两个属性,其中一个为Id,等于清单的的bqItemId,另外一个属性为values,其对应的是数组,数组元素为后端生成的图片sId(参见前面后端代码),下图为前端工程一条工程记录的结构图

2.el -upload 在前端的使用##

2.1上传与读取

<el-dialog custom-class="bqItemReferFile" :title="bqItemReferFile.description" 
            :visible.sync="dialogShowFlg" v-if="dialogShowFlg" :closeOnClickModal="false" v-drag>
            <div class="dialog-center">
              <p>注:图片支持jpg、jpeg、png格式;图片大小不能超过512KB;每条清单最多上传4张图片</p><br><br>
              <el-upload
              :multiple="multiple"
              :limit="4"
              :on-exceed="handleExceed"
              :file-list="images"
              action="http://127.0.0.1:9001/edit/other/referFileData/"
              accept='image/jpg,image/jpeg,image/png'
              list-type="picture-card"
              :on-preview="handlePictureCardPreview"
              :before-remove="onBeforeRemove"
              :on-remove="handleRemove"
              :auto-upload="true"
              :before-upload="onBeforeUpload"
              :http-request="uploadFile"
              :disabled="fbfxItem.Locked"
              v-bind:class="{ 'upload-hide': curReferFile.value.length >= 4 || fbfxItem.Locked}"
              ref="upload">
              <i class="el-icon-plus"></i>

              </el-upload>
            </div>
            <div slot="footer" class="dialog-footer">
              <el-button size="large" @click="closeDialog">取消</el-button>
            </div>
       </el-dialog>
       <el-dialog :visible.sync="dialogVisible" v-drag>
            <img width="100%" :src="dialogImageUrl" alt="">
       </el-dialog>

:file-list="images"其中的images 里面可以存储图片的name和URL 信息(本项目使用时直接将后端生成的sId作为name对应的信息),所以我们选择该属性主动操作images数组

:http-request="uploadFile",我们在uploadFile方法中写上传代码如下

uploadFile(file) {
        let vueObj = this;
        vueObj.formData = new FormData(); 
        vueObj.formData.append('file', file.file);
        vueObj.formData.append('userID', vueObj.userID);
        vueObj.formData.append('projectID', vueObj.projectID);
        vueObj.formData.append('bqItemID', vueObj.bqItemID);

        let image = {};
        vueObj.images.push(image);//由于下面操作是异步的所以在此先装入空的image,否则上传图片会抖动
        
        request().submitReferFile(vueObj.formData, function(retData){//上传到后端
          let ret = JSON.parse(retData);
          ret.data.forEach(pictureId => {
            image.name = pictureId;
            image.url = process.env.API_EDIT_ROOT + 'other/referFileData/' + pictureId;
          
        });
          vueObj.$message(ret.message);
          vueObj.submitFileIDToProject(ret.data);//保存到前端工程         
        });
      },

请注意如下两行代码(预防图片抖动):

let image = {}; vueObj.images.push(image);//由于下面操作是异步的所以在此先装入空的image,否则上传图片会抖动 保存到前端工程的方法如下:

submitFileIDToProject: function(imageIdList) {
        let vueObj = this;
        if (vueObj.curProject.data == null) {
          return;
        }
        if (vueObj.curProject.data.bqItem_referFile == undefined) {
          vueObj.curProject.data.bqItem_referFile = [];
        } 
        let bqItemData = vueObj.curProject.data.bqItem_referFile.find(p => p.id == vueObj.bqItemID);
        if (bqItemData == null) {
          let bqItem = {};
          bqItem.id = vueObj.bqItemID;         
          bqItem.value = imageIdList;
          vueObj.curProject.data.bqItem_referFile.push(bqItem);
          vueObj.curReferFile = bqItem;
        } else {
          bqItemData.value = bqItemData.value.concat(imageIdList);
        }
        db().save();//保存到前端数据库
      },

后端生成的imageId(sId)保存在了前端工程数据库中,以后点击清单直接从前端数据库中读取image的Id信息就可以了,(当然工程第一次所有前端工程里面的内容(data)也是从对应的后端数据库中取出来的。)详情见如下:

created: function() {
        let vueObj = this;  
        vueObj.$on('show-dialog',function(record, onSuccess){       
          vueObj.onSuccess = onSuccess;
          vueObj.fbfxItem = record;
          vueObj.bqItemID = vueObj.fbfxItem.id;
          vueObj.projectID = db().getProjectID(); 
          vueObj.Locked = record.Locked;    
          vueObj.images = [];
          vueObj.curProject =  db().getCurProject();
          vueObj.init();
          vueObj.dialogShowFlg = true;   
        });
    },

主要是在init方法里面加载图片Id(用图片的Id构造每张图片唯一的URL)

init: function() {
        let vueObj = this;
        vueObj.curReferFile= {value: []};
        if (vueObj.curProject == null || vueObj.curProject.data.bqItem_referFile == undefined) {
          return;
        }
        let bqItemData = vueObj.curProject.data.bqItem_referFile.find(p => p.id == vueObj.bqItemID); 
        if (bqItemData == undefined) {
          return;
        }
        vueObj.curReferFile = bqItemData;
        if (bqItemData.value) {
           bqItemData.value.forEach(pictureId => {
              let image = {name : pictureId, url :  process.env.API_EDIT_ROOT + 'other/referFileData/' + pictureId};
              vueObj.images.push(image);
            });
        }
      },

2.2 el-upload钩子函数

这个控件最坑的是在onBeforeUpload钩子函数返回false后会进入onBeforeRemove,而onBeforeRemove返回true 后竟然会进入handleRemove钩子删除images数组里面的元素造成显示图片无故消失。所以我们在删除钩子做了一个判断如下

handleRemove(file, fileList) {
        let vueObj = this;
        if(vueObj.images.indexOf(file) < 0){//不做此判断会无故删除images元素
          return;
        }
        if(!vueObj.Locked) {
          let param = {'ids' : [file.name]};
          let oldLength = vueObj.images.length;
          vueObj.images = vueObj.images.filter(item => item.name != file.name);
          request().deleteReferFile(param, function() {
            vueObj.deleteFileIDFromProject(file.name);
          });
        }       
      },

2.3 el-upload图片显示问题

图片上的预览符号变成了方块,需要主动在css文件加上

.el-icon-zoom-in:before{ content: "\e62a"; }