移动前端—H5实现图片先压缩再上传
?
在做移動端圖片上傳的時候,用戶傳的都是手機本地圖片,而本地圖片一般都相對比較大,拿iphone6來說,平時拍很多圖片都是一兩M的,如果直接這樣上傳,那圖片就太大了,如果用戶用的是移動流量,完全把圖片上傳顯然不是一個好辦法。
目前來說,HTML5的各種新API都在移動端的webkit上得到了較好的實現。根據查看caniuse,本demo里使用到的FileReader、Blob、Formdata對象均已在大部分移動設備瀏覽器中得到了實現(safari6.0 、android 3.0 ),所以直接在前端壓縮圖片,已經成了很多移動端圖片上傳的必備功能了。
在移動端壓縮圖片并且上傳主要用到filereader、canvas 以及 formdata 這三個h5的api。邏輯并不難。整個過程就是:
(1)用戶使用input file上傳圖片的時候,用filereader讀取用戶上傳的圖片數據(base64格式)
(2)把圖片數據傳入img對象,然后將img繪制到canvas上,再調用canvas.toDataURL對圖片進行壓縮
(3)獲取到壓縮后的base64格式圖片數據,轉成二進制塞入formdata,再通過XmlHttpRequest提交formdata。
如此三步,就完成了圖片的壓縮和上傳。
說起來好像挺簡單,其實還是有些坑的。接下來就直接用代碼進行分析:
【一】獲取圖片數據
先是獲取圖片數據,也就是監聽input file的change事件,然后獲取到上傳的文件對象files,將類數組的files轉成數組,然后進行forEach遍歷。
接著判斷文件類型,如果不是圖片則不作處理。如果是圖片就實例化一個filereader,以base64格式讀取上傳的文件數據,判斷數據長度,如果大于200KB的圖片就調用compress方法進行壓縮,否則調用upload方法進行上傳。
filechooser.onchange = function () {if (!this.files.length) return;var files = Array.prototype.slice.call(this.files);if (files.length > 9) {alert("最多同時只可上傳9張圖片");return;}files.forEach(function (file, i) {if (!/\/(?:jpeg|png|gif)/i.test(file.type)) return;var reader = new FileReader();var li = document.createElement("li");li.innerHTML = '<div class="progress"><span></span></div>';$(".img-list").append($(li));reader.onload = function () {var result = this.result;var img = new Image();img.src = result;//如果圖片大小小于200kb,則直接上傳if (result.length <= maxsize) {$(li).css("background-image", "url(" result ")");img = null;upload(result, file.type, $(li));return;}// 圖片加載完畢之后進行壓縮,然后上傳if (img.complete) {callback();} else {img.onload = callback;}function callback() {var data = compress(img);$(li).css("background-image", "url(" data ")");upload(data, file.type, $(li));img = null;}};reader.readAsDataURL(file);})};【2】壓縮圖片
上面做完圖片數據的獲取后,就可以做compress壓縮圖片的方法了。而壓縮圖片也并不是直接把圖片繪制到canvas再調用一下toDataURL就行的。
在IOS中,canvas繪制圖片是有兩個限制的:
首先是圖片的大小,如果圖片的大小超過兩百萬像素,圖片也是無法繪制到canvas上的,調用drawImage的時候不會報錯,但是你用toDataURL獲取圖片數據的時候獲取到的是空的圖片數據。
再者就是canvas的大小有限制,如果canvas的大小大于大概五百萬像素(即寬高乘積)的時候,不僅圖片畫不出來,其他什么東西也都是畫不出來的。
應對第一種限制,處理辦法就是瓦片繪制了。瓦片繪制,也就是將圖片分割成多塊繪制到canvas上,我代碼里的做法是把圖片分割成100萬像素一塊的大小,再繪制到canvas上。
而應對第二種限制,我的處理辦法是對圖片的寬高進行適當壓縮,我代碼里為了保險起見,設的上限是四百萬像素,如果圖片大于四百萬像素就壓縮到小于四百萬像素。四百萬像素的圖片應該夠了,算起來寬高都有2000X2000了。
如此一來就解決了IOS上的兩種限制了。
除了上面所述的限制,還有兩個坑,一個就是canvas的toDataURL是只能壓縮jpg的,當用戶上傳的圖片是png的話,就需要轉成jpg,也就是統一用canvas.toDataURL('image/jpeg', 0.1) ,?類型統一設成jpeg,而壓縮比就自己控制了。
另一個就是如果是png轉jpg,繪制到canvas上的時候,canvas存在透明區域的話,當轉成jpg的時候透明區域會變成黑色,因為canvas的透明像素默認為rgba(0,0,0,0),所以轉成jpg就變成rgba(0,0,0,1)了,也就是透明背景會變成了黑色。解決辦法就是繪制之前在canvas上鋪一層白色的底色。
function compress(img) {var initSize = img.src.length;var width = img.width;var height = img.height;//如果圖片大于四百萬像素,計算壓縮比并將大小壓至400萬以下var ratio;if ((ratio = width * height / 4000000)>1) {ratio = Math.sqrt(ratio);width /= ratio;height /= ratio;}else {ratio = 1;}canvas.width = width;canvas.height = height;// 鋪底色ctx.fillStyle = "#fff";ctx.fillRect(0, 0, canvas.width, canvas.height);//如果圖片像素大于100萬則使用瓦片繪制var count;if ((count = width * height / 1000000) > 1) {count = ~~(Math.sqrt(count) 1); //計算要分成多少塊瓦片// 計算每塊瓦片的寬和高var nw = ~~(width / count);var nh = ~~(height / count);tCanvas.width = nw;tCanvas.height = nh;for (var i = 0; i < count; i ) {for (var j = 0; j < count; j ) {tctx.drawImage(img, i * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh);ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh);}}} else {ctx.drawImage(img, 0, 0, width, height);}//進行最小壓縮var ndata = canvas.toDataURL('image/jpeg', 0.1);console.log('壓縮前:' initSize);console.log('壓縮后:' ndata.length);console.log('壓縮率:' ~~(100 * (initSize - ndata.length) / initSize) "%");tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;return ndata;}【三】圖片上傳
完成圖片壓縮后,就可以塞進formdata里進行上傳了,先將base64數據轉成字符串,再實例化一個ArrayBuffer,然后將字符串以8位整型的格式傳入ArrayBuffer,再通過BlobBuilder或者Blob對象,將8位整型的ArrayBuffer轉成二進制對象blob,然后把blob對象append到formdata里,再通過ajax發送給后臺即可。
XmlHttpRequest2中不僅可以發送大數據,還多出了比如獲取發送進度的API,我代碼里也進行了簡單的實現。
// 圖片上傳,將base64的圖片轉成二進制對象,塞進formdata上傳function upload(basestr, type, $li) {var text = window.atob(basestr.split(",")[1]);var buffer = new ArrayBuffer(text.length);var ubuffer = new Uint8Array(buffer);var pecent = 0 , loop = null;for (var i = 0; i < text.length; i ) {ubuffer[i] = text.charCodeAt(i);}var Builder = window.WebKitBlobBuilder || window.MozBlobBuilder;var blob;if (Builder) {var builder = new Builder();builder.append(buffer);blob = builder.getBlob(type);} else {blob = new window.Blob([buffer], {type: type});}var xhr = new XMLHttpRequest();var formdata = new FormData();formdata.append('imagefile', blob);xhr.open('post', '/cupload');xhr.onreadystatechange = function () {if (xhr.readyState == 4 && xhr.status == 200) {console.log('上傳成功:' xhr.responseText);clearInterval(loop);//當收到該消息時上傳完畢$li.find(".progress span").animate({'width': "100%"}, pecent < 95 ? 200 : 0, function () {$(this).html("上傳成功");});$(".pic-list").append('<a href="' xhr.responseText '">' xhr.responseText '<img src="' xhr.responseText '" /></a>')}};//數據發送進度,前50%展示該進度xhr.upload.addEventListener('progress', function (e) {if (loop) return;pecent = ~~(100 * e.loaded / e.total) / 2;$li.find(".progress span").css('width', pecent "%");if (pecent == 50) {mockProgress();}}, false);//數據后50%用模擬進度function mockProgress() {if (loop) return;loop = setInterval(function () {pecent ;$li.find(".progress span").css('width', pecent "%");if (pecent == 99) {clearInterval(loop);}}, 100)}xhr.send(formdata);}至此,整個上傳的前端圖片壓縮就完成了,因為是用了formdata提交,所以后臺接數據的時候就跟普通form表單提交數據一樣處理即可。
匯總前端代碼:
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport"><title>移動端圖片壓縮上傳demo</title><style>*{margin: 0;padding: 0;}li{list-style-type: none;}a,input{outline: none;-webkit-tap-highlight-color:rgba(0,0,0,0);}#choose{display: none;}canvas{width: 100%;border: 1px solid #000000;}#upload{display: block;margin: 10px;height: 60px;text-align: center;line-height: 60px;border: 1px solid;border-radius: 5px;cursor: pointer;}.touch{background-color: #ddd;}.img-list{margin: 10px 5px;}.img-list li{position: relative;display: inline-block;width: 100px;height: 100px;margin: 5px 5px 20px 5px;border: 1px solid rgb(100,149,198);background: #fff no-repeat center;background-size: cover;}.progress{position: absolute;width: 100%;height: 20px;line-height: 20px;bottom: 0;left: 0;background-color:rgba(100,149,198,.5);}.progress span{display: block;width: 0;height: 100%;background-color:rgb(100,149,198);text-align: center;color: #FFF;font-size: 13px;}.size{position: absolute;width: 100%;height: 15px;line-height: 15px;bottom: -18px;text-align: center;font-size: 13px;color: #666;}.tips{display: block;text-align:center;font-size: 13px;margin: 10px;color: #999;}.pic-list{margin: 10px;line-height: 18px;font-size: 13px;}.pic-list a{display: block;margin: 10px 0;}.pic-list a img{vertical-align: middle;max-width: 30px;max-height: 30px;margin: -4px 0 0 10px;}</style> </head> <body> <input type="file" id="choose" accept="image/*" multiple> <ul class="img-list"></ul> <a id="upload">上傳圖片</a> <span class="tips">只允許上傳jpg、png及gif</span> <div class="pic-list">你上傳的圖片(圖片有效期為1分鐘): </div><script src="/public/jquery-2.1.1.min.js"></script> <script>var filechooser = document.getElementById("choose");// 用于壓縮圖片的canvasvar canvas = document.createElement("canvas");var ctx = canvas.getContext('2d');// 瓦片canvasvar tCanvas = document.createElement("canvas");var tctx = tCanvas.getContext("2d");var maxsize = 100 * 1024;$("#upload").on("click", function() {filechooser.click();}).on("touchstart", function() {$(this).addClass("touch")}).on("touchend", function() {$(this).removeClass("touch")});filechooser.onchange = function() {if (!this.files.length) return;var files = Array.prototype.slice.call(this.files);if (files.length > 9) {alert("最多同時只可上傳9張圖片");return;}files.forEach(function(file, i) {if (!/\/(?:jpeg|png|gif)/i.test(file.type)) return;var reader = new FileReader();var li = document.createElement("li"); // 獲取圖片大小var size = file.size / 1024 > 1024 ? (~~(10 * file.size / 1024 / 1024)) / 10 "MB" : ~~(file.size / 1024) "KB";li.innerHTML = '<div class="progress"><span></span></div><div class="size">' size '</div>';$(".img-list").append($(li));reader.onload = function() {var result = this.result;var img = new Image();img.src = result;$(li).css("background-image", "url(" result ")");//如果圖片大小小于100kb,則直接上傳if (result.length <= maxsize) {img = null;upload(result, file.type, $(li));return;} // 圖片加載完畢之后進行壓縮,然后上傳if (img.complete) {callback();} else {img.onload = callback;}function callback() {var data = compress(img);upload(data, file.type, $(li));img = null;}};reader.readAsDataURL(file);})};// 使用canvas對大圖片進行壓縮function compress(img) {var initSize = img.src.length;var width = img.width;var height = img.height;//如果圖片大于四百萬像素,計算壓縮比并將大小壓至400萬以下var ratio;if ((ratio = width * height / 4000000) > 1) {ratio = Math.sqrt(ratio);width /= ratio;height /= ratio;} else {ratio = 1;}canvas.width = width;canvas.height = height; // 鋪底色ctx.fillStyle = "#fff";ctx.fillRect(0, 0, canvas.width, canvas.height);//如果圖片像素大于100萬則使用瓦片繪制var count;if ((count = width * height / 1000000) > 1) {count = ~~(Math.sqrt(count) 1); //計算要分成多少塊瓦片 // 計算每塊瓦片的寬和高var nw = ~~(width / count);var nh = ~~(height / count);tCanvas.width = nw;tCanvas.height = nh;for (var i = 0; i < count; i ) {for (var j = 0; j < count; j ) {tctx.drawImage(img, i * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh);ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh);}}} else {ctx.drawImage(img, 0, 0, width, height);}//進行最小壓縮var ndata = canvas.toDataURL('image/jpeg', 0.1);console.log('壓縮前:' initSize);console.log('壓縮后:' ndata.length);console.log('壓縮率:' ~~(100 * (initSize - ndata.length) / initSize) "%");tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;return ndata;}// 圖片上傳,將base64的圖片轉成二進制對象,塞進formdata上傳function upload(basestr, type, $li) {var text = window.atob(basestr.split(",")[1]);var buffer = new Uint8Array(text.length);var pecent = 0, loop = null;for (var i = 0; i < text.length; i ) {buffer[i] = text.charCodeAt(i);}var blob = getBlob([buffer], type);var xhr = new XMLHttpRequest();var formdata = getFormData();formdata.append('imagefile', blob);xhr.open('post', '/cupload');xhr.onreadystatechange = function() {if (xhr.readyState == 4 && xhr.status == 200) {var jsonData = JSON.parse(xhr.responseText);var imagedata = jsonData[0] || {};var text = imagedata.path ? '上傳成功' : '上傳失敗';console.log(text ':' imagedata.path);clearInterval(loop);//當收到該消息時上傳完畢$li.find(".progress span").animate({'width': "100%"}, pecent < 95 ? 200 : 0, function() {$(this).html(text);});if (!imagedata.path) return;$(".pic-list").append('<a href="' imagedata.path '">' imagedata.name '(' imagedata.size ')<img src="' imagedata.path '" /></a>');}};//數據發送進度,前50%展示該進度xhr.upload.addEventListener('progress', function(e) {if (loop) return;pecent = ~~(100 * e.loaded / e.total) / 2;$li.find(".progress span").css('width', pecent "%");if (pecent == 50) {mockProgress();}}, false);//數據后50%用模擬進度function mockProgress() {if (loop) return;loop = setInterval(function() {pecent ;$li.find(".progress span").css('width', pecent "%");if (pecent == 99) {clearInterval(loop);}}, 100)}xhr.send(formdata);}/*** 獲取blob對象的兼容性寫法* @param buffer* @param format* @returns {*}*/function getBlob(buffer, format) {try {return new Blob(buffer, {type: format});} catch (e) {var bb = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder);buffer.forEach(function(buf) {bb.append(buf);});return bb.getBlob(format);}}/*** 獲取formdata*/function getFormData() {var isNeedShim = ~navigator.userAgent.indexOf('Android')&& ~navigator.vendor.indexOf('Google')&& !~navigator.userAgent.indexOf('Chrome')&& navigator.userAgent.match(/AppleWebKit\/(\d )/).pop() <= 534;return isNeedShim ? new FormDataShim() : new FormData()}/*** formdata 補丁, 給不支持formdata上傳blob的android機打補丁* @constructor*/function FormDataShim() {console.warn('using formdata shim');var o = this,parts = [],boundary = Array(21).join('-') ( new Date() * (1e16 * Math.random())).toString(36),oldSend = XMLHttpRequest.prototype.send;this.append = function(name, value, filename) {parts.push('--' boundary '\r\nContent-Disposition: form-data; name="' name '"');if (value instanceof Blob) {parts.push('; filename="' (filename || 'blob') '"\r\nContent-Type: ' value.type '\r\n\r\n');parts.push(value);}else {parts.push('\r\n\r\n' value);}parts.push('\r\n');};// Override XHR send()XMLHttpRequest.prototype.send = function(val) {var fr,data,oXHR = this;if (val === o) {// Append the final boundary stringparts.push('--' boundary '--\r\n');// Create the blobdata = getBlob(parts);// Set up and read the blob into an array to be sentfr = new FileReader();fr.onload = function() {oldSend.call(oXHR, fr.result);};fr.onerror = function(err) {throw err;};fr.readAsArrayBuffer(data);// Set the multipart content type and boudarythis.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' boundary);XMLHttpRequest.prototype.send = oldSend;}else {oldSend.call(this, val);}};} </script> </body> </html> View Code貼出后臺的實現(nodejs):
/**移動端圖片壓縮上傳功能后臺*/ "use strict";var fs = require('fs'); var router = require("../router"); //var FormParser = require("./formParser"); var formidable = require('formidable'); var path = require('path');var fileSaveDir = path.join(STATIC_PATH, 'upload');router.setMap({"uindex_2": path.join(__dirname, "./index_2.html"),"cupload": cupload });function cupload(req, res) {if (!fs.existsSync(fileSaveDir)) {fs.mkdirSync(fileSaveDir)}var form = new formidable.IncomingForm();var responseData = [];form.uploadDir = fileSaveDir;form.type = true;form.keepExtensions = true;form.parse(req, function(err, fields, files){if(!err) {Object.keys(files).forEach(function(key){var file = files[key];var filename = path.basename(file.path);//每張圖片給予一分鐘保存時間setTimeout(function() {if (!fs.existsSync(file.path)) return;console.log("\x1B[33m刪除文件" filename "\x1B[0m");fs.unlinkSync(file.path);}, 60 * 1000);// 塞入響應數據中 responseData.push({type: file.type,name: filename,path: '/public/upload/' filename,size: file.size / 1024 > 1024 ? (~~(10 * file.size / 1024 / 1024)) / 10 "MB" : ~~(file.size / 1024) "KB"});});} else {console.warn(err);}res.writeHead(200);res.end(JSON.stringify(responseData));}); }轉載地址:http://www.cnblogs.com/axes/p/4603984.html
相關鏈接:http://stackoverflow.com/questions/15639070/empty-files-uploaded-in-android-native-browser/28809955#28809955
深入研究HTML5實現圖片壓縮上傳:http://www.cnblogs.com/hutuzhu/p/5265023.html
前端本地客戶端壓縮圖片,兼容IOS,Android,PC、自動按需加載文件:https://github.com/think2011/localResizeIMG
?
更多專業前端知識,請上 【猿2048】www.mk2048.com 創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎
總結
以上是生活随笔為你收集整理的移动前端—H5实现图片先压缩再上传的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HTML5效果:实现树叶飘落
- 下一篇: 微信浏览器返回刷新,监听微信浏览器返回事