前端文件操作--上传篇

上传方式

一、Form && Input

采用传统的form表单或异步ajax上传。至于以前通过iframe来进行异步上传,这里不详细介绍,想了解可以看http://www.ruanyifeng.com/blog/2012/08/file_upload.html

<form id="upload-form" action="./upload.do" onsubmit="return handleSubmit(this)" method="post" enctype="multipart/form-data" >
    <div class="filePicker">
        <label>点击选择文件</label>
        <input id="fileInput1" type="file" name="files" multiple="multiple">
    </div>
    <input type="submit" value="上传" />
 </form>

Tips:

  1. input file标签设置accept属性进行文件选择过滤,该属性的值必须为一个逗号分割的列表,包含了多个唯一的内容类型声明:
    • 以 STOP 字符 (U+002E) 开始的文件扩展名。(例如:".jpg,.png,.doc")一个有效的 MIME 类型,但没有扩展名
    • audio/* 表示音频文件 HTML5
    • video/* 表示视频文件 HTML5
    • image/* 表示图片文件
  2. 设置multiple属性可以进行设置为多选。
  3. 设置capture属性可以进行设置打开摄像拍照或者录像。multiple属性和capture属性不能同时生效。(移动端里在安卓和ios上有不同的表现,但效果都一样)
//Capture Image: 
    <input type="file" accept="image/*" capture="camera"> 
//Capture Audio: 
    <input type="file" accept="audio/*" capture="microphone"> 
//Capture Video: 
    <input type="file" accept="video/*" capture="camcorder"> 

接着就是通过js来获取input里的value并进行下一步的操作

var fileInput1 = document.getElementById("fileInput1");
fileInput1.addEventListener('change', function(event) {
        var file = fileInput1.files[0];
        // 或file = fileInput1.files.item(0);
        console.log(file);
        document.getElementById('showFile1').innerHTML= file.name
}, false);

function handleSubmit(_this) {
    var form = $(_this);
    // mulitipart form,如文件上传类
        var formData = new FormData();
            formData.append('files', $('#fileInput1')[0].files[0]);
        //var formData = new FormData(form[0]);
    $.ajax({
            type: form.attr('method'),
            url: form.attr('action'),
            data: formData,
            mimeType: "multipart/form-data",
            contentType: false,
            cache: false,
            processData: false
    })
    .success(function (res) {
            //成功提交
            console.log(res)
            document.getElementById('result').innerHTML= '上传成功'
    })
    .error(function (jqXHR, textStatus, errorThrown) {
            //错误信息
            document.getElementById('result').innerHTML= '上传失败'
    });
    return false;
}

Tips: 预防form表单提交的跳转action。

  1. ajax中的resquest-header: multipart/form-data
  2. 参数及文件均需要使用 new FormData() 实例 append()

这里需要对file对象进一步的说明:

  • lastModifiedDate:文件对象最后修改的日期
  • name:文件名,只读字符串,不包含任何路径信息.
  • size:文件大小,单位为字节,只读的64位整数.
  • type:MIME类型,只读字符串,如果类型未知,则返回空字符串.

(type属性判断用户的文件类型不太准确,用户会改变后缀名;size可以做大小限制判断)

二、HTML5 拖拽操作文件

image.png

H5拖拽操作之后会单开一篇另做介绍

下面会对H5拖拽操作文件进行简单的讲解

<div class='demo-box'>          
    <div class="demo-content">
        <a href="javascript:;">选择图片文件拖拽至下方框中,实现拖拽上传及图片预览</a>
        <div id="dropbox" class="dropbox">
            <div class="area"></div>
        </div>
        <div id="preview"></div>
    </div>
</div>
var dropbox = document.getElementById("dropbox");
var preview = document.getElementById("preview");

dropbox.addEventListener("dragenter", function(e){
    e.stopPropagation();
      e.preventDefault();
}, false);

dropbox.addEventListener("dragover", function(e){
    e.stopPropagation();
      e.preventDefault();
}, false);

dropbox.addEventListener("drop", function(e){
    e.stopPropagation();
      e.preventDefault();

      var dt = e.dataTransfer;
      var files = dt.files;//获取文件
      
      for (var i = 0; i < files.length; i++) {
        var file = files[i];
        var imageType = /^image\//;

        if ( !imageType.test(file.type) ) {
              continue;
        }
        
        // 填充选择的图片到展示区
        var img = document.createElement("img");
        img.classList.add("obj");
        img.file = file;
        preview.appendChild(img);
        
        // 读取File对象中的内容
        var reader = new FileReader();
        reader.onload = (function(aImg) { 
          return function(e) { 
            aImg.src = e.target.result; 
          }; 
        })(img);
        reader.readAsDataURL(file);
    }
}, false);

Blob、File、DataURL(Base64)和BlobURL之间的闭环关系

讲到这里就有必要对这些概念展开介绍,也方便对整个前端文件处理生态有一个更全面的认识。

相信在工作中经常遇到,文件上传、图片压缩、文件下载、大文件断点续传,等等关于 js 来操作文件的需求。那么你真的了解文件类型之间的转换关系吗?如下:

  • Blob --> File
  • File --> DataURL(base64)
  • File --> BlobURL
  • HTTPURL| DataURL | BlobURL --> Blob
image.png

一、Blob类型

Blob 类型是 File 文件类型的父类,它表示一个不可变、原始数据的类文件对象

如何得到 blob 对象?

1. new Blob(array, options)

let hiBlob = new Blob([`<h1>Hi!<h1>`], { type: 'text/html' })

如上代码,就创建了一个 blob 对象,并声明了 text/html 类型 ,就像是创建一个 .html 文件。只不过它存在于浏览器的内存里。

2. fetch(url)

js 为我们提供了很多获取资源的 api,如:
Fetch API 提供了一个获取资源的统一接口(包括跨域请求)

关于 fetch(url, options), url 参数支持格式有:

  • http、https

  • blobURL: 比如通过 URL.createObjectURL() 获得

    // blobURL 示例:
    blob:null/7025638d-c05f-4c75-87d6-470a427e9aa3
    
  • dataURL: 如图片的 base64 格式,比如通过 convasElement.toDataURL() 获得

    // dataURL(base64) 黑色 1 像素示例:
    data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=
    

fetch(url, options) 响应数据可被解析成:

  • res.arrayBuffer(): 通用、固定长度的原始二进制数据缓冲区
  • res.blob(): Blob 类型
  • res.formData(): 表单数据类型
  • res.json(): JSON 格式
  • res.text(): 文本格式

这里主要关心 blob 类型转换,如下代码,用 fetch api 获取图片资源的 blob 对象,
当然也可以获取其它类型的资源。如:.txt .html

// 获取图片的 blob 对象
// 通过 http、https 获取
fetch('http://eg.com/to/path/someImg.png')
  .then(res => res.blob())
  .then(blob => {
    console.log('blob: ', blob)
})

3. canvasElement.toBlob(callback)

canvas 具有图像操作能力,支持将一个已有的图片作为图片源,来操作图像。

如下,通过 canvas 将图片资源转成 blob 对象

<body>
  <canvas width="100" height="100"></canvas>
</body>

<script>
  const $ = arg => document.querySelector(arg)
  let convas = $('canvas')
  // async 自执行函数
  (async () => {
    let imgUrl = 'http://eg.com/to/path/someImg.png'
    let ctx = convas.getContext('2d')
    let img = await fetchImg(imgUrl)
    // 向 canvas 画布上下文绘制图片
    ctx.drawImage(img, 0, 0)

    // 获取图片 blob 对象
    convas.toBlob((blob) => {
      console.log('blob: ', blob)
    })

    // 获取图片 dataURL,也是 base64 格式
    let dataURL = convas.toDataURL()
    console.log('dataURL: ', dataURL)
  })()
  
  // 获取图片资源,封装成 promise
  function fetchImg (url) {
    return new Promise((resolve, reject) => {
      let img = new Image()
      // 跨域图片处理
      img.crossOrigin = 'anonymous'
      img.src = url
      // 图片资源加载完成回调
      img.onload = () => {
        resolve(img)
      }
    })
  }
</script>

Tips:

  • 如果图片没加载完,就调用 drawImage,canvas 绘制将失败,所以我们简单封装了 fetchImg 方法,确保图片资源加载完成后再开始绘制图片。

  • 由于 canvas 中的图片可能来自一些第三方网站。在不做处理的情况下,使用跨域的图片绘制时会污染画布,这是出于安全考虑。在“被污染”的画布中调用 toBlob() toDataURL() getImageData() 会抛出安全警告。

    解决方法:

    let img = new Image()
    // 1. 增加 crossOrigin 属性,值为 anonymous
    // 含义:执行一个跨域请求,在请求头里加 origin 字段
    // 2. 后端要返回 Access-Control-Allow-Origin 响应头来允许跨域
    img.crossOrigin = 'anonymous'
    img.src = 'to/path'
    

    本质就是解决跨域问题,也可以使用 nginx 做个代理来解决

  • blobslice(startIndex, endIndex) 方法,复制 blob 对象某片段,与 js 数组的 slice 方法类似,文件的断点续传功能就是利用了该特性。

二、File类型

File 包含文件的相关信息,可以通过 js 来访问其内容

如何获取 file 对象?

1. new File(bits, name[, options])

// 1. 参数是字符串组成的数组
let hiFile = new File([`<h1>Hi gauseen!<h1>`], 'fileName', { type: 'text/html' })

// 2. blob 转 file 类型
let hiBlob = new Blob([`<h1>Hi gauseen!<h1>`], { type: 'text/html' })
let hiFile = new File([ hiBlob ], 'fileName', { type: 'text/html' })

如上代码,通过 File 构造函数,创建一个 file 对象,与上面的提到的 blob 类似。可以将 blob 转成 file 类型,这意味着上面获取的 blob,可以转成 file 类型。

2. inputElement.files

通过 `` 标签获取 file 对象

// input 上传文件时触发 change 事件
$('input').addEventListener('change', e => {
  let file = e.target.files[0]
  console.log('file: ', file)
})

3. DragEvent.dataTransfer.files

通过拖、放获取 file 对象

<body>
  <div id="output">
     将文件拖放到这里~
  </div>
</body>

<script>
  const $ = arg => document.querySelector(arg)
  let outputEle = $('#output')
  // ondragover 事件规定在何处放置被拖动的数据
  outputEle.addEventListener('dragover', dragEvent => {
    dragEvent.preventDefault()
  })
  // ondrop 事件放置文件时触发
  outputEle.addEventListener('drop', dragEvent => {
    dragEvent.preventDefault()
    // DataEvent.dataTransfer 属性保存着拖拽操作中的数据
    let files = dragEvent.dataTransfer.files
    console.log('drag files: ', files)
  })
</script>

4. canvasElement.mozGetAsFile()

注: 截止当前,该方法仅支持火狐浏览器

let file = canvasElement.mozGetAsFile('imgName')

三、DataURL(base64)

DataURL,前缀为 data: 协议的 URL,可以存储一些小型数据

语法:data:[][;base64],

如下,黑色 1 像素示例:

data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=

上面提到的 Blob File 类型,如何“消费”它们呢?接着向下看

1. FileReader

允许 Web 应用程序异步读取存储在用户计算机上的文件(blobfile)。

// 将 blob 或 file 转成 DataURL(base64) 形式
fileReader(someFile).then(base64 => {
  console.log('base64: ', base64)
})

function fileReader (blob) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader()
    reader.onload = (e) => {
      resolve(e.target.result)
    }
    reader.readAsDataURL(blob)
  })
}

2. convasElement.toDataURL()

可以通过 canvas 图像处理能力,将图片转成 dataURL 形式。在上面 Blob 部分讲解中,代码已实现。

四、BlobURL(ObjectURL)

BlobURL 也叫 ObjectURL,它可以让只支持 URL 协议的 Api(如:<a> <link> <img> <script>) 访问 fileblob 对象。
dynamic-import-polyfill 库也用到了其特性。

如下,生成 blobURLcreateObjectURL 方法创建从 URL 到 Blob 的映射关系。
如:blob:http://eg.com/550e8400-e29b-41d4-a716-446655440000

// object 创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象
let blobURL = URL.createObjectURL(object)

如下,revokeObjectURL 方法撤消 blobURL 与 Blob 的映射关系,有助于浏览器垃圾回收,提示性能。

URL.revokeObjectURL(blobURL)

五、形成闭环

通过上面的一系列转换关系,可以知道:

blob --> file --> dataURL(base64) | blobURL --> blob

这样就形成了一个闭环,文章开头的思维导图很好的说明了之间的转换关系。

上传中可能遇到的问题

  1. 有时候我们需要将DataURI对象转blob对象:

    /**
     * dataURI 转 blob
     * @param {Object} dataURI
     */
    function dataURItoBlob(dataURI) {
        var arr = dataURI.split(','), mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
        while(n--){
            u8arr[n] = bstr.charCodeAt(n);
        }
        return new Blob([u8arr], {type:mime});
    }
    
  2. 预览图片

    //利用URL对象在前端对js获取到的file进行预览
    //window.URL.createObjectURL(file);
    //静态方法会创建一个 DOMString,它的 URL 表示参数中的对象。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示着指定的 File 对象或者 Blob 对象。
    
    //window.URL.revokeObjectURL(objecturl)
    //静态方法用来释放一个之前通过调用 window.URL.createObjectURL() 创建的已经存在的 URL 对象。当你结束使用某个 URL 对象时,应该通过调用这个方法来让浏览器知道不再需要保持这个文件的引用了
    
    window.URL = window.URL || window.webkitURL;
    
    var img = document.createElement("img");
    img.src = window.URL.createObjectURL(blob);//二进制对象
    img.height = 60;
    img.onload = function(e) {
        window.URL.revokeObjectURL(this.src);
    }
    document.body.appendChild(img);
    
  3. 以二进制流上传文件

    var fileInput = document.getElementById("fileInput");
    fileInput.addEventListener('change', function(event) {
        var file = fileInput.files[0];
        if (file) {
            var reader = new FileReader();  
            var xhr = new XMLHttpRequest();
            xhr.onprogress=function(e){
                var percentage = Math.round((e.loaded * 100) / e.total);
                console.log("percentage:"+percentage);
            }
            xhr.onload=function(e){
                console.log("percentage:100");
            }
            xhr.open("POST", "这里填写服务器地址");  
            reader.onload = function(evt) {
                xhr.send(evt.target.result);
            };
            reader.readAsBinaryString(file);
        }
    }); 
    
  4. 如果在浏览器端js生成的内容,想让浏览器进行下载要如何办到呢?DataURI可以实现这个效果,但是DataURI的文件类型被限制了,我们这里可以变通一下实现blob对象

    <a id="aLink">下载</a>
    <script type="text/javascript">
        function downloadFile (el, fileName, content) {
            var aLink = document.querySelector(el);
            var blob = new Blob([content]);                
            aLink.download = fileName;
            aLink.href = URL.createObjectURL(blob);
        }
        document.querySelector('#aLink').addEventListener('click',function () {
            downloadFile('#aLink', 'hello.txt', '<h1>hello world</h1>');
        })
    </script>
    
  5. 使用Blob对象的slice方法,将二进制数据按照字节分块,返回一个新的Blob对象。以此实现大文件分割上传

    function upload(blobOrFile) {
      var xhr = new XMLHttpRequest();
      xhr.open('POST', '/server', true);
      xhr.onload = function(e) { ... };
      xhr.send(blobOrFile);
    }
    
    document.querySelector('input[type="file"]').addEventListener('change', function(e) {
      var blob = this.files[0];
    
      const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
      const SIZE = blob.size;
    
      var start = 0;
      var end = BYTES_PER_CHUNK;
    
      while(start < SIZE) {
        upload(blob.slice(start, end));
    
        start = end;
        end = start + BYTES_PER_CHUNK;
      }
    }, false);
    

终语

以上就是前端上传篇的整体内容,如有不当之处欢迎指正。

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

推荐阅读更多精彩内容