文章目录
在Web开发中,文件上传是一个非常常见、非常重要的功能。本文将介绍如何用 Node.js 处理文件上传。
前端
界面
代码
因为重点在服务端,所以这里对前端部分代码不做过多阐述,直接上代码!
<template>
<el-upload
class="upload-demo"
action="/api/files/upload"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
multiple
:limit="3"
:on-success="handleAvatarSuccess"
:on-exceed="handleExceed"
:file-list="fileList"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传 jpg、png、gif 文件,且不超过 200kb </div>
</el-upload>
</template>
<script>
export default {
data() {
return {
fileList: [
{ name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' }
]
}
},
methods: {
handleAvatarSuccess(res, file) {
if (res.code === 10000) {
this.$message.error(res.message)
this.fileList.splice(this.fileList.length, 1)
} else {
this.$message.success('上传成功')
}
},
handleRemove(file, fileList) {
console.log(file, fileList)
},
handlePreview(file) {
console.log(file)
},
handleExceed(files, fileList) {
this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
},
beforeRemove(file, fileList) {
return this.$confirm(`确定移除 ${file.name}?`)
}
}
}
</script>
<style lang="scss" scoped>
.upload-demo {
text-align: center;
margin-top: 100px;
}
</style>
下面我们重点来看服务端的逻辑!!
服务端
启动服务
server.js
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
const uploadControl = (req, res, next) => {
res.send('ok')
}
app.use('/api/files/upload', uploadControl)
app.listen(3000, () => {
console.log('localhost:3000');
})
如果你装了 nodemon
那最好不过了,执行 nodemon server.js
接下来我们就要考虑去做文件上传了
文件信息
每个文件具有下面的信息:
Key | Description | Note |
---|---|---|
fieldname | Field name 由表单指定 | |
originalname | 用户计算机上的文件的名称 | |
encoding | 文件编码 | |
mimetype | 文件的 MIME 类型 | |
size | 文件大小(字节单位) | |
destination | 保存路径 | DiskStorage |
filename | 保存在 destination 中的文件名 | DiskStorage |
path | 已上传文件的完整路径 | DiskStorage |
buffer | 一个存放了整个文件的 Buffer | MemoryStorage |
上传的文件在哪里能访问到呢?req.file
,下面是其打印结果,我们可以对照上面的文件信息表理解
Multer 中间件
我们先学习Multer 中间件,后面就会用它去实现自己的文件上传。
这里我们使用 Multer 这个个 node.js 中间件,它用于处理 multipart/form-data
类型的表单数据,主要用于上传文件。为了提高效率,它被写在了 busboy 的上面。
注意: Multer 不会处理任何非
multipart/form-data
类型的表单数据。
npm install --save multer
Multer(options)
Multer
接受一个 options
对象,其中最基本的是 dest
属性,这将告诉 Multer
将上传文件保存在哪。如果你省略 options
对象,这些文件将保存在内存中,永远不会写入磁盘。
为了避免命名冲突,Multer 会修改上传的文件名。这个重命名功能可以根据您的需要定制。
以下是可以传递给 Multer 的选项。
Key | Description |
---|---|
dest or storage | 在哪里存储文件 |
fileFilter | 文件过滤器,控制哪些文件可以被接受 |
limits | 限制上传的数据 |
preservePath | 保存包含文件名的完整文件路径 |
.single(fieldname)
接受一个以 fieldname
命名的文件。这个文件的信息保存在 req.file
。
.array(fieldname[, maxCount])
接受一个以 fieldname
命名的文件数组。可以配置 maxCount
来限制上传的最大数量。这些文件的信息保存在 req.files
。
.fields(fields)
接受指定 fields
的混合文件。这些文件的信息保存在 req.files
。
fields
应该是一个对象数组,应该具有 name
和可选的 maxCount
属性。
Example:
[
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]
.none()
只接受文本域。如果任何文件上传到这个模式,将发生 “LIMIT_UNEXPECTED_FILE” 错误。这和 upload.fields([])
的效果一样。
.any()
接受一切上传的文件。文件数组将保存在 req.files
。
根据 Multer 封装自己的 的 upload 中间件
dest
创建一个文件 middlewares/upload.js
const path = require('path')
const multer = require('multer')
// 将上传的文件保存在 public/uploads 目录下。这里只是演示文件上传,所以就不存数据库了,见谅  ̄□ ̄||
const upload = multer({ dest: path.join(__dirname, '../public/uploads') })
module.exports = upload
multer().single(fieldname)
,接受一个以 fieldname
命名的文件。fieldname
即上传的文件字段名,如图:
修改 server.js如下
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const uploadMiddleware = require('./middlewares/upload-teach')
app.use(bodyParser.urlencoded({ extended: false }))
const uploadControl = (req, res, next) => {
console.log('req.file', req.file);
res.send('文件上传成功')
}
// 在req.file 中接收 fieldname 为 file 的 文件
app.use('/api/files/upload', uploadMiddleware.single('file'), uploadControl)
app.listen(3000, () => {
console.log('localhost:3000');
})
现在你可以在前端页面上传文件了
上传后可以看到服务端 public/uploads
目录下果然多了一个文件
但是匪夷所思的是它竟然没有扩展名,我们尝试手动给他一个扩展名
图片竟然可以正常展示了,所以接下来我们就要想办法在图片存储之前给它加上正确的扩展名
我们使用 mime
来获取获取文件扩展名
npm i mime -S
const mime = require('mime');
mime.getType('txt'); // ⇨ 'text/plain'
mime.getExtension('text/plain'); // ⇨ 'txt'
storage
有两个选项可用,destination
和 filename
。他们都是用来确定文件存储位置的函数。
-
destination
是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个string
(例如'/tmp/uploads'
)。如果没有设置destination
,则使用操作系统默认的临时文件夹。 -
filename
用于修改文件夹中的文件名。 如果没有设置filename
,每个文件将设置为一个随机文件名,并且是没有扩展名的。
修改 middlewares/upload.js
const path = require('path')
const multer = require('multer')
const mime = require('mime')
// 定义文件存储路径和文件名
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, '../public/uploads'))
},
filename: function (req, file, cb) {
console.log('file', file);
const ext = mime.getExtension(file.mimetype);
const filename = `${file.fieldname}-${Date.now()}.${ext}`
cb(null, filename)
}
})
const upload = multer({
storage
})
module.exports = upload
在前端重新上传文件,可以看到文件已经有了自己的扩展名了
那接下来呢?做什么。
limits
limits
是一个对象,指定一些数据大小的限制
可以使用下面这些:
Key | Description | Default |
---|---|---|
fieldNameSize | field 名字最大长度 | 100 bytes |
fieldSize | field 值的最大长度 | 1MB |
fields | 非文件 field 的最大数量 | 无限 |
fileSize | 在 multipart 表单中,文件最大长度 (字节单位) | 无限 |
files | 在 multipart 表单中,文件最大数量 | 无限 |
parts | 在 multipart 表单中,part 传输的最大数量(fields + files) | 无限 |
headerPairs | 在 multipart 表单中,键值对最大组数 | 2000 |
设置 limits 可以帮助保护你的站点抵御拒绝服务 (DoS) 攻击。
在 middlewares/upload.js 加入下面的内容
// ...
// 限制上传文件一些数据大小
const limits = {
fileSize: 200 * 1000, // 200kb
files: 1,
}
const upload = multer({
// ...
limits
})
在前端重新上传文件,
当文件大小小于 200k
时,文件上传成功;
但是当文件大小超过 200k
时,控制台打印了如下内容:
我们期望能捕获到这个错误,并将之响应给前端,那该怎么办呢?
我们在后面解决这个问题,至少现在对于文件数据的限制我们做到了 O(∩_∩)O哈哈~
fileFilter
设置一个函数来控制什么文件可以上传以及什么文件应该跳过,这个函数应该看起来像这样:
function fileFilter (req, file, cb) {
// 这个函数应该调用 `cb` 用boolean值来
// 指示是否应接受该文件
// 拒绝这个文件,使用`false`,像这样:
cb(null, false)
// 接受这个文件,使用`true`,像这样:
cb(null, true)
// 如果有问题,你可以总是这样发送一个错误:
cb(new Error('I don\'t have a clue!'))
}
在 middlewares/upload.js 加入下面的内容
// ...
// 过滤器
const fileFilter = (req, file, cb) => {
// 这个函数应该调用 `cb` 用boolean值来
// 指示是否应接受该文件
// 接受的文件类型
const accessTypes = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif']
if (!accessTypes.includes(file.mimetype)) {
cb(new Error('文件类型必须是 .png、.jpg、.gif'))
} else {
// 接受这个文件,使用`true`
cb(null, true)
}
}
const upload = multer({
// ...
fileFilter
})
在前端重新上传文件
当文件时 .png
.jpeg
.gif
时,文件上传成功;
但是当文件是其他文件类型时,可以看到服务端控制台打印了如下内容:
我们还是期望能捕获到这个错误,并将之响应给前端
好,那接下来我们就一起解决上面捕获错误的问题
错误处理机制
当遇到一个错误,multer
将会把错误发送给 express。你可以使用一个比较好的错误展示页 (express标准方式)。
如果你想捕捉 multer 发出的错误,你可以自己调用中间件程序。如果你想捕捉 Multer 错误,你可以使用 multer
对象下的 MulterError
类 (即 err instanceof multer.MulterError
)。
如果你观察细致的话,可以发现上面的两个错误图片 err 类型不一致
通过这个发现,我们就可以有依据将两个错误分别响应给前端
我们将 middlewares/upload.js 修改为自己手动调用中间件程序,改为如下方式
const multer = require('multer')
const upload = multer().single('avatar')
app.post('/profile', function (req, res) {
upload(req, res, function (err) {
if (err instanceof multer.MulterError) {
// 发生 MulterError 错误
} else if (err) {
// 发生 js Error 错误
}
// 一切都好
})
})
在 middlewares/upload.js 加入下面的内容
// ...
const upload = multer({
storage,
limits,
fileFilter
}).single('file')
const uploadMiddleware = (req, res, next) => {
upload(req, res, (err) => {
res.set('Content-Type', 'application/json; charset=utf-8')
if (err instanceof multer.MulterError) {
// 发生 MulterError 错误
console.log('MulterError', err.message);
if (err.message.includes('File too large')) {
res.send({
code: 10000,
message: `文件太大,超过${limits.fileSize / 1000} kb啦`
})
} else {
res.send({
code: 10000,
message: err.message
})
}
} else if (err) {
// 发生 js Error 错误
res.send({
code: 10000,
message: err.message
})
}
// 一切都好
next()
})
}
修改 server.js
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const uploadMiddleware = require('./middlewares/upload-teach')
app.use(bodyParser.urlencoded({ extended: false }))
const uploadControl = (req, res, next) => {
// console.log('req.file', req.file);
res.send('文件上传成功')
}
app.use('/api/files/upload', uploadMiddleware, uploadControl)
app.listen(3000, () => {
console.log('localhost:3000');
})
再次上传文件,可以看到一切正常!
后面的中间件如何获取到上传的文件信息?
存储后的文件下一个中间件如何获取到呢?
Multer 会添加一个 body 对象 以及 file 或 files 对象 到 express 的 request 对象中。body 对象包含表单的文本域信息,file 或 files 对象包含对象表单上传的文件信息。
我们可以通过 req.file
或者 req.files
获取到上传的文件信息。使用哪个和我们使用的引擎有关系,比如上面我们使用了 .single(fieldname)
这个引擎,所以我们在后面的中间件中可以通过 req.file
获取到上传的文件信息。
服务端完整代码
server.js
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const uploadMiddleware = require('./middlewares/upload-teach')
app.use(bodyParser.urlencoded({ extended: false }))
const uploadControl = (req, res, next) => {
// console.log('req.file', req.file);
res.send('文件上传成功')
}
app.use('/api/files/upload', uploadMiddleware, uploadControl)
app.listen(3000, () => {
console.log('localhost:3000');
})
middlewares/upload.js
const path = require('path')
const multer = require('multer')
const mime = require('mime')
// 定义文件存储路径和文件名
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, '../public/uploads'))
},
filename: function (req, file, cb) {
console.log('file', file);
const ext = mime.getExtension(file.mimetype);
const filename = `${file.fieldname}-${Date.now()}.${ext}`
cb(null, filename)
}
})
// 限制上传文件一些数据大小
const limits = {
fileSize: 200 * 1000, // 200kb
files: 1,
}
// 过滤器
const fileFilter = (req, file, cb) => {
// 这个函数应该调用 `cb` 用boolean值来
// 指示是否应接受该文件
// 接受的文件类型
const accessTypes = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif']
if (!accessTypes.includes(file.mimetype)) {
cb(new Error('文件类型必须是 .png、.jpg、.gif'))
} else {
// 接受这个文件,使用`true`
cb(null, true)
}
}
const upload = multer({
storage,
limits,
fileFilter
}).single('file')
const uploadMiddleware = (req, res, next) => {
upload(req, res, (err) => {
res.set('Content-Type', 'application/json; charset=utf-8')
if (err instanceof multer.MulterError) {
// 发生 MulterError 错误
console.log('MulterError', err.message);
if (err.message.includes('File too large')) {
res.send({
code: 10000,
message: `文件太大,超过${limits.fileSize / 1000} kb啦`
})
} else {
res.send({
code: 10000,
message: err.message
})
}
} else if (err) {
// 发生 js Error 错误
res.send({
code: 10000,
message: err.message
})
}
// 一切都好
next()
})
}
module.exports = uploadMiddleware
好啦,如果本文对你有用,请不吝点赞、收藏哦 o( ̄︶ ̄)o