簡介
上傳是個老生常談的話題了,多數情況下各位想必用的是uplodify,webUploader之類的插件,但近期團隊定制組件的時候,筆者總覺得插件太重,許多功能用不到,那么就自己練手寫了一個demo,並且支持圖片拖拽排序。支持chrome 31及以上,IE就呵呵了。不過筆者的團隊就是不用兼容IE,所以任性。。另外,后端處理部分本篇不會詳細討論,請直接查看下面的源碼。
單圖上傳
上傳主要涉及 XMLHttpRequest Level 2的API:FormData。下面的腳本chrome 31版后才會兼容。
css部分使用了一個障眼法,將input type=file的表單項設置為opacity:0的,然后絕對定位撐滿父容器。這樣一來用戶看到的只有父容器的樣子,而點擊到的元素input type=file卻是透明的。
.photo-item, .photo-add {
position: relative;
float: left;
width: 120px;
height: 90px;
margin-bottom: 52px;
margin-right: 16px;
}
}
.item-image {
display: block;
width: 100%;
height: 100%;
}
.uploader-file {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
accept屬性表示可以選擇的文件MIME類型,多個MIME類型用英文逗號分開,常見MIME類型見這里。但是accept會使得瀏覽器調用文件選擇界面的速度變慢,大概是與瀏覽器需要篩選不同類型的文件有關,不使用accept屬性的話就不會有嚴重的延遲。
<div class="photo-add">
<img class="item-image" src="http://7xn4mw.com1.z0.glb.clouddn.com/16-9-13/13827291.jpg" alt="">
<input type="file" accept="image/*"
name="uploader-input"
class="uploader-file"
id="upload">
</div>
<div id="box"></div>
js需要監聽input的onchange事件,從而拿到file對象,塞進FormData的實例對象里,就能用ajax提交。
document.getElementById('upload').addEventListener('change', function (event) {
var $file = event.currentTarget;
var formData = new FormData();
var file = $file.files;
formData = new FormData();
formData.append(file[0].name, file[0]);
$.ajax({
url: '/upload',
type: 'POST',
dataType: 'json',
data: formData,
contentType: false,
processData: false
})
.done(data => {
$('#box').append(`<div class="photo-item">
<img class="item-image" width="100%" height="100%" src="${data.url}"/>
</div>`);
})
.fail(data => {
console.log(data);
});
});
多選上傳
注意多了一個multiple屬性,是否可以選擇多個文件,多個文件時其value值為第一個文件的虛擬路徑。
<input type="file" accept="image/*" multiple
name="uploader-input"
class="uploader-file"
id="upload">
注意需要對file對象進行遍歷,由於服務器暫時做的很簡單,只能響應單圖的上傳請求,所以需要多次發起ajax。
document.getElementById('upload').addEventListener('change', function (event) {
var $file = event.currentTarget;
var formData = new FormData();
var file = $file.files;
for (var i = 0; i < file.length; i++) {
// 文件名稱,文件對象
formData = new FormData();
formData.append(file[i].name, file[i]);
$.ajax({
url: '/upload',
type: 'POST',
dataType: 'json',
data: formData,
contentType: false,
processData: false
})
.done(data => {
$('#box').append(`<div class="photo-item">
<img class="item-image" width="100%" height="100%" src="${data.url}"/>
</div>`);
})
.fail(data => {
console.log(data);
});
}
});
drag and drop api
這里已經支持外部文件系統拖拽圖片到div.photo-add上可上傳,瀏覽器已經替我們做了處理了,為了練習drag api,於是嘗試加入上傳的圖可以拖動排序的需求,則可以使用drag and drop的api。chrome 7+以上支持。這里只使用了三個事件:
- ondragstart 當拖拽元素開始被拖拽的時候觸發的事件,此事件作用在被拖曳元素上
- ondragover 拖拽元素在目標元素上移動的時候觸發的事件,此事件作用在目標元素上
- ondrop 被拖拽的元素在目標元素上同時鼠標放開觸發的事件,此事件作用在目標元素上
......
var temp;
$('#box')
.on('dragstart', '.photo-item', function (e) {
temp = this;
})
.on('dragover', '.photo-item', function (e) {
//此事件切記要preventDefault,否則接下來將不會觸發drop事件
e.preventDefault();
})
.on('drop', '.photo-item', function (e) {
var sourceHTML = temp.innerHTML;
temp.innerHTML = this.innerHTML;
this.innerHTML = sourceHTML;
});
壓縮圖片
需要canvas的支持,原理是利用了canvas定好所要生成的寬高,並且HTMLCanvasElement.toDataURL()的第二個參數代表着清晰度。這樣就完成了裁剪和壓縮的步驟。
window.atob()表示從base64字符中解碼,window.btob()表示編碼為base64字符,chrome 4+支持。
ArrayBuffer表示二進制數據的原始緩沖區,該緩沖區用於存儲各種類型化數組的數據。 無法直接讀取或寫入ArrayBuffer,但可根據需要將其傳遞到類型化數組或 DataView 對象 來解釋原始緩沖區。
blob的api是為了讓buffer轉化為二進制文件,在chrome 20版以上就支持
js需要改寫成下面的樣子:
var uploadFn = function (formData) {
//發送到服務端
$.ajax({
url: '/upload2',
type: 'POST',
dataType: 'json',
data: formData,
contentType: false,
processData: false
})
.done(res => {
$('#box').append(`<div class="photo-item">
<img class="item-image" width="100%" height="100%" src="${res.url}"/>
</div>`);
console.log(res.path);
})
.fail(res => {
console.log(res);
});
};
var compass = function (imgObj, type, maxWidth, maxHeight, encoderOptions) {
//生成比例
if (imgObj.height > maxHeight) { //按最大高度等比縮放
imgObj.width = Math.round(imgObj.width * (maxHeight / imgObj.height));
imgObj.height = maxHeight;
}
if (imgObj.width > maxWidth) { //按最大高度等比縮放
imgObj.height = Math.round(imgObj.height * (maxWidth / imgObj.width));
imgObj.width = maxWidth;
}
//生成canvas
var $canvas = document.createElement('canvas');
var ctx = $canvas.getContext('2d');
$canvas.width = imgObj.width;
$canvas.height = imgObj.height;
ctx.drawImage(imgObj, 0, 0, $canvas.width, $canvas.height);
//canvas.toDataURL的第二個參數決定了圖片的質量
var base64 = $canvas.toDataURL(type, encoderOptions);
$canvas = null;
//window.atob()把數據從base64格式中解碼,接着壓入二進制數據的原始緩沖區,最后使用blob轉為二進制文件。
var text = window.atob(base64.split(',')[1]);
var buffer = new ArrayBuffer(text.length);
var ubuffer = new Uint8Array(buffer);
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});
}
return blob;
};
document
.getElementById('upload')
.addEventListener('change', function (event) {
var $file = event.currentTarget;
var file = $file.files;
for (var i = 0; i < file.length; i++) {
var url = window.URL.createObjectURL(file[i]);
var $img = new Image();
$img.src = url;
$img.onload = (function (sourceFile) {
return function () {
var formData = new FormData();
//png圖片不需要canvas壓縮,不然會越壓越大
if (sourceFile.type === 'image/png') {
formData.append('upload', sourceFile, sourceFile.name);
} else {
formData.append('upload',
compass(this, sourceFile.type, 1000, 800, 0.65),
sourceFile.name);
}
uploadFn(formData);
}
})(file[i])
}
});
本地預覽並壓縮
本地預覽需要依賴fileReader API。
function compressImg(imgData, file, maxHeight, maxWidth, onCompress) {
if (!imgData) return;
onCompress = onCompress || function() {};
maxHeight = maxHeight || 1000; //默認最大高度200px
maxWidth = maxWidth || 1000; //默認最大高度200px
var canvas = document.createElement('canvas');
var img = new Image();
img.onload = function() {
if (img.height > maxHeight) { //按最大高度等比縮放
img.width = Math.round(img.width * (maxHeight / img.height));
img.height = maxHeight;
}
if (img.width > maxWidth) { //按最大高度等比縮放
img.height = Math.round(img.height * (maxWidth / img.width));
img.width = maxWidth;
}
var ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.clearRect(0, 0, canvas.width, canvas.height); // canvas清屏
//重置canvans寬高 canvas.width = img.width; canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height); // 將圖像繪制到canvas上
var base64 = canvas.toDataURL(file.type, 0.65);
var text = window.atob(base64.split(',')[1]);
var buffer = new ArrayBuffer(text.length);
var ubuffer = new Uint8Array(buffer);
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(file.type);
} else {
blob = new window.Blob([buffer], {type: file.type});
}
//必須等壓縮完才讀取canvas值,否則canvas內容是黑帆布
//canvas.toDataURL的第二個參數決定了圖片的質量,筆者在此寫死0.65
onCompress(blob, file.name, file.type);
};
// 記住必須先綁定事件,才能設置src屬性,否則img沒內容可以畫到canvas
img.src = imgData;
}
document
.getElementById('upload')
.addEventListener('change', function (event) {
var $file = event.currentTarget;
var file = $file.files;
var FR;
for (var i = 0; i < file.length; i++) {
FR = new FileReader();
FR.readAsDataURL(file[i]); //先注冊onload,再讀取文件內容,否則讀取內容是空的
FR.onload = (function (targetFile) {
return function (previewObj) {
compressImg(previewObj.target.result, targetFile, 800, 1000,
function(compressData, name, type) {
var formData = new FormData();
//壓縮完成后執行的callback
formData.append('upload', compressData, name);
$.ajax({
url: '/upload2',
type: 'POST',
dataType: 'json',
data: formData,
contentType: false,
processData: false
})
.done(res => {
console.log(res.path);
})
.fail(res => {
console.log(res);
});
$('#box').append(`<div class="photo-item">
<img class="item-image" width="100%" height="100%" src="${previewObj.target.result}"/>
</div>`);
});
}
})(file[i]);
}
});
思考
上面的代碼只是演示的demo,當然有很多改進的空間,比如說:
- 上傳圖片的刪除,酷一點的當然可以將圖片拖到頁面某個區域直接就刪除。
- 上傳顯示進度百分比,這個效果需要ajax請求里加入xhr參數
服務端
服務端是一個結構分層的node處理圖片上傳,抄自node入門篇里的結構:
- /staticfile 靜態文件所在,主要是jq和uploader插件。
- /tmp 圖片存儲文件夾。
- index.js 請求控制,控制某請求調用某方法進行的。
- requestHandlers.js 請求處理,邏輯最重的地方。
- router.js 請求的路由,控制請求接受來后調用哪個請求控制組的。
- server.js 應用啟動入口。
這里略過描述,有需要的直接去下文檔下面找到github源碼地址看。
源碼
源代碼包括node版服務端,路徑:github地址,目錄如下:
- /staticfile 靜態文件所在,主要是jq和uploadify插件。
- /tmp 上傳圖片后存儲文件夾的位子。
- index.js 服務端:請求控制,控制某請求調用某方法進行的。
- requestHandlers.js 服務端:請求處理,邏輯最重的地方。
- router.js 服務端:請求的路由,控制請求接受來后調用哪個請求控制組的。
- server.js 服務端:應用啟動入口。
- uploader.html 前端頁面,演示了用uploadify插件上傳,訪問
localhost:8066/uploader.html
可以看到。 - uploader1.html 前端頁面,演示了h5多圖上傳,訪問
localhost:8066/uploader1.html
可以看到。 - uploader2.html 前端頁面,演示了圖片上傳前壓縮,訪問
localhost:8066/uploader2.html
可以看到。 - uploader3.html 前端頁面,演示了圖片壓縮並預覽上傳,訪問
localhost:8066/uploader3.html
可以看到。
找到代碼目錄后運行命令
npm install formidable
node index.js