博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[译] 我要写个上传组件…(实践教程)
阅读量:4941 次
发布时间:2019-06-11

本文共 9173 字,大约阅读时间需要 30 分钟。

原文地址: 

标题: So We Wanted To Build A File Uploader… (A Case Study)

作者: Konstantin Lebedev

某一天我发现自己需要设计一个API实现从客户端上传文件到服务端的功能。我在一家名叫的公司工作,我主要负责开发俄罗斯语言的web邮箱服务,每天都要处理这些方面的javascript代码。Web邮箱服务有个最基本的功能就是给邮件添加附件

Mail.ru也不例外: 我们过去用的是Flash上传组件,这种方式其实还是相当不错的,但也存在一些问题。HTML标记,图表,业务逻辑,甚至一些本土化的特性都杂糅在里面, 导致上传组件相当臃肿。更糟糕的是,只有Flash开发者才能修改它。于是我们意识到需要构建一个全新的上传组件。本文会详细介绍我们是如何构建这个更棒的工具(上传组件)的。

所有写过Flash上传组件的人都知道它通常会带来以下问题:

  • 用于验证的Cookie,因为这些Cookie依赖于浏览器的实现以及不同的操作系统,所以在Flash中表现很不稳定(比如,HTTP请求与FileReference的上传/下载不能共享cookie)。官方Flash只在IE下支持cookie,并且不能在其它浏览器间共享,否则会被IE回收;
  • 虽然没被官方证实,有假设表明在Flash中,cookie都是从Internet Exporer中读取的;
  • ;用Flash时,cookie都会被IE回收,无论使用的是哪个浏览器;
  • 错误# 和#,在某些网络设置,浏览器,以及Flash播放器版本结合的情况下,会产生难以捉摸的错误。

  • AdBlock及类似(不予评论)。

于是我们决定是时候改变了。下面是我们期望的新的解决方案的功能列表:

  • 能够多选文件;
  • 获取文件信息(name, type, mini-type);
  • 上传前预览图片;
  • 在客户端缩放,裁剪,旋转图片;
  • 上传处理后的结果到服务端,包括;
  • 独立于外部库;
  • 可扩展。

在过去的四年内,我们都听闻了关于HTML5的丰富特性和方案的激烈讨论, 包括。 很多刊物都谈及这个API,而我们也有一些关于这个API的运行示例。也许有人会想,“这是一个解决之前问题的工具。” 但它真如看起来那么简单么?

我们来看看。我们只选择了一些支持File API的浏览器版本,尽管在某些情况下,这些浏览器不一定会完全支持这个API。

支持File API的浏览器

上图显示超过87%的浏览器都支持File API:

  • Chrome 10+
  • Firefox 3.6+
  • Opera 11.10+
  • Safari 5.4+
  • IE 10+

当然,我们不能忽略日渐流行的移动设备上的浏览器。拿iOS 6+举例,已经支持了File API。然而, 87%并不是100%,在我们的产品中,现阶段完全放弃Flash是不切实际的。

于是,我们的任务就涉及到构建一个结合了两种技术(File API和Flash)的工具,并且这个利器能够让开发者忽略文件上传的实现方式。在开发过程中,我们决定结合所有的初期开发工作以实现一个独立的库(独立API),这个库能够独立于环境运行,并且在任何地方使用,不仅仅是在我们的服务中。

那我们就详细探讨一下这次开发过程中的一些细节,看看我们构建了什么,如何构建,以及我们在这个过程中学到的什么。

获取文件列表

从基础开始。下面是HTML5中获取文件的方式。十分简单。

 

但是当你只支持Flash,没有File API时该怎么办呢?对于支持Flash的用户,我们的基本思想就是所有的交互都通过Flash完成。你不能简单地调起一个文件选择框。由于安全策略的限制,只有点击了Flash Object之后才能打开文件选择框。

这也是为什么要把Flash Object定位到目标输入框之上的原因。然后你就能给document 添加一个mouseover事件,并且在用户hover 到Flash object上时把Flash object放到input元素的父元素中。

用户就能够点击Flash object,打开文件选择框,并且选择文件了。利用ExternalInterface,文件数据会通过Flash 传到Javascript。Javascript会把接收到的数据和input 元素绑定,用来模拟change事件。

[[Flash]] --> jsFunc([{     id: "346515436346", // 文件id     name: "hello-world.png", // 文件名     type: "image/png", // mime-type     size: 43325 // 文件大小   }, {     // etc. }])

 

Javascript和Flash更进一步的交互都是通过Flash中唯一可用的一个方法。这个方法第一个参数是一个命令名,第二个参数是一个object,它有两个必须的属性: 文件idcallbackcallback会在这个命令执行结束后从Flash中调用。

flash.cmd("imageTransform", {    id: "346515436346",    // 文件id    matrix: { },    // 变换矩阵    callback: "__UNIQ_NAME__"});

 

两种方式结合的效果会体现在API中,API看起来非常像原生的Javascript。唯一的区别就是文件的接收方式。现在我们只能使用API方法来得到文件, 因为只有当浏览器支持HTML5和File API时input才有files属性,而在Flash中方式中,文件列表来自于与之相关的数据信息。

    

 

过滤器

通常情况下,文件上传总是伴随着一系列的限制。最常见的限制就是文件大小,图片类型和尺寸(宽,高)。你会发现很多解决方案都是在服务端检查这些限制,如果文件不符合这些限制条件,用户就会收到一个错误信息。我尝试了另外一种解决办法,在客户端就验证文件的限制条件,即在文件上传之前。

太好了!不过,真的这么容易么?我们最开始拿到文件列表的时候,只能获得关于文件的极少信息:文件名,大小及类型。为了得到更多文件信息,我们需要读取这些文件。因此我们使用了。

我们来研究一下FileReader,就能得到下面一些过滤技术:

FileAPI.filterFiles(files, function (file, info){    if( /^image/.test(file.type) ){        return info.width > 320 && info.height > 240;    } else if( file.size ){        return file.size < 10 * FileAPI.MB;     } else {         // 很不幸,既不支持File API 也不支持Flash。需要在服务端验证。         // 这种情况很少见,但我们还是需要考虑这一点。         return true;     } }, function (files, ignore){     if( files.length > 0 ){        // ...    }});

 

你可以拿到文件的"原本"的尺寸,也能够收集到所有你需要的数据:

FileAPI.addInfoReader(/^audio/, function (file, callback){    // 收集必要信息    // 回调    callback(        false,    // 或者错误信息        { artist: "...", album: "...", title: "...", ... }    );});

 

处理图片

在开发API的过程中,我们还希望我们构造的工具足够方便强大,能够处理图片——比如,创建预览,裁切,旋转以及缩放——并且这些功能要在HTML5和Flash方式下都能使用。

FLASH

首先,我们需要知道怎么用Flash实现这一功能——也就是,把什么传给Javascript来构建图片。大家也许知道,通常使用data URI来完成传递工作。Flash以Base64形式读取文件然后传递给Javascript。因此我们将data:image/png;base64加到前面,并将这个字符串作为图片的src

万事大吉了么?可惜, IE 6 、7不支持data URI, 还有IE 8+,虽然支持data URI,却不能处理大于32KB的图片。这样一来,Javascript就需要创建第二个Flash Object,并且把Base64编码的内容传递给它。用这个Flash Object来保存图片。

HTML5

使用HTML5方式时,我们会先得到原始图片,然后用canvas进行指定的图片处理(转换)。有两种方式获得原始图片。一种就是用FileReader来读取文件的dataURI。另一种就是用给文件生成一个链接,这个链接会绑定到当前标签页。当然,这种方式也很好,足以用来生成预览,但并不是所有浏览器都支持。比如Opera 12就不支持后续需要调用的URL.revokeObjectURL,这个方法能告知浏览器不必再为这个文件保留链接。

于是我们整合了这些方法,得到了一个FileAPI.Image类:

  • crop(x, y, width, height)
  • resize(width,[height])
  • rotate(deg)
  • preview(width, height) — 裁切和缩放
  • get(callback) — 得到最终的图片

所有这些方法会生成一个变换矩阵,当get()方法被调用的时候这个变换矩阵就会在图片上生效。变换会通过HTML5 的canvas或者Flash(采用Flash接口上传文件时)执行。

下面对我们矩阵的描述:

{   // 原来的参数片段   sx: Number,   sy: Number,   sw: Number,   sh: Number,   // 目标大小   dw: Number,   dh: Number,   deg: Number}

 

下面是一个简短的例子:

FileAPI.Image(imageFle) // 返回 FileAPI.Image 实例   .crop(300, 300)      // 裁切图像的宽高   .resize(100, 100)    // 缩放到100x100px   .get(function (err, img){      if( !err ){        // 将结果Append到DOM节点中(
). images.appendChild(img); } });

 

缩放

数码相机出现很久了,至今还是非常流行。有些只需要花$20到$30就能拍出10 MP甚至更高分辨率的照片。我们曾尝试降低这些照片的大小,下面就是我们最后得到的效果:

一次性降低图片大小

你也看到了,质量实在不咋地。不过,如果我们先将图片缩放到一半,这样重复几次,直到达到我们想要的尺寸,这时候图片质量就好多了。这种方法其实很古老了,这就是的结果;而当直接压缩图像时,我们就会立马损失图像质量。

多次缩小图片到一半的大小

差别很明显:

图片质量差别明显

加上轻微的锐化效果,图像就能达到很理想的效果了。

我们也尝试过其它方法,比如双三次插值和 Lanczos 算法。结果会更好些,但是处理起来更费时: 1.5秒至200到300毫秒。在canvas和Flash中也是产生相同的结果。

上传文件

现在总结一下我们上传文件到服务器的各种选项。

IFRAME

没错,我们在多年之后还在用这种方式:

 

首先,我们创建一个form元素,内部嵌套一个iframe。(这个form的target属性和iframe的name应保持一致。)然后,把input[type="file"]移到里面,因为如果你拷贝一个input再放进去,这个拷贝就是空的。

为了说明这个问题,想像一下你通过iframe加载了一个文件。我们用如下的代码来说明问题:

var inp = document.getElementById('photo');var form = getIFrameFormTransport();form.appendChild(inp.cloneNode(true)); // 发送一个“拷贝”form.submit();

 

然而,这种input,即它不会包含选择的文件,这也就是我们需要“发送”原始文件(原来的input)并且用一个拷贝来替换它的原因。

这也是为什么我们通过API方法注册这样的事件,用以在拷贝的同时保存文件。然后,我们调用form.submit(),把form的内容通过iframe提交。我们就能通过JSONP得到结果。

var inp = document.getElementById('photo');var cloneInp = inp.cloneNode(true);var form = getIFrameFormTransport();// 将“拷贝”的节点插入到“原始”的那个后面inp.parentNode.insertBefore(cloneInp, inp);form.appendChild(inp); // 发送“原始”的节点form.submit();

 

是的,这方法确实很诡异。

FLASH

原则上,每件事情都很简单: Javascript调用Flash object的方法,把要上传的文件的ID传给Flash。反过来Flash会复制Javascript中所有的状态和事件。

XMLHTTPREQUEST 和 FORMDATA

我们现在不仅可以发送文本数据,还可以发送二进制数据。很简单:

// 收集要发送的数据var form = new FormDataform.append("foo", "bar"); // 第一个参数是POST参数名form.append("attach", file); // 第二个参数是字符串,文件或者Blob// send to servervar  xhr = new XMLHttpRequest;xhr.open("POST", "/upload", true);xhr.send(form);

 

那么假如我们不是发送一个文件而是canvas数据呢?有两个选择。有一种方法是最简单且正确的,就是把canvas转化成Blob

canvasToBlob(canvas, function (blob){    var form = new FormData    form.append("foo", "bar");    form.append("attach", blob, "filename.png"); //并不是所有都支持第三个参数    // ...});

 

不过这种方法并不被完全支持。某些情况下canvas没有Canvas.toBlob()方法(或者无法被执行),我们需要选择另外的方法。不支持FormData的浏览器也支持这种方法。这个方法的重点就是手动创建多部分请求,将其发送到服务端。canvas的代码如下:

var dataURL = canvas.toDataURL("image/png"); // 或者FileReader的结果var base64 = dataURL.replace(/^data:[^,]+,/, ""); // 去掉开头部分var binaryString = window.atob(base64); // decode Base64// 现在把这些部分拼起来,一点都不复杂var uniq = '1234567890';var data = [      '--_'+ uniq    , 'Content-Disposition: form-data; name="my-file"; filename="hello-world.png"'    , 'Content-Type: image/png'    , ''    , binaryString    , '--_'+ uniq +'--'].join('\r\n');var xhr = new XMLHttpRequest;xhr.open('POST', '/upload', true);xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=_'+uniq);if( xhr.sendAsBinary ){  xhr.sendAsBinary(data);} else {  var bytes = Array.prototype.map.call(data, function(c){     return c.charCodeAt(0) & 0xff;   });  xhr.send(new Uint8Array(bytes).buffer);}

 

最终,我们提供的方法如下:

var xhr = FileAPI.upload({   url:  '/upload',   data:  { foo: 'bar' },   headers:  { 'Session-Id': '...' },   files:   { images: imageFiles, others: otherFiles },   imageTransform:  { maxWidth: 1024, maxHeight: 768 },   upload: function (xhr){},   progress: function (event, file){},   complete: function (err, xhr, file){},   fileupload: function (file, xhr){},   fileprogress: function (event, file){},   filecomplete: function (err, xhr, file){}});

 

这个API方法有很多参数,最重要的就是imageTransform。它在Flash和HTML5方式下都有效。

到这儿才说了一半。我们还允许设置多种imageTransform呢:

{    huge:   { maxWidth: 800, maxHeight: 600, rotate: 90 },    medium: { width: 320, height: 240, preview: true },    small:  { width: 100, height: 120, preview: true }}

 

这意味着3份拷贝(除了原始图片)会被发送到服务端。为什么要这么做呢?假如你能够把从服务端加载转移到直接在客户端显示,这会是一个很好的主意。服务端只需对输入的文件进行最低限度的验证。首先,你不仅减少了一次加载,还避免了在服务端写更多的逻辑,可以完全由客户端来完成这些工作。

其次,假如这些文件原本就没必要上传到服务端,那我们就节省了带宽。还有,经常会有这样的问题,那就是无法在服务端进行进一步的处理,比如与第三方服务(例如Amazon S3)集成的时候。我们的经验是,将原本在服务端执行的附加的逻辑转移到客户端是完全没问题的。

上传函数也会返回一个类似于XMLHttpRequest的对象; 也就是说, 它借鉴了XMLHttpRequest的一些属性和方法, 比如:

  • status

    HTTP 状态码

  • statusText

    HTTP状态描述

  • responseText

    服务端返回

  • getResponseHeader(name)

    得到服务端返回的头部

  • getAllResponseHeaders()

    得到全部头部

  • abort()

    取消上传

尽管HTML5允许在一次请求中上传多个文件,但标准的Flash只允许一次上传一个文件。 而且,我们觉得,批量上传文件并不是一个好主意。一来Flash不支持这么做,且我们希望Flash和HTML5方式保持一样的行为。二来在用户那里也许会发生内存不足,导致浏览器崩溃的状况。

实际上,从上传函数返回的XMLHttpRequest是一个代理XMLHttpRequest。它的方法和属性反映了当前正在上传的文件的状态。

结束语

给大家看一个关于我们API中拖拽上传文件的简单的例子:

 

我们花了不少时间去开发这个上传库。我们利用日常工作挤出来的时间,耗费了5个月来完成这个库的开发工作。主要难点就是不同浏览器的细节上的差异。Chrome, Firefox 以及IE10+都还好,但是Safari和Opera每个版本之间的表现都有很大的差异,包括在Win/Mac平台上的不一致。不过,主要问题还是结合所有三种技术——iframe, Flash, HTML5——来完成一个强大的上传组件。

现在在上能够看到我们的上传库, 我们还写了一个。希望大家能够多多反馈这个组件的Bugs以及多使用它哦!

有用的链接

  • (以及), Mail.ru, GitHub
  • , GitHub
    查找Tarantool, fest还有更多其它
  • “,” Can I Use…?
    检查哪些浏览器支持input[type="file" multiple]
  • “,” Can I Use…?
  • “,” Mozilla Developer Network
  • “” 以及“,” Mozilla Developer Network
  • “,” Mozilla Developer Network
  • “,” Mozilla Developer Network

本文由审校和编辑,他是一个前端工程师,在前端组工作。

 

转载于:https://www.cnblogs.com/huangxiaolu/p/3473021.html

你可能感兴趣的文章
网站开发技能图谱
查看>>
4.27随笔
查看>>
CSS实例:图片导航块
查看>>
poj1860 Currency Exchange(spfa判断正环)
查看>>
SQL CHECK 约束&Case when 的使用方法
查看>>
[整理]HTTPS和SSL证书
查看>>
[转载] Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
查看>>
水晶苍蝇拍:聊聊估值那些事儿——“指标”背后的故事 (2011-11-01 14:58:32)
查看>>
3.每周总结
查看>>
应用提交 App Store 上架被拒绝
查看>>
Android实现异步处理 -- HTTP请求
查看>>
数据清空js清空div里的数据问题
查看>>
Fortran中的指针使用
查看>>
移动终端app测试点总结
查看>>
14-6-27&28自学内容小结
查看>>
JSP
查看>>
---
查看>>
(第一组_GNS3)自反ACl
查看>>
hdu--1258--Sum It Up(Map水过)
查看>>
Spring @DeclareParents 的扩展应用实例
查看>>