发布框前端体验优化

javascript/jquery

浏览数:176

2019-3-12

AD:资源代下载服务

通过前端代码,利用File API对图文发布操作进行优化,提高用户体验。


发布框是web应用的一种常见图文发布功能,在微博、评论、论坛、博客或内容管理系统等产品中经常使用。做好发布框的交互设计,能提高用户的编辑效率,提高用户体验,给产品增加锦上添花的效果。

需求背景:

在实际的项目中,遇到了以下需求,用户(运营人员)可以通过发布框发布话题相关内容,产品经理期望在此发布框上实现以下功能:
1、用户可以拖动文件,当文件进入浏览器时提示用户拖动文件到发布框;
2、当拖动的文件(例如.exe)不符合要求时,给予拒绝提示,不能上传;
3、当拖动文件(批量)为图片或文档时,解析图片和文字,预览(或上传),其余类型的文件拒绝;


4、发布框支持图片复制、和QQ、PrintScreen键等工具的截图后粘贴(或ctrl+v)。

技术点:

  1. 拖放功能(drag & drop)
  2. File API功能
  3. 复制粘贴事件

drag & drop

拖放是 HTML5 中常见的功能。即:把抓取的对象拖放到其他位置(想想一下两个元素换位)。与他相关就是两个动作——拖和放。所以,它涉及到两个元素。一个是被拖的元素,称为拖放源;另一个是要放的目标,称为拖放目标。所涉及到的事件就是两类:drag(源)和drop(目标)。
与它相关的两个事件(按触发的先后顺序,参照物是鼠标指针而非文件边缘):

拖拽源:

  1. dragstart:按下鼠标时触发
  2. drag:按下鼠标持续时触发 (执行多次)
  3. dragend:鼠标放开时触发

投放目标:

  1. dragenter:拖动目标且鼠标进入投放区时触发
  2. dragover:拖动目标且鼠标移动在投放区时触发(每隔 350 毫秒会触发一次)
  3. dragleave:拖动对象且离开投放区时触发
  4. drop:拖动对象且在投放区放开鼠标时触发(需要在dragover上设置禁止默认事件,才会有触发,奇怪的设定)

兼容性

这里我们主要用到了投放目标的drop事件,它的兼容性如下图所示:

主要代码:

    <textarea  rows="" cols="" id="myTextarea"></textarea>
    <script type="text/javascript">
    //<!--
        let myTextarea = document.getElementById('myTextarea');
        document.addEventListener("dragenter", function(e) {
            // 被拖动物品进入页面给予虚线边提示
            e.preventDefault();
            myTextarea.style.border = "#666 1px dashed";
        }, false);
        document.addEventListener("dragleave", function(e) {
            // 被拖动物品离开页面
            e.preventDefault();
            myTextarea.style.border = "none";
        }, false);

        myTextarea.addEventListener("dragover", function(e) {
            // 被拖动物品移动
            e.preventDefault();
        }, false);

        myTextarea.addEventListener('drop', function(e){
             // 被拖动物品放置
             e.preventDefault();
             let upfile = e.dataTransfer.files;
             console.log(upfile);
         },false)
    //-->
    </script>

 遇到问题

dragenter(dragleave)的事件触发类似于mouseover(mouseout),当在子节点内外的拖动时,会触发子节点的drage事件并向上冒泡,引起多次触发当前节点的drag事件。举例来说,我们只在document上绑定dragenter事件,但是任何进出页面子标签的拖动,都会再次触发document的dragenter事件。可以通过是否包含和relatedTarget来解决。


两种方向的拖动都会触发目标节点的dragenter事件,这不是我们想要的结果!

//判断两个a中是否包含b
function 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 大鹏哥