本项目支持IE8+,测试环境IE8,IE9,IE10,IE11,Chrome,FireFox测试通过
另:本项目并不支持Vue,React等,也不建议,引入JQuery和Vue、React本身提倡的开发方式并不一致
注:本项目未对移动端进行测试,不保证移动端可以使用,并且也不推荐移动端使用这个项目,移动端建议使用Cropper插件,功能更丰富,也更强大,使用更便捷,地址:https://github.com/fengyuanchen/cropper
在工作中会有很多项目需要实现图片上传裁剪预览的功能,但目前很多插件基本上在较低版本的浏览器上使用的是flash来实现,我则更倾向于尽量使用js来解决,在js无能为力的时候再借助其他的工具(其实更多的还是因为不会,又懒得去学╮(╯﹏╰)╭,毕竟flash已经可以说是被淘汰了),当然,实现这些功能的插件也不少,只不过,基本上都没有实现页面无刷新上传的功能,或者都是需要先上传图片再对已上传的图片进行裁剪,或者两个问题都有,这就不是我想要的功能了,我希望的是能够在还未上传的时候就对其进行预览,并裁剪,,也就是实现本地预览裁剪并上传裁剪结果。那么,废话收了那么多,动手呗!
此项目中使用的裁剪数据采集插件是Jcrop插件,这个插件感觉做的还是挺好的,那么下一步就是进一步封装这个插件咯,也就是说,需要自己来实现预览和上传,这个最终能够支持到IE8+
先来上个效果图先
左侧大图就是我们要上传并进行裁剪的原图,而右侧则是最终裁剪的结果,恩,看起来好像还是可以的,恩,好,继续
html结构很简单,无非就是引入css,js,写个div就够了,来看下html结构先(样式直接写在了页面上,并没有进行分离,懒-_-|||)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>IE Image Upload</title> <link rel="stylesheet" href="css/jquery.Jcrop.min.css"> <script src="js/jquery-1.8.3.min.js"></script> <script type="text/javascript" src="js/jquery.Jcrop.min.js"></script> <script type="text/javascript" src="js/imgCropUpload.js"></script> <style type="text/css"> body { font-size: 16px; font-family:"Microsoft YaHei",Arial, Helvetica, sans-serif } *, *:before, *:after { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; } .crop-picker-wrap { position: relative; 100px; height: 30px; overflow: hidden; } .crop-picker { 100%; height: 100%; line-height: 1; -webkit-appearance: none; margin: 0; border: none; border-radius: 5px; padding: 9px 0; background-color: #1ab2ff; color: #fff; cursor: pointer; } .crop-picker-file { position: absolute; top: 0; right: 0; height: 100%; opacity: 0; cursor: pointer; filter: alpha(opacity=0); } .crop-wrapper { display: inline-block; min- 750px; margin: 10px 0; padding: 10px; border: 1px solid #ccc; border-radius: 5px; box-shadow: 0 0 5px 2px #ccc; } .crop-container { font-size: 0; } .crop-container img[src=""] { visibility: hidden; } .crop-area-wrapper, .crop-preview-wrapper { display: inline-block; vertical-align: top; } .crop-area-wrapper { 500px; height: 400px; } .crop-preview-wrapper { 200px; height: 200px; margin-left: 28px; overflow: hidden; } .crop-preview-container { position: relative; overflow: hidden; } .crop-operate { text-align: center; margin: 10px 0; } .crop-save, .crop-cancel { display: inline-block; vertical-align: middle; 150px; height: 50px; line-height: 50px; -webkit-appearance: none; margin: 0 5px; border: none; border-radius: 5px; background-color: #1ab2ff; color: #fff; cursor: pointer; } .crop-hidden { display: none; } </style> </head> <body> <div id="TCrop"></div> <script type="text/javascript"> $(function() { Crop.init({ id: 'TCrop', /* 上传路径 */ url: '', /* 允许上传的图片的后缀 */ allowsuf: ['jpg', 'jpeg', 'png', 'gif'], /* JCrop参数设置 */ cropParam: { minSize: [50, 50], // 选框最小尺寸 maxSize: [300, 300], // 选框最大尺寸 allowSelect: true, // 允许新选框 allowMove: true, // 允许选框移动 allowResize: true, // 允许选框缩放 dragEdges: true, // 允许拖动边框 onChange: function(c) {}, // 选框改变时的事件,参数c={x, y, x1, y1, w, h} onSelect: function(c) {} // 选框选定时的事件,参数c={x, y, x1, y1, w, h} }, /* 是否进行裁剪,不裁剪则按原图上传,默认进行裁剪 */ isCrop: true, /* 图片上传完成之后的回调,无论是否成功上传 */ onComplete: function(data) { console.log('upload complete!'); } }); }); </script> </body> </html>
接下来我们来看看js的实现
首先我们来分析下流程
简单点说就是:选图片 -> 预览 -> 裁剪 -> 上传,就像下面这样
详细点:选择文件 -> 判断文件类型是否允许上传 -> 判断浏览器类型 -> 预览图片 -> 裁剪并实时更新裁剪结果图 -> 上传原图及裁剪信息,就像下面这样
注意:这里有一点需要关注下,那就是,有些同学可能会发现在运行我这个项目的时候,那个隐藏的input文件选择按钮并没有被真正的隐藏起来,而是定位到了选择图片那个标签上,为什么要这么做呢?而不直接使用js来出发input的click事件?原因在于,为了兼容IE8!在IE8下,使用js触发的input文件选择按钮,并不能进行提交,会出现拒绝访问的错误,这是IE8的一个安全策略,必须是用户手动触发的input选择的文件才可以在form表单中被提交
本地图片预览
对于这个项目而言,最重要的就是在不同的浏览器里面如何进行预览,至于裁剪,则全部交由Jcrop插件来进行,那么我们就来看看如何实现预览吧
对于支持HTML5的File接口的浏览器
对于支持的浏览器,则可以直接使用FileReader获取图片的数据,并在页面进行展示,js如下:
var img = document.createElement('img'); img.style.visibility = 'hidden'; cropArea.appendChild(img); img.onload = function() { /* 在图片加载完成之后便可以获取原图的大小,根据原图大小和预览区域大小获取图片的缩放比例以及原图在预览时所展现的大小 */ var scaleOpt = _getScale(cropArea.clientWidth, cropArea.clientHeight, img.offsetWidth, img.offsetHeight); img.setAttribute('style', 'position: absolute;visibility: visible; ' + scaleOpt.w + 'px;height: ' + scaleOpt.h + 'px'); if(!opt.isCrop) {return ;} var cropPreviewImg = img.cloneNode(true); cropPreview.appendChild(cropPreviewImg); _startCrop(img, jcropOpt); /* 记录原始比例,上传数据需要还原实际裁剪尺寸 */ Crop.ratio = scaleOpt.scale; /* 记录裁剪图片及裁剪预览图像对象,更新预览图时需要使用 */ Crop.cropPreview = { cropAreaImg: img, cropPreviewImg: cropPreviewImg }; }; var fr = new FileReader(); fr.onload = function(eve) { img.src = eve.target.result; } fr.readAsDataURL(fileInp.files[0]);
对于不支持HTML5的File接口的浏览器
对于不支持的IE8、9浏览器,则必须要使用滤镜来进行实现,js实现如下,
PS:至于不考虑其他不支持的浏览器,原因在于,现在浏览器除IE8、9是被绑定在windows上并且基本不会自动升级外,其余现代浏览器都已实现了File接口,故不去考虑
var img = document.createElement('div'); img.style.visibility = 'hidden'; img.style.width = '100%'; img.style.height = '100%'; cropArea.appendChild(img); fileInp.select(); var src = document.selection.createRange().text; // console.log(document.selection.createRange()); var img_filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod='image',src='" + src + "')"; img.style.filter = img_filter; /* 需等待滤镜加载完毕之后才能进行下一步操作 */ window.setTimeout(function() { _loadFiter(cropArea, img); }, 100); /* 加载滤镜,等待两秒,超时则判定加载失败 */ function _loadFiter(cropArea, img) { var time = 0; if(img.offsetWidth != cropArea.clientWidth) { /* 滤镜加载成功,进入裁剪流程 */ _filterCrop(cropArea, img); } else { time ++; if(time < 20) { window.setTimeout(function() { _loadFiter(cropArea, img); }, 100); } else { alert('图片加载失败,请重试!'); } } }; /* 使用滤镜的裁剪 */ function _filterCrop(cropArea, img) { var scaleOpt = _getScale(cropArea.clientWidth, cropArea.clientHeight, img.offsetWidth, img.offsetHeight); /* 更改滤镜设置 */ var s_filter = img.style.filter.replace(/sizingMethod='image'/g, "sizingMethod='scale'"); var jcropOpt = _getOpt().cropParam; img.setAttribute('style', 'position: absolute;visibility: visible; ' + scaleOpt.w + 'px;height: ' + scaleOpt.h + 'px;filter: ' + s_filter); if(!_getOpt().isCrop) {return ;} var cropPreview = cropArea.nextSibling.firstChild; var cropPreviewImg = img.cloneNode(true); cropPreview.appendChild(cropPreviewImg); _startCrop(img, jcropOpt); /* 记录原始比例,上传数据需要还原实际裁剪尺寸 */ Crop.ratio = scaleOpt.scale; /* 记录裁剪图片及裁剪预览图像对象,更新预览图时需要使用 */ Crop.cropPreview = { cropAreaImg: img, cropPreviewImg: cropPreviewImg }; };
以上便可以实现本地预览,以下放出所有封装后的源码
如果有同学需要使用这个项目的话,可以直接将以下代码封装copy,并保存为imgCropUpload.js,再如上面那个HTML页面那样引入即可
;(function(global, $, Crop) { var defaultOpt = { /* 整个图片选择、裁剪、上传区域的最外围包裹元素id,默认TCrop */ id: 'TCrop', /* 上传路径 */ url: '', /* 允许上传的图片的后缀,暂时支持以下四种,其余格式图片未测试 */ allowsuf: ['jpg', 'jpeg', 'png', 'gif'], /* JCrop参数设置 */ cropParam: { minSize: [50, 50], // 选框最小尺寸 maxSize: [300, 300], // 选框最大尺寸 allowSelect: true, // 允许新选框 allowMove: true, // 允许选框移动 allowResize: true, // 允许选框缩放 dragEdges: true, // 允许拖动边框 onChange: function(c) {}, // 选框改变时的事件 onSelect: function(c) {} // 选框选定时的事件,参数c={x, y, x1, y1, w, h} }, /* 是否进行裁剪,不裁剪则按原图上传,默认进行裁剪 */ isCrop: true, /* 图片上传完成之后的回调,无论是否成功上传 */ onComplete: function(data) { console.log('upload complete!'); } }; /* 记录jcrop实例 */ var jcropApi; /* 创建Dom结构 */ /* 完整DOM结构 --s-- */ /* <iframe id="uploadIfr" name="uploadIfr" class="crop-upload-ifr"></iframe> <form action="index.html" enctype="multipart/form-data" method="post" target="uploadIfr"> <input type="hidden" name="cropData"> <div class="crop-picker-wrap"> <button class="crop-picker" type="button">选择图片</button> <input type="file" id="file" class="crop-picker-file"> </div> <div class="crop-wrapper"> <div class="crop-container clearfix"> <div class="crop-area-wrapper"><img src="" alt=""></div> <div class="crop-preview-wrapper"><img src="" alt=""></div> </div> <div class="crop-operate"> <div class="crop-save">上传原图</div> <div class="crop-save">保存截图</div> <div class="crop-cancel">取消</div> </div> </div> </form> */ /* 完整DOM结构 --e-- */ function _createDom($wrap, opt) { var accept = 'image/' + opt.allowsuf.join(', image/'); var $ifr = $('<iframe id="uploadIfr" name="uploadIfr" class="crop-hidden"></iframe>'); var $form = $('<form action="' + opt.url + '" enctype="multipart/form-data" method="post" target="uploadIfr"/>'); var $cropDataInp = $('<input type="hidden" name="cropData">'); var $picker = $('<div class="crop-picker-wrap"><button class="crop-picker" type="button">选择图片</button></div>'); var $fileInp = $('<input type="file" id="file" accept="' + accept + '" class="crop-picker-file">'); $picker.append($fileInp); $form.append($cropDataInp).append($picker); var $cropWrap = $('<div class="crop-wrapper crop-hidden"/>'); var $cropArea = $('<div class="crop-area-wrapper"></div>'); var $cropPreviewWrap = $('<div class="crop-preview-wrapper"></div>'); var $cropPreview = $('<div class="crop-preview-container"/>'); $cropPreviewWrap.append($cropPreview); var $cropContainer = $('<div class="crop-container"/>').append($cropArea).append($cropPreviewWrap); $cropWrap.append($cropContainer); // var $saveSource = $('<div class="crop-save">上传原图</div>'); var $save = $('<div class="crop-save">保存</div>'); var $cropCancel = $('<div class="crop-cancel">取消</div>'); var $cropOpe = $('<div class="crop-operate"/>').append($save).append($cropCancel); if(!opt.isCrop) { $cropPreviewWrap.addClass('crop-hidden'); } $cropWrap.append($cropOpe); $form.append($cropWrap); $wrap.append($ifr).append($form); return { $ifr: $ifr, $form: $form, $cropDataInp: $cropDataInp, $cropPicker: $picker, $fileInp: $fileInp, $cropWrap: $cropWrap, $cropArea: $cropArea, $cropPreview: $cropPreview, // $saveSource: $saveSource, $save: $save, $cancel: $cropCancel }; }; /* * 绑定事件 * */ function _bind($cropObj, opt) { var $cropPicker = $cropObj.$cropPicker; var $fileInp = $cropObj.$fileInp; var $save = $cropObj.$save; var $cancel = $cropObj.$cancel; var $ifr = $cropObj.$ifr; $fileInp.change(function(eve) { if(!this.value) {return ;} var fileSuf = this.value.substring(this.value.lastIndexOf('.') + 1); if(!_checkSuf(fileSuf, opt.allowsuf)) { alert('只允许上传后缀名为' + opt.allowsuf.join(',') + '的图片'); return ; } /* 进入裁剪流程 */ _crop($cropObj, this); $cropPicker.addClass('crop-hidden').next().removeClass('crop-hidden'); }); $save.click(function(eve) { eve.preventDefault(); Crop.upload(); }); $cancel.click(function(eve) { eve.preventDefault(); Crop.cancel(); }); /* iframe的load应该延迟绑定,避免首次插入文档中加载完毕时触发load事件 */ window.setTimeout(function() { $ifr.load(function() { var body = this.contentWindow.document.body; var text = body.innerText; opt.onComplete(text); }); }, 100); }; /* 检查文件是否符合上传条件 */ function _checkSuf(fileSuf, suffixs) { for(var i = 0, j = suffixs.length;i < j; i ++) { if(fileSuf.toLowerCase() == suffixs[i].toLowerCase()) { return true; } } return false; }; /* 主要裁剪流程 */ function _crop($cropObj, fileInp) { var cropArea = $cropObj.$cropArea.get(0); var cropPreview = $cropObj.$cropPreview.get(0); var opt = _getOpt(); var jcropOpt = opt.cropParam; cropArea.innerHTML = ''; if(fileInp.files && fileInp.files[0]) { var img = document.createElement('img'); img.style.visibility = 'hidden'; cropArea.appendChild(img); img.onload = function() { /* 在图片加载完成之后便可以获取原图的大小,根据原图大小和预览区域大小获取图片的缩放比例以及原图在预览时所展现的大小 */ var scaleOpt = _getScale(cropArea.clientWidth, cropArea.clientHeight, img.offsetWidth, img.offsetHeight); img.setAttribute('style', 'position: absolute;visibility: visible; ' + scaleOpt.w + 'px;height: ' + scaleOpt.h + 'px'); if(!opt.isCrop) {return ;} var cropPreviewImg = img.cloneNode(true); cropPreview.appendChild(cropPreviewImg); _startCrop(img, jcropOpt); /* 记录原始比例,上传数据需要还原实际裁剪尺寸 */ Crop.ratio = scaleOpt.scale; /* 记录裁剪图片及裁剪预览图像对象,更新预览图时需要使用 */ Crop.cropPreview = { cropAreaImg: img, cropPreviewImg: cropPreviewImg }; }; var fr = new FileReader(); fr.onload = function(eve) { img.src = eve.target.result; } fr.readAsDataURL(fileInp.files[0]); } else { var img = document.createElement('div'); img.style.visibility = 'hidden'; img.style.width = '100%'; img.style.height = '100%'; cropArea.appendChild(img); fileInp.select(); var src = document.selection.createRange().text; // console.log(document.selection.createRange()); var img_filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod='image',src='" + src + "')"; img.style.filter = img_filter; /* 需等待滤镜加载完毕之后才能进行下一步操作 */ window.setTimeout(function() { _loadFiter(cropArea, img); }, 100); } }; /* 加载滤镜,等待两秒,超时则判定加载失败 */ function _loadFiter(cropArea, img) { var time = 0; if(img.offsetWidth != cropArea.clientWidth) { /* 滤镜加载成功,进入裁剪流程 */ _filterCrop(cropArea, img); } else { time ++; if(time < 20) { window.setTimeout(function() { _loadFiter(cropArea, img); }, 100); } else { alert('图片加载失败,请重试!'); } } }; /* 使用滤镜的裁剪 */ function _filterCrop(cropArea, img) { var scaleOpt = _getScale(cropArea.clientWidth, cropArea.clientHeight, img.offsetWidth, img.offsetHeight); /* 更改滤镜设置 */ var s_filter = img.style.filter.replace(/sizingMethod='image'/g, "sizingMethod='scale'"); var jcropOpt = _getOpt().cropParam; img.setAttribute('style', 'position: absolute;visibility: visible; ' + scaleOpt.w + 'px;height: ' + scaleOpt.h + 'px;filter: ' + s_filter); if(!_getOpt().isCrop) {return ;} var cropPreview = cropArea.nextSibling.firstChild; var cropPreviewImg = img.cloneNode(true); cropPreview.appendChild(cropPreviewImg); _startCrop(img, jcropOpt); /* 记录原始比例,上传数据需要还原实际裁剪尺寸 */ Crop.ratio = scaleOpt.scale; /* 记录裁剪图片及裁剪预览图像对象,更新预览图时需要使用 */ Crop.cropPreview = { cropAreaImg: img, cropPreviewImg: cropPreviewImg }; }; /* 开始裁剪,初始化裁剪插件 */ function _startCrop(img, jcropOpt) { var imgW = img.offsetWidth; var imgH = img.offsetHeight; var minW = jcropOpt.minSize[0], minH = jcropOpt.minSize[1]; var offsetWidth = (imgW / 2) - (minW / 2); var offsetHeight = (imgH / 2) - (minH / 2); var obj = { x: offsetWidth, y: offsetHeight, x2: offsetWidth + minW, y2: offsetHeight + minH, w: minW, h: minH }; $(img).Jcrop(jcropOpt, function() { jcropApi = this; this.animateTo([obj.x, obj.y, obj.x2, obj.y2]); }); }; /* 获取配置参数opt */ function _getOpt() { var id = Crop.crop.id; var cropDom = document.getElementById(id); var opt = $.data(cropDom, 'crop').opt; return opt; }; /* * 获取缩放比例 * * 原始宽高vw,vh * 实际显示宽高sw,sh * 返回: * {w,h,scale:max(sw/vw,sh/vh)} * w,h均为缩放到sw、sh后的宽高 */ function _getScale(vw, vh, sw, sh) { vw = Number(vw); vh = Number(vh); sw = Number(sw); sh = Number(sh); if(vw <= 0 || vh <= 0) { console.log('参数不能为0'); return false; } var wScale = sw / vw; var hScale = sh / vh; var scale = 1, w, h; if(wScale > hScale) { scale = wScale; w = vw; h = sh / scale; } else { scale = hScale; h = vh; w = sw / scale; } return { scale: scale, w: w, h: h }; }; /* 更新裁剪预览图 */ function _updatePreview(c) { var cropAreaImg = Crop.cropPreview.cropAreaImg; var cropPreviewImg = Crop.cropPreview.cropPreviewImg; var $cropObj = $.data(document.getElementById(Crop.crop.id), 'crop').$cropObj; var $cropDataInp = $cropObj.$cropDataInp; var $cropPreview = $cropObj.$cropPreview; var $previewParent = $cropPreview.parent(); var vw = $previewParent.width(), vh = $previewParent.height(); var scaleOpt = _getScale(vw, vh, c.w, c.h); $cropPreview.width(scaleOpt.w); $cropPreview.height(scaleOpt.h); var width = $(cropAreaImg).width() / scaleOpt.scale; var height = $(cropAreaImg).height() / scaleOpt.scale; var top = -(c.y / scaleOpt.scale); var left = -(c.x / scaleOpt.scale); cropPreviewImg.style.width = width + 'px'; cropPreviewImg.style.height = height + 'px'; cropPreviewImg.style.top = top + 'px'; cropPreviewImg.style.left = left + 'px'; _setCropData($cropDataInp, c); }; /* 设置裁剪数据 */ function _setCropData($cropDataInp, c) { var ratio = Crop.ratio; var data = { x: c.x * ratio, y: c.y * ratio, w: c.w * ratio, h: c.h * ratio }; var dataJson = JSON.stringify(data); $cropDataInp.val(dataJson); }; /* 扩展配置参数,尤其是Jcrop裁剪参数中的onSelect,onChange参数 */ function _extendOpt(opt) { opt = $.extend(true, {}, defaultOpt, opt); var select = opt.cropParam.onSelect; var change = opt.cropParam.onChange; if(Object.prototype.toString.call(select) == '[object Function]') { opt.cropParam.onSelect = function(c) { _updatePreview.call(jcropApi, c); select.call(jcropApi, c); }; } else { opt.cropParam.onSelect = _updatePreview; } if(Object.prototype.toString.call(change) == '[object Function]') { opt.cropParam.onChange = function(c) { _updatePreview.call(jcropApi, c); change.call(jcropApi, c); } } else { opt.cropParam.onChange = _updatePreview; } return opt; }; /* 初始化上传裁剪区域 */ function init(opt) { var opt = _extendOpt(opt); var $uploadCropWrap = $('#' + opt.id); var hasDom = true; if($uploadCropWrap.length == 0) { $uploadCropWrap = $('<div id="' + opt.id + '" />'); hasDom = false; } /* 清空Dom原有内部结构 */ $uploadCropWrap.html(''); var $cropObj = _createDom($uploadCropWrap, opt); $.data($uploadCropWrap.get(0), 'crop', {opt: opt, $cropObj: $cropObj}); hasDom || $('body').append($uploadCropWrap); _bind($cropObj, opt); Crop.crop = {id: opt.id, hasDom: hasDom}; }; /* 上传 */ function upload() { var id = (Crop.crop && Crop.crop.id) || ''; var dom = document.getElementById(id); if(!dom) { return ; } var form = $.data(dom, 'crop').$cropObj.$form.get(0); form.submit(); }; /* 取消裁剪 */ function cancel() { var id = (Crop.crop && Crop.crop.id) || ""; var dom = document.getElementById(id); if(!dom) { return ; } var $cropObj = $.data(dom, 'crop').$cropObj; $cropObj.$cropWrap.addClass('crop-hidden'); $cropObj.$cropPicker.removeClass('crop-hidden'); }; /* 销毁上传裁剪区域 */ function destroy() { var crop = Crop.crop || {}; var $cropWrap = $('#' + crop.id); if(crop.hasDom === true) { $cropWrap.html(''); } else { $cropWrap.remove(); } }; if($.isEmptyObject(Crop)) { global.Crop = Crop = { init: init, upload: upload, cancel: cancel, destroy: destroy, crop: {} }; } else { Crop = $.extend(Crop, { init: init, upload: upload, cancel: cancel, destroy: destroy, crop: {} }); } })(window, jQuery, window.Crop || {});
后台接受到的数据将会是两个,1、上传的图片的二进制文件,2、json格式的裁剪数据,例如:'{"x":366.592,"y":208.89600000000002,"w":389.12,"h":335.872}'
由于后端代码属于工作上的业务代码,这里不再放出,其实实现也很简单,也没什么复杂的逻辑,只需要对接收到的图片直接进行裁剪即可,因为传给后端的裁剪数据就是根据原图的大小进行裁剪的数据
恩,以上,便是我个人在工作中遇到的关于本地图片上传预览裁剪遇到的问题,并将之整理实现,我知道还有很多不足,我会继续努力的。
各位如若发现什么问题,也可以指出,一起探讨进步,谢谢!