专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  快!快!快!DeepSeek 满血版真是快 ·  昨天  
码农翻身  ·  漫画 | 为什么大家都愿意进入外企? ·  昨天  
程序员的那些事  ·  清华大学:DeepSeek + ... ·  3 天前  
OSC开源社区  ·  升级到Svelte ... ·  5 天前  
51好读  ›  专栏  ›  SegmentFault思否

实战:图片上传组件开发

SegmentFault思否  · 公众号  · 程序员  · 2018-01-31 08:00

正文

写在前面

图片上传,作为web端一个常用的功能,在不同的项目中有不同的需求,在这里实现一个比价基本的上传图片插件,主要能实现图片的浏览,剪裁,上传这三个功能,同时也是为了让自己对图片/文件上传和HTML5中名声在外的 canvas 相关能够有一些了解。

我就要自行车 - 需求整理

放眼WWW,一般的图片上传模块,主要就是实现了三个功能,图片的预览,图片的剪裁及预览,图片的上传,那我也就整这么一个吧,再细化一下需求。

图片的预览

用户使用:用户点击“选择图片”,弹出文件浏览器,可以选择本地的图片,点击确认后,所选图片会按照原始比例出现在页面的浏览区域中。

组件调用:开发者可以自己定义图片预览区域的大小,并限定所传图片的文件大小和尺寸大小。

图片的剪裁

用户使用:用户根据提示,在预览区域的图片上拖动鼠标框出想要上传的图片区域,并且能在结果预览区域看到自己的剪裁结果。

组件调用:开发者可以自定义是否剪裁图片,并可以定义是否限定剪裁图片的大小及比例,并且设定具体大小及比例。

图片的上传

用户使用:用户点击“图片上传”,图片开始上传,现实“上传中…”,完成后显示“上传完成”。

组件调用:开发者得到base64格式的urlData图片,自己编写调用Ajax的函数及其回调函数。

扔出原型图

作为设计师,扔图是我的最爱,画了一套全功能,包含剪裁及剪裁浏览的原型图

state-1:初始状态:

state-2:点击"选择图片",浏览本地后载入图片:

state-3:剪裁,在图片区域上拖动鼠标选择要剪裁的部分,确认要上传的部分:

一次历史性的对话 - 本地图片读取

自打干上web开发这活,就都是在捣鼓浏览器内部这点事,从没想过跟浏览器之外计算机本地的一些文件能发生什么关系。但是该来的总要来,既然要上传图片,就肯定要从计算机本地来选择文件并在浏览器内打开,这历史性的对话就要这么开启了…

图片的选择

其实在HTML中的 标签就提供了浏览本地文件的功能,前提是 type="file" ,真是很讲道理… 试过就知道一点击就会打开文件浏览器。

  1. id="inputArea" type="file" />

但这么做有两个经典的问题:

  • 第一,会有一个输入框傻乎乎的在那里…

  • 第二,我用的是Ajax,怎么才能get到表单当中的文件呢?

对于问题一,很好解决直接各种方式hide这个input标签即可,再主动触发 click()

  1. var imgFrom = document.getElementById("inputArea");

  2. function loadImg(){

  3.    imgFrom.click();

  4. }

对于问题二,这就要介绍一下 FormData 对象了。

XMLHttpRequest Level 2添加了一个新的接口FormData.利用FormData对象,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,我们还可以使用XMLHttpRequest的send()方法来异步的提交这个"表单". 比起普通的ajax,使用FormData的最大优点就是我们可以异步上传一个二进制文件.

摘自MDN Web docs - Web技术文档/Web API 接口/FormData

正如上面的文档所说 FormData 对象可以干的事无非就是用javascript模拟表单控件,也正因为如此所以可以在模拟的表单中放入一个文件:

  1. var myFrom = new FormData();

  2. var imageData = imgFrom.files[0];//获取表单中第一个文件

  3. myFrom.append("image",imageDate);//向表单中添加一个键值对

  4. console.log(myFrom.getAll("image"));//获取表单中image字段对应的值,结果见下图

正如我们所见,文件我们已经通过Web拿到手了。

图片的展现

既然是要上传图片,我们肯定得知道自己传的是啥图片啊,所以下一步就是如何把读取的图片展现在页面上了,正如上图中的显示,我的得到的图片是一个 File 对象,而 File 对象是特殊的 Blob 对象,那 Blob 对象又是个啥呢…

Blob 对象表示不可变的类似文件对象的原始数据。Blob表示不一定是JavaScript原生形式的数据。File 接口基于Blob,继承了 blob的功能并将其扩展使其支持用户系统上的文件。

摘自MDN Web docs - Web技术文档/Web API 接口/Blob

说实话,真是懵逼。但仔细理解下大概意思就是 Blob 对象是用来表示/承载文件对象的原始数据(二进制)的,借助一些博文会有助于理解:

  • js中关于Blob对象的介绍与使用 - 可乐Script

  • HTML5 Blob对象 - zdy0_2004

说到底,重点不在这,了解一下有个概念即可,重点在于我们怎么展示这个 File 对象

这就要请出 FileReader 对象了。

FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。

摘自MDN Web docs - Web技术文档/Web API 接口/FileReader

不难看出, FileReader 对象就是用来读取本地文件的,而这其方法 readAsDataURL() 就是我们要用的东西啦。

该方法会读取指定的 Blob 或 File 对象。读取操作完成的时候,readyState 会变成已完成(DONE),并触发 loadend 事件,同时 result 属性将包含一个data:URL格式的字符串(base64编码)以表示所读取文件的内容。

摘自MDN Web docs - Web技术文档/Web API 接口/FileReader/FileReader.readAsDataURL()

这里面又提到一个新名词 data:URL ,也就是说 readAsDataURL() 的作用就是能把文件转换为data:URL,不过这个data:URL又是什么呢,执行来看看:

  1. var reader = new FileReader(); //调用FileReader对象

  2. reader.readAsDataURL(imgData); //通过DataURL的方式返回图像

  3. reader.onload = function(e) {                

  4.    console.log(e.target.result); //看看你是个啥

  5. }

控制台的结果全脸懵逼。

可以通过这篇文章去大概了解一下 DATA URL简介及DATA URL的利弊 - 薛陈磊

说到底这dataURL我就粗略的理解它为 URL形式的data ,也就是说这段URL并不是与普通的URL一样指向某个地址,而是它本身就是数据,我们试着把这一堆字符粘到一个 src 属性中。

终于看到了,结果正如所料,将这段包含了数据的URL赋给一个 确实可以让数据被展现为图片。

至此,我们实现了本地文件的 读取 展现

指哪儿截哪儿 - 利用canvas的图片截取

温馨提示-乱入:看明白这里需要对canvas有基本的了解: MDN Web docs - Web技术文档/Web API接口/Canvas/Canvas教程

在Web上对图像进行操作,没有比canvas相关技术更合适的了,所以本文用canvas技术来实现对图片的截取。

canvas中的图片展现

在上文中,我们利用 展现出了我们选择的图片,但是我们的图片截取功能可是要利用 来实现的,所以怎么在 中展现我们刚才获取的图片就是下一步要干的事情了。

canvas的API中自带 drawImage() 函数,其作用就是在 中渲染一张图片出来,其可以支持多种图片来源见: MDN Web docs - Web技术文档/Web API接口/CanvasRenderingContext2D/CanvasRenderingContext2D.drawImage()

最简单的,我们直接把刚刚显示图片的那个 传入是不是就可以呢?

  1. var theCanvas = document.getElementById("imgCanvas");

  2. var canvasImg = theCanvas.getContext("2d");//获取2D渲染背景

  3. var img = document.getElementById("image");

  4. img.onload = function(){//确认图片已载入    

  5.    canvasImg.drawImage(img,0,0);

  6. }

结果如下:

从图中看,左侧是之前的' ',右侧是渲染了图片信息的 。这么看来虽然成功?在 中渲染出了图片但是有两个明显的问题:

  1. 左边的' '留着干啥?

  2. 右边看上去是不是有点不一样?

这俩问题其实都好办,针对第一个问题,我们其实可以根本不用实体的' '直接利用'Image'对象即可,第二个问题明显是因为 的大小与获取到的图片大小不一致所产生的,综合这两点,对代码进行进化!

  1. var theCanvas = document.getElementById("imgCanvas");

  2. var canvasImg = theCanvas.getContext("2d");

  3. var img = new Image();//创建img对象

  4. reader.onload = function(e) {                

  5.    img.src = e.target.result;

  6. }

  7. img.onload = function(){

  8.    theCanvas.Width = img.width;//将img对象的长款赋给canvas标签

  9.    theCanvas.height = img.height;    

  10.    canvasImg.drawImage(img,0,0);

  11. }

结果与我们所期待的一样,至此我们成功的在 中展现了从本地获取的图片。

canvas中图片的截取

其实截图,说白了就是在一个图像上,获取某个区域中的图像信息。

canvas作为专门用来处理图像及像素相关的一套API,获取区域中的相关图像信息可以说是再简单不过的事情,利用 getImageData() 函数即可 //详情,当然我们不光要把图像信息获取到,最好还能展现出来我们的截图结果,这里就要用到与之相对的 putImageData() 函数 //详情。

  1. var resultCanvas = document.getElementById("resultCanvas");

  2. var resultImg = resultCanvas.getContext("2d");

  3. var cutData = canvasImg.getImageData(100,100,200,200);

  4. resultImg.putImageData(cutData,0,0);

结果如图:

我也要画一个圈/框

既然这个工具是面向用户的,截图的过程肯定是要所见即所得的,在函数 getImageData() 中有4个参数,分别是截图起点的两个坐标和区域的宽度及高度,所以问题就变成了如何更合理的让用户输入这4个值。

其实现存的主流解决方案就做的非常好了: 在图上拖动鼠标,拉出一个框,这个框内就是用户希望截取的区域。

在画布上画出一个框很简单,只需用到 strokeRect() 函数 //详情。但是让用户自己拖出一个框就比较复杂了,先分析一下用户的一套动作都有什么:

  1. 用户选定起始点,点下鼠标左键。

  2. 用户选定截图区域的大小,保持鼠标左键不抬起,同时移动鼠标选择。

  3. 用户完成选择,抬起鼠标左键。

回过头再来看程序需要干什么:

  1. 获取起始点的坐标,并记录为已点击状态。

  2. 判断一下如果为已点击状态那么,获取每一次移动/帧的鼠标坐标,并计算出与起始点之间的横纵坐标距离,而这距离就是所画框的长度和宽度,清除上一帧的 整个画面 ,再绘制一个新的图片再画一个新的框,同时按照框的起始坐标及宽高,截取图像信息,再清除预览区域的上一帧的画布,再将这一帧的图像信息载入。

  3. 鼠标抬起后,停止记录及绘制,保持最终一帧的框停留在画面上。

在这里,要说明一下,为什么非要清除整个画面不可,其实可以把通过 canvas.getContext("2d") 获取到的 2D 画布的渲染上下文 //详情 就当作一块画布,已经渲染出来的东西就已经留在了上面,无法再修改,如果想要更改画面上已经存在的元素的大小位置形状等等属性,那么在程序层面,就 只能 (个人理解,不一定对,如果有问题请一定跟我唠唠)把之前的画布清空再重新渲染。

这个思路与我们之前端开发中动画相关的开发思路不同,并不是像之前那样直接操作现有元素属性就可以改变该元素在画面上的呈现结果的,而在这里其实更像是在现实生活中的动画制作原理就是: 每一帧都需要重新绘制整张画面 。而其实这是任何动画渲染方式的最底层思路与行为。话说回来按照上文相关的开发思路,实现这个功能的代码如下:

  1. var flag = false;//记录是否为点击状态的标记

  2. var W = img.width;

  3. var H = img.height;

  4. var startX = 0;

  5. var startY = 0;

  6. //当鼠标被按下

  7. theCanvas.addEventListener("mousedown", e => {

  8.    flag = true;//改变标记状态,置为点击状态

  9.    startX = e.clientX;//获得起始点横坐标

  10.    startY = e.clientY;//获得起始点纵坐标

  11. })

  12. //当鼠标在移动

  13. theCanvas.addEventListener("mousemove", e => {

  14.    if(flag){//判断鼠标是否被拖动

  15.        canvasImg.clearRect(0,0,W,H);//清空整个画面

  16.        canvasImg.drawImage(img,0,0);//重新绘制图片

  17.        canvasImg.strokeRect(startX, startY, e.clientX - startX, e.clientY - startY);//绘制黑框

  18.        resultImg.clearRect(0,0,cutData.width,cutData.height);//清空预览区域

  19.        cutData = canvasImg.getImageData(startX, startY, e.clientX - startX, e.clientY - startY);//截取黑框区域图片信息

  20.        resultImg.putImageData(cutData,0,0);//将图片信息赋给预览区域

  21.    }

  22. })







请到「今天看啥」查看全文