vue+node实现拖拽上传图片

效果图

首先介绍几个要用到的知识点,源码地址在最底部,不想看文字的同学可以直接拉到最底部下载。

一、知识点

实现拖拽上传需要用到的知识点如下:

前端
拖拽事件
dataTransfer
FileReader
FormData
progress

后端
multer
fs.renameSync

1.H5拖拽事件

我们拖动图片放到一个div上时,下列事件会依次发生:

  • dragenter
  • dragover
  • dragleave或drop

只要图片被拖动到div上,就会触发dragenter事件,类似于mouseover事件。
紧接着是dragover事件,而且只要图片在div内移动,就会不停的触发。
如果拖出了div的范围,dragover事件不再发生,但会触发dragleave事件。
如果你拖着图片在div上松手了,就会触发drop事件。

2.dataTransfer对象

只有简单的拖放而没有数据变化是莫得用的。我们拖个图片进来的目的是啥?当然是为了获得图片数据,这样才能然后传到服务器上去。于是有了dataTransfer对象,它是事件对象的一个属性,用于从被拖动元素向放置目标传递字符串格式的数据。我们要完成拖拽上传图片就得靠这个对象,因为它是事件对象的属性,所以我们只能在事件处理程序中使用,参考以下代码:

//在drop事件中使用dataTransfer对象
 onDrop: function(e) {
            console.log("松手");
            var dt = e.dataTransfer;
          }

3.FormData

MND文档

具体的用法还是得阅读MDN文档好,以下是本菜鸡读了文档后对FormData的理解:

首先明确FomeData是一个对象,由键值对组成,有个append()方法增加键值,我们可以append字符、数值或是文件

var formData = new FormData();

formData.append("name", "Ciger");
formData.append("phone", 123456); //数字123456会被立即转换成字符串 "123456"
// HTML 文件类型input,由用户选择
formData.append("userfile", fileInputElement.files[0]);

append后,我们可通过get()和set()操作对象的值

formData.get('name')  // 获取值-> Ciger
formData.set('name','Ciger2') //重置值-> Ciger2

知道如何使用这个对象了,最关键的就是它有什么用?
formData的作用有两个:

  • 用于发送表单数据,也可独立于表单使用
  • 上传文件

独立于表单使用有点抽象,我们来看代码。

<form  id="myForm">
  <input type="email" name="userid" placeholder="email"/>
  <input type="text" name="content" />
  <input type="submit" value="Stash the file!" />
</form>

代码里是一个form表单,有两个input输入框加一个提交按钮。通常来说,我们提交数据时要先获取到两个input框的数据,拼接在一起,然后通过ajax发送。
有了FormData对象,两行代码就可以实现form对象数据的拼接

var form =  var form=document.querySelector("#myForm");;
var data = new FormData(form);

上述代码的data都是form表单里的填的,独立于表单使用即代表我们可以不需要html元素,直接生成数据,然后发送给后端。

来看下面的上传文件代码

var file = e.dataTransfer.files[0]  //通过dataTransfer获取拖拽过来的文件

var formData = new FormData()
formData.append('file',file)

//ajax发送formData
...
xhr.send(formData)

4.FileReader

MDN文档

FileReader对象是用来读取文件的,我们可以通过new FileReader(file)创建一个FileReader对象,file参数代表要读取的文件,可以来自用户在一个<nput>元素上选择文件后返回的FileList对象,也可以是拖放操作生成的DataTransfer对象

FileReader有如下事件:

  • onabort
  • onerror
  • onload
  • onloadstart
  • onloadend
  • onprogress

我们可以通过这些事件实现图片的预览,代码如下:

 var fr = new FileReader();
 fr.readAsDataURL(file); 
 fr.onload = function() {
  console.log(this.result)
//这里的this指向是FileReader对象!!!
//这里的this指向是FileReader对象!!!
//这里的this指向是FileReader对象!!!

//将图片的地址src设置为this.result即可
 }

注意!!读取后的结果会存在onload事件中的this.result中!

5.进度事件(Progress)

progress事件是针对XHR操作的,会在浏览器接受新数据期间周期性的触发,而onprogress事件处理程序会接收到一个events对象,其target属性是XHR对象,但包含了三个额外的属性

  • lengthComputable 进度信息是否可用
  • position 已接收到的字节数
  • totalSize 根据Content-Length响应头部确定的预期字节数

有了这些信息,我们就可以为用户创建一个上传进度条

var xhr = createXHR();
xhr.onload = function () {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
        alert(xhr.responseText);
    } else {
        alert("Request was unsuccessful: " + xhr.status);
    }
};

//post一般用来获取上传进度
xhr.upload.onprogress = function(e) {
    if (e.lengthComputable) {
        console.log(e.loaded / e.total * 100)
    }
}

二、前端代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
    />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link
      href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <script src="https://cdn.bootcss.com/vue/2.5.13/vue.min.js"></script>
    <style>
      .dropbox {
        border: 0.25rem dashed #ddd;
        min-height: 8rem;
        display: flex;
        justify-content: flex-start;
        align-items: center;
        flex-wrap: wrap;
      }
    </style>
    <title>Document</title>
  </head>
  <body>
    <div id="app" class="m-5">
      <div class="dropbox p-3" ref="dropbox">
        <h5
          v-if="files.length===0"
          class="text-center"
          style="width:100%;color:#aaa;"
        >
          将文件拖到这里
        </h5>
        <div
          class="border m-2 d-inline-block p-4"
          style="width:15rem;flex:none;"
          v-for="(file,index) in files"
          :key="index"
        >
          <h5 class="mt-0">{{ file.name }}</h5>
          <img
            :src="file.src"
            style="width:auto;height:auto;max-width: 100%;max-height: 100%;"
          />
          <div class="progress" v-if="file.showPercentage">
            <div
              class="progress-bar progress-bar-striped"
              :style="{ width: file.uploadPercentage+'%' }"
            ></div>
          </div>
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#app",
        data: {
          files: []
        },
        methods: {
          uploadFile: function(file, url) {
            return new Promise((resolve, reject) => {
              var fr = new FileReader();
              var that = this;
              var item = {};
              fr.readAsDataURL(file);
              fr.onload = function() {
                item = {
                  src: this.result,
                  name: file.name,
                  uploadPercentage: 0,
                  showPercentage: true
                };
                that.files.push(item);
                var fd = new FormData();
                fd.append("file", file);

                var xhr = new XMLHttpRequest();
                xhr.open("POST", url, true);
                xhr.upload.addEventListener(
                  "progress",
                  function(e) {
                    if (e.loaded == e.total) {
                      item.uploadPercentage = Math.round(
                        (e.loaded * 100) / e.total
                      );
                      setTimeout(() => {
                        item.showPercentage = false;
                      }, 1000);
                    } else {
                      item.uploadPercentage = Math.round(
                        (e.loaded * 100) / e.total
                      );
                    }
                  },
                  false
                );
                xhr.onload = function() {
                  //   alert("上传完成!");
                };
                xhr.send(fd);
              };
            });
          },
          onDrag: function(e) {
            e.stopPropagation();
            e.preventDefault();
            console.log("进入");
            this.$refs.dropbox.style = "border:0.25rem dashed #007bff;";
          },
          onDragLeave: function(e) {
            e.stopPropagation();
            e.preventDefault();
            console.log("离开");
            this.$refs.dropbox.style = "border:0.25rem dashed #ddd;";
          },
          onDrop: function(e) {
            e.stopPropagation();
            e.preventDefault();
            console.log("松手");
            var url = "http://127.0.0.1:3000/upload-multiply";
            var dt = e.dataTransfer;
            for (var i = 0; i !== dt.files.length; i++) {
              this.uploadFile(dt.files[i], url);
            }
          }
        },
        mounted: function() {
          var dropbox = document.querySelector(".dropbox");
          dropbox.addEventListener("dragenter", this.onDrag, false);
          dropbox.addEventListener("dragover", this.onDrag, false);
          dropbox.addEventListener("dragleave", this.onDragLeave, false);
          dropbox.addEventListener("drop", this.onDrop, false);
        }
      });
    </script>
  </body>
</html>


代码解析

  1. HTML部分有个ref="dropbox"的div,这个就是我们的拖拽区域
<div id="app">
  <div class="dropbox p-3" ref="dropbox">...</div>
  ...
</div>

  1. JS部分,mounted的时候,对dropbox添加拖拽事件的监听
 mounted: function() {
          var dropbox = document.querySelector(".dropbox");
          dropbox.addEventListener("dragenter", this.onDrag, false);
          dropbox.addEventListener("dragover", this.onDrag, false);
          dropbox.addEventListener("dragleave", this.onDragLeave, false);
          dropbox.addEventListener("drop", this.onDrop, false);
        }

3.methods中实现这几个事件函数

upload(){} //上传文件方法
onDrag(){}
onDragLeave(){}
onDrop(){}
//具体实现看上述代码

三、后端代码

var fs = require("fs");
var express = require("express");
var multer = require("multer");

var app = express();
var upload = multer({ dest: "upload/" });
//设置跨域访问
app.all("*", function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  res.header("X-Powered-By", " 3.2.1");
  // res.header("Content-Type", "application/json;charset=utf-8");
  next();
});

// 多图上传
app.post("/upload-multiply", upload.array("file", 2), function(req, res, next) {
  var files = req.files;
  var fileName = "";
  if (files.length > 0) {
    files.forEach(item => {
      fileName = new Date().getTime() + "-" + item.originalname;
      fs.renameSync(item.path, __dirname + "\\upload" + "\\" + fileName);
    });
    res.send({ code: 1, url: "127.0.0.1:3000/upload/" + fileName });
  } else {
    res.send({ code: 0 });
  }
});

app.listen(3000);


后端需要npm install express multer
运行前要先创建upload文件夹用于存放文件

四、源码与总结

源码地址:https://github.com/C-Utopia/pratice-project.git

之前一直对H5的拖拽事件和文件上传迷迷糊糊,所以做了这个小练习。

遇到没做过的东西,首先上网搜索,例如我想实现拖拽上传,那就在百度搜索vue拖拽上传文件之类的关键词,先看看别人如何实现的,复制别人的代码下来看能不能运行,要是能成功运行则仔细阅读源码,源码有没见过的单词,如FormData、FileReader,直接上MDN看文档,了解清楚这个知识点之后再继续阅读源码。

了解清楚拖拽上传的相关知识点以及实现思路,我们再动手写代码就是水到渠成的事了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345