基于原生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