通过前端代码,利用File API对图文发布操作进行优化,提高用户体验。
发布框是web应用的一种常见图文发布功能,在微博、评论、论坛、博客或内容管理系统等产品中经常使用。做好发布框的交互设计,能提高用户的编辑效率,提高用户体验,给产品增加锦上添花的效果。
需求背景:
在实际的项目中,遇到了以下需求,用户(运营人员)可以通过发布框发布话题相关内容,产品经理期望在此发布框上实现以下功能:
1、用户可以拖动文件,当文件进入浏览器时提示用户拖动文件到发布框;2、当拖动的文件(例如.exe)不符合要求时,给予拒绝提示,不能上传;3、当拖动文件(批量)为图片或文档时,解析图片和文字,预览(或上传),其余类型的文件拒绝;4、发布框支持图片复制、和QQ、PrintScreen键等工具的截图后粘贴(或ctrl+v)。
技术点:
- 拖放功能(drag & drop)
- File API功能
- 复制粘贴事件
drag & drop
拖放是 HTML5 中常见的功能。即:把抓取的对象拖放到其他位置(想想一下两个元素换位)。与他相关就是两个动作——拖和放。所以,它涉及到两个元素。一个是被拖的元素,称为拖放源;另一个是要放的目标,称为拖放目标。所涉及到的事件就是两类:drag(源)和drop(目标)。
与它相关的两个事件(按触发的先后顺序,参照物是鼠标指针而非文件边缘):拖拽源:
- dragstart:按下鼠标时触发
- drag:按下鼠标持续时触发 (执行多次)
- dragend:鼠标放开时触发
投放目标:
- dragenter:拖动目标且鼠标进入投放区时触发
- dragover:拖动目标且鼠标移动在投放区时触发(每隔 350 毫秒会触发一次)
- dragleave:拖动对象且离开投放区时触发
- drop:拖动对象且在投放区放开鼠标时触发(需要在dragover上设置禁止默认事件,才会有触发,奇怪的设定)
兼容性
这里我们主要用到了投放目标的drop事件,它的兼容性如下图所示:
主要代码:
遇到问题
dragenter(dragleave)的事件触发类似于mouseover(mouseout),当在子节点内外的拖动时,会触发子节点的drage事件并向上冒泡,引起多次触发当前节点的drag事件。举例来说,我们只在document上绑定dragenter事件,但是任何进出页面子标签的拖动,都会再次触发document的dragenter事件。可以通过是否包含和relatedTarget来解决。
两种方向的拖动都会触发目标节点的dragenter事件,这不是我们想要的结果!
//判断两个a中是否包含bfunction contains(a,b){ return a.contains ? a != b && a.contains(b) :!!(a.compareDocumentPosition(b) & 16);}NodeB.addEventListener("dragenter", (e) => { event.preventDefault(); let related = e.relatedTarget || e.fromElement; if ((related != NodeB) && !NodeB.contains(related)) { //do something }}, false);NodeB.addEventListener("dragleave", (e) => { e.preventDefault(); let related = e.relatedTarget || e.toElement; if ((related != NodeB) && !NodeB.contains(related)) { //do something }}, false);
event对象有一个属性叫relatedTarget,这个属性就是用来判断enter和leave事件目标节点的相关节点的属性。简单的来说就是当触发enter事件时,relatedTarget属性代表的就是鼠标刚刚离开的那个节点,当触发leave事件时它代表的是鼠标移向的那个对象。由于IE不支持这个属性,不过它有代替的属性,分别是 fromElement和toElement, node.contains()返回的是一个布尔值,来表示传入的节点是否为该节点的后代节点。利用这两个特性,就可以解决这个问题。
DataTransfer对象
任何拖动事件,event参数中都会一个DataTransfer属性,它有一些常用属性和方法:
1、DataTransfer.effectAllowed和dropEffect,用来设置拖和放的鼠标指针类型,用处不大,具体效果可点击此处查看2、DataTransfer.files,拖拽的本地文件列表。如果拖动操作不涉及拖动文件,则此属性为空列表。3、DataTransfer.items,只读,提供DataTransferItemList对象,该对象是所有拖动数据的列表,包含DataTransfer.files。读取文件:
// 图片校验 checkFile(file){ const isJPG = /jpg|jpeg|png/.test(file.type.toLowerCase()); const isFile = /jpg|jpeg|png/.test(file.name.toLowerCase()); if (!isJPG || !isFile) { console.log('只可以上传jpg、png的图片。') } const isLt2M = file.size / 1024 / 1024 < 2; if (!isLt2M) { console.log('图片尺寸不允许超过2MB!') } return isJPG && isFile && isLt2M; }let content:string, pic:[]textareaDropfn(e){ e.preventDefault(); let fileList = e.dataTransfer.files; let contentText:string; for (let i = 0; i < fileList.length; i++) { const el = fileList[i]; if(el.type == 'text/plain' || el.type == 'text/html'){ // 文本文件 let reader = new FileReader(); let that = this; reader.onload = (function(file) { return function(e) { that.weiboContent += this.result; }; })(el); //读取文本内容 reader.readAsText(el, "gbk"); } else if(this.checkFile(el)) {//读取图片 this.pic.push(el); } } return false; }
文件操作:
DataTransfer.files对象包含了我们拖动的File对象,是个数组对象,包含以下属性:
- name:文件名,该属性只读。
- size:文件大小,单位为字节,该属性只读。
- type:文件的 MIME 类型,如果分辨不出类型,则为空字符串,该属性只读。
- lastModified:文件的上次修改时间,格式为时间戳。
- lastModifiedDate:文件的上次修改时间,格式为 Date 对象实例
我们需要name、type、size属性来校验格式和大小是否满足要求。HTML5给我们提供了FileReader API 用于读取文件,即把文件内容读入内存。它的参数是 File 对象或 Blob 对象。
什么是File
在前端开发中,最常见的file就是表单上传的文件,它是一个file对象,而FileList对象则是这些file对象的集合列表,代表所选择的所有文件。file对象继承于Blob对象,该对象表示二进制原始数据,提供slice方法(可以用来文件分片),可以访问到字节内部的原始数据块。总之,file对象包含与FlieList对象,而file对象继承于Blob对象!他们的关系如下图:
对于不同类型的文件,FileReader 提供不同的方法读取文件。
readAsText(Blob|File, opt_encoding):返回文本字符串。默认情况下,文本编码格式是 UTF-8,可以通过可选的格式参数,指定其他编码格式的文本。用此方法我们可以读取文件内容。
readAsDataURL(Blob|File):返回一个基于 Base64 编码的 data-uri 对象。用此方法我们可以做图片预览。图片本地预览
我们知道,img的src属性或background的url属性,可以通过被赋值为图片网络地址或base64的方式显示图片。在文件上传中,我们一般会先将本地文件上传到服务器,上传成功后,由后台返回图片的网络地址再在前端显示。通过FileReader的readAsDataURL方法,我们可以不经过后台,直接将本地图片显示在页面上。这样做可以减少前后端频繁的交互过程,减少服务器端无用的图片资源,代码如下:
let input = document.getElementById("file");input.onchange = function(){ let file = this.files[0]; if(!!file){ let reader = new FileReader(); // 图片文件转换为base64 reader.readAsDataURL(file); reader.onload = function(){ // 显示图片 document.getElementById("file_img").src = this.result; } }}
还有,URL对象也提供了一个把File类型的文件,转化为url(数据URL)的方法。
传入一个 File 对象或者 Blob 对象,能生成一个链接:let objecturl = window.URL.createObjectURL(file|blob);
这个 URL 可以放置于任何通常可以放置 URL 的地方,也可用此方法做图片预览。
复制粘贴事件
在发布框里支持粘贴图片,可省去用户截图保存、再删除的麻烦。copy、cut、paste这三个事件是一个类型的事件。我们期望获得剪贴板(clipboard)里面的图片,可以用给页面中的元素绑定paste事件的方法,当用户鼠标在该元素上或者该元素处于focus状态,右键粘贴或者ctrl+v的操作都会触发。
粘贴图片我们需要解决下面几个问题1、监听用户的粘贴操作2、获取到剪切板上的数据3、将获取到的数据渲染到网页中myTextarea.addEventListener("paste", function (e){ let items = e.clipboardData && e.clipboardData.items || [];});
clipboardData对象是一个DataTransfer类型的对象,DataTransfer 是拖动产生的一个对象,但实际上粘贴事件也是它。items的DataTransferItem有两个属性kind和type,我们可以通过循环取出粘贴板上的数据,然后通过kind来判断是文件(file)还是字符串(string),如果kind是file,可以用getAsFile方法获取到文件。type属性则包含的是具体体的数据类型即MIME-Type。
textareaPaste(e){ let cbd = e.clipboardData; for(let i = 0; i < cbd.items.length; i++) { let item = cbd.items[i]; if(item.kind == "file"){ var blob = item.getAsFile(); if (blob.size === 0) { return; } this.postImg(blob); } } }
获取到file文件之后,就可以进行一些列操作了。
小结
结合拖放事件API,DataTransfer对象和文件读取对象FileList等方面的知识,可以实现拖拽上传图文并预览效果。其实它们能实现的功能远不止这些,比如拖动排序、大文件分片断点上传,或者结合后台处理Word文档等操作。由于技术的发展,这些桌面上的功能,都可以在前端实现,我们需要有一个探索的心。
作者:TNFE 大鹏哥