Nodejs 图片上传及回显

写在前面

这是一个比较简单的全栈Demo,写这个的动机主要是:当我查询网上一些nodejs资料时,很少有完整的传输文件的案例,大多是通过前端form 的action属性直接上传,所以萌生了写通过FromData上传图片的案例。功能实现之后做了一些前端展示。算是个比较不错的入门全栈项目。

##版本依赖

  • “express”: “^4.16.2”,
  • “file-type”: “^7.3.0”,
  • “formidable”: “^1.1.1”,
  • “multer”: “^1.3.0”,(可选)
  • “read-chunk”: “^2.1.0”

这里的 read-chunk和filetype是为了读取文件后缀名,如果不想用库。

可以通过 split('.')[1] 拿到后缀名是一样的(好像用库代码还多了,捂脸(之前笔误写成了splice)splice是用于分割字符串,抱歉抱歉

直接在更目录下 npm installnode app.js就可以运行了。

前后端构架

前端是基于boostrap 的一个简单的表单上传和展示,并没有特别复杂的组件,这里不再阐述了。

###后端

  1. 先引入express,把服务跑起来
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const express=require('express');
    const path = require('path');

    const app = express()
    app.set('port',(process.env.PORT || 3080));

    app.use(express.static('public'));
    app.use('/uploads',express.static('uploads'));

    app.get('/',function(req,res){
    res.sendFile(path.join(__dirname,'views/index.html'));
    });

    app.listen(app.get('port'),function(){
    console.log('Express started at port ' + app.get('port'));
    })

这样就把之前放在views下的index.html引入进来了,定义的端口是3080。这样就可以访问了。
app.use是挂载中间件到特定的路径。path.join是解析相对路径,类似的path.resolve是绝对路径。

2.前端通过Ajax formData到后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$('#upload-photos').on('submit',function(){
event.preventDefault();
var formData=new FormData();
var files=$('#photos-input').get(0).files;

if(files.length===0){
alert('select at least One photo');
return false
}
if(files.length>3){
alert('You can upload no more than three photos');
return false
}
for(let i=0; i<files.length;i++){
formData.append('photo[]',files[i],files[i].name);
}

uploadFiles(formData);
})

以上代码可以拿到前端传过来的files,并通过new FormData()构建一个新的实例,用append方法传入文件数据。
event.preventDefault():
When the form is submitted, the default form submission action is prevented by calling the event.preventDefault() method, which prevents the form submission being sent to the server.
作用是阻止默认的表单上传。

可以在MDN看到append方法的文档。

拿到数据之后就可以通过如下代码post数据到nodejs了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function uploadFiles(formData) {
$.ajax({
url:'/upload_photos',
method:'post',
data:formData,
processData: false,
contentType: false,
xhr: function(){
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress',function (oEvent) {
var progressBar = $('.progress-bar')
if(oEvent.lengthComputable){
var percent = (oEvent.loaded / oEvent.total) * 100;
progressBar.width(percent + '%');
}
if(percent===100){
progressBar.removeClass('active');
}
})
return xhr
}
}).done(handleSuccess).fail(function(xhr,status){
alert(status)
})
}

值得注意的是,这里的processData,contentType必须设置为false
xhr即XMLHttpRequest 可以在不刷新页面的情况下 更新数据,这里用来更新下面的进度条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
app.post('/upload_photos',function(req,res){
var photos=[];
var form = new formidable.IncomingForm();
form.multiples = true;
//临时目录
form.uploadDir = path.join(__dirname,'tmp_uploads');
form.parse(req)
.on('file',function(name,file){
if(photos.length === 3){
fs.unlink(file.path, function(err){
if(err){console.log(err)}
});
return true
}

var type = null;
var filename='';
var buffer = readChunk.sync(file.path, 0, 262);
type = fileType(buffer); //=> {ext: 'png', mime: 'image/png'}
if (type!==null && (type.ext === 'png' || type.ext === 'jpg' || type.ext === 'jpeg')){
//改名 避免重名
filename = Date.now() + '-' + file.name;
fs.rename(file.path, path.join(__dirname,'uploads/'+ filename),function(err){
if(err){console.log(err)}
})
photos.push({
status:true,
filename:filename,
type:type.ext,
publicPath:'uploads/' + filename
})
}else{
photos.push({
status:false,
filename:file.name,
message: 'Invalid file type'
})
fs.unlink(file.path, (err)=>{
if(err){console.log(err)}
})
}
//console.log('photos is',photos)
})
.on('error', (err)=> {
console.log('Error occurred during processing - ' + err);
})
.on('end',()=>{
console.log('All the request fields have been processed.');
res.status(200).json(photos);
})
})

前端Post的数据通过app.post进行接收。这里就到了formidable的用武之地了。

这里使用了链式调用,让formfidable响应(’file’ ‘error’ ‘end’)最后处理完成之后通过res.status.json()把所需要的数据传回后端,
一次完成的交互就差不多大功告成了!

删除过期文件

等上面都跑通之后,再来加点功能,拓展下get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
app.get('/',function(req,res){
var filesPath = path.join(__dirname,'uploads/');
//read all the files of the uploads
fs.readdir(filesPath,function(err,files){
if(err){
console.log(err);
return
}
console.log('files:',files);
files.forEach((file)=>{
fs.stat(filesPath+file,(err,stats)=>{
if (err) {
console.log(err);
return;
}
var createAt = stats.ctime;
var days = Math.round((Date.now()-createAt)/(1000*60*60*24));
if (days > 1){
fs.unlink(filesPath+file,(err)=>{
console.log(err)
});
}
})
})
})

res.sendFile(path.join(__dirname,'views/index.html'));
});

这里用了 fs 的 readdir方法来读取文件夹下所有的文件 stat 读取文件的信息。这里ctime就是时间了。自定义一个最长时间,就可以用unlink方法删除了!

##后记
附上github地址
写到这里大概整体框架也就说完了,第一次写完整的技术博客压力山大。(技术很菜,还希望不要误导了别人。
有一些细节问题,知识点,在这里不方便阐述,我会单开页面来说,感谢各位的阅读!