基于原生JS,主要功能为HTML5本地预览+ajax多图片资源提交,服务端采用nodejs简易搭建.
关键知识点:FileList File FormData URL.createObjectURL
预览效果图

前端部分源码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>多图片预览上传DEMO</title>
<style>
*{margin: 0;padding: 0;}
#previewBox{
display: flex;
flex-wrap: wrap;
width: 438px;
border:1px solid #ccc;
margin: 10px;
}
#previewBox .item{
width:134px;height: 134px;margin: 5px;border:1px solid #ccc; position: relative;
display: flex;
align-items :center;
justify-content: center;
}
#previewBox .item img{width: 130px;height: 130px;}
#btnAdd{
text-decoration: none;
background: url(add.png) no-repeat center center;
position: relative;
}
#btnAdd.off{display: none;}
#btnAdd:hover{
background-color:#ccc;
}
#file{width: 0;height: 0; position: absolute;top:-100px;left: -100px;opacity: 0;}
.console{
margin: 20px;;
}
#consoleBox{
border: 1px solid #ccc;font-size: 12px;
height: 300px;margin-top:10px;
padding:8px;
overflow: hidden;
overflow-y: auto;
}
#consoleBox>div{margin-top:10px;word-break: break-all;}
#consoleBox>div img{width: 50px;height: 50px;}
#footer{margin: 10px;}
</style>
</head>
<body>
<div id="previewBox">
<a href="javascript:;" id="btnAdd" class="item" title="添加图片"><input id="file" type="file" accept="image/*" /></a>
</div>
<div class="console">
<a href="javascript:;" id="btnSubmit">提交到服务端</a>
<div id="consoleBox"></div>
</div>
<div id="footer">
<p>此demo基于原生JS,主要功能为HTML5本地预览+ajax多图片资源提交,服务端采用nodejs简易搭建.</p>
<p>服务端功能仅为测试上传,未做文件合法校验,大小校验,数量限制等判断,请勿直接拷贝到您的项目代码中.</p>
<p>上传的文件将保存在src\res\upload目录中.</p>
<p> </p>
<h5>测试项目创建</h5>
<p>npm install</p>
<h5>测试项目运行</h5>
<p>node ./src/main.js</p>
<h5>测试项目访问</h5>
<p>http://127.0.0.1:3456/</p>
<p> </p>
<p><a href="https://www.zhaixiaowai.com">@zhaixiaowai</a></p>
</div>
<script>
let btnAdd=document.querySelector("#btnAdd");
let file = document.querySelector("#file");
let consoleBox = document.querySelector("#consoleBox");
/**
* 用户选择的文件数组
*/
let useFiles = [];
/**
*显隐添加图片按钮
*/
function setAddVisible(){
btnAdd.classList[useFiles.length>=9?'add':'remove']("off");
}
/**
* 日志输出
*/
function consoleLog(value){
const div = document.createElement("div");
const now = new Date();
div.innerHTML = `[${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}] ${ value}`;
consoleBox.insertBefore(div,consoleBox.firstChild);
}
/**清理数据*/
function clear(){
useFiles.length=0;
let previews=document.querySelectorAll("#previewBox>.item");
for (let index = 0; index < previews.length; index++) {
const element = previews[index];
if(element!==btnAdd){
element.parentNode.removeChild(element);
}
}
}
/**
* 加载File资源
* @param {File} file
* @returns {Promise<{src:"blob:""",width:10,height:0}>} Promise
*/
function loadFileImg(file){
return new Promise((reject,resolve)=>{
if(file==null){
resolve("文件异常,file==null");
}
if(file.size>=1024*500){
resolve("文件超过500kb");
return;
}
let img = new Image();
img.onload=function(e){
img=null;
let target= e.target;
reject({
width:target.naturalWidth,
height:target.naturalHeight,
src:target.src
});
};
img.onerror = function(e){
img=null;
resolve("图片加载失败");
}
img.src=URL.createObjectURL(file);
});
}
//文件框change事件响应,检测图片资源
file.addEventListener("change",function(e){
let files = e.target.files;
if(files.length===0)return;
let file=files[0];
//重置隐藏input信息
e.target.value = "";
loadFileImg(file).then(info=>{
//添加dom预览元素
let item = document.createElement("div");
item.className = "item";
item.file = file;
let preview = document.createElement("img");
preview.src = info.src;
item.appendChild(preview);
//创建删除按钮
let btnRemove = document.createElement("span");
btnRemove.addEventListener("click",function(e){
let item = e.target.parentNode;
item.parentNode.removeChild(item);
let file=item.file;
if(file){
let index = useFiles.indexOf(file);
if(index!=-1){
useFiles.splice(index,1);
}
}
setAddVisible();
});
//添加到容器
let previewBox=document.querySelector("#previewBox");
if(previewBox){
previewBox.insertBefore(item,btnAdd);
}
//添加到数组
useFiles.push(file);
//显隐添加按钮
setAddVisible();
}).catch(err=>{
consoleLog(err);
});
});
//添加图片事件注册
if(btnAdd){
btnAdd.addEventListener("click",function(e){
if(useFiles.length>=9){
consoleLog("最多只能添加9张图片");
return;
}
//触发隐藏的input[type=file]事件
if(file){
file.click();
}
});
}
//提交事件注册
let btnSubmit=document.querySelector("#btnSubmit");
if(btnSubmit){
btnSubmit.addEventListener("click",function(e){
//提交资源
if(useFiles.length===0){
consoleLog("请添加图片");
return;
}
let formData = new FormData();
//遍历添加图片资源
useFiles.forEach((item,index)=>{
formData.append("img" + index,item,item.name);
});
//添加其他非图片字段信息
formData.append("value","其他信息字段");
formData.append("time",Date.now());
window.formData=formData;
//ajax
fetch("./upload",{body:formData,method:"post"}).then(res=>{
if(res.status!==200){
consoleLog("提交失败:status=" + res.status);
return;
}
res.json().then(json=>{
let html = `提交完毕,服务端返回:${JSON.stringify(json)}<div>`
if(json && json.files && json.files.length>0){
json.files.forEach(item=>{
html+=`<a href="${item.filePath}" title="点击展开大图" target="_blank"><img src="${item.filePath}" title="name:${item.fileOriginName}\nsize:${item.size}" /></a>`;
});
}
html+='</div>'
//清理历史信息
clear();
consoleLog(html);
}).catch(ex=>{
consoleLog("解析返回值失败:" + ex.toString());
});
}).catch(ex=>{
consoleLog("提交失败:" + ex.toString());
});
});
}
</script>
</body>
</html>nodejs简易upload服务器源码
const http = require('http');
const fs = require("fs");
const url = require("url");
const formdata = require('formidable');
const server = http.createServer();
const HTTP_PORT = 3456;
/**
*
* @param {http.IncomingMessage} req request
* @param {http.ServerResponse} res response
* @param {string} url_path url路径部分
* @param {string} url_ext url扩展名
*/
const onGet= function(req,res,url_path,url_ext){
let contentType;
switch(url_ext){
case "html":{
contentType = "text/html";
break;
}
case "js":{
contentType = "text/javascript";
break;
}
case "css":{
contentType = "text/css";
break;
}
case "png":case "jpg":case "jpeg":case "gif":case "icon":{
contentType = `image/${url_ext}`;
break;
}
default:{
return false;
}
}
const filepath=`${__dirname}\\res${url_path}`;
fs.exists(filepath,exists=>{
if(exists){
var file = fs.createReadStream(filepath);
res.writeHead(200, {'Content-Type' : contentType});
file.pipe(res);
}else{
output404(req,res,contentType);
}
});
return true;
}
/**
*
* @param {http.IncomingMessage} req request
* @param {http.ServerResponse} res response
* @param {string} url_path url路径部分
* @param {string} url_ext url扩展名
*/
const onPost= function(req,res,url_path,url_ext){
switch(url_path){
case "/upload":{
//简易提交,此处没有校验文件合法性及大小,仅作上传测试
const reqData = {
data:{},
files:[]
};
let foldpath = `/upload/`;
let form = new formdata.IncomingForm();
form.uploadDir = `${__dirname}/res${foldpath}`;
form.keepExtensions=true;
//创建目录
if(!fs.existsSync(form.uploadDir)){
fs.mkdirSync(form.uploadDir);
}
//捕获键值数据
form.on("field",(name,value)=>{
reqData.data[name]=value;
})
.on("file",(name,file)=>{
let filepath = file.path;
let m =filepath.match(/[\\\/]([^\\\/]+)$/);
if(m===null){ //异常信息
return;
}else{
filepath = m[1];
}
filepath = foldpath + filepath;
reqData.files.push({
//文件大小
size:file.size,
//客户端提交的文件名
fileOriginName:file.name,
//客户端提交的键值对的键
name:name,
//文件存储路径
filePath:filepath
});
}).on("end",()=>{
//输出到接口
res.write(JSON.stringify(reqData));
res.end();
});
form.parse(req);
return true;
}
}
}
/**
*
* @param {http.IncomingMessage} req
* @param {http.ServerResponse} res
*/
const output404 = (req,res,contentType)=>{
if(!contentType){
contentType = "text/plain";
}
res.setHeader("content-type",contentType);
res.statusCode = 404;
res.write('404,url=');
res.write(req.url);
res.end();
};
server.on('request',
(req,res)=>{
let url_path = url.parse(req.url).pathname;
if(url_path[url_path.length-1]==="/"){
url_path += "index.html";
}
let extindex = url_path.lastIndexOf(".");
let ext;
if(extindex===-1){
ext = "";
}else{
ext = url_path.substr(extindex+1).toLowerCase();
}
switch(req.method){
case "GET":{
if(onGet(req,res,url_path,ext))return;
}
case "POST":{
if(onPost(req,res,url_path,ext))return;
}
}
output404(req,res);
});
server.listen(HTTP_PORT,()=>{
console.log(`服务端已开启,访问地址 http://127.0.0.1:${HTTP_PORT}/ `);
});demo下载github地址:https://github.com/zhaixiaowai/multi-image-preview-and-upload

