需求
一般情况下,上传的图片(文件)都放文件夹内,获取的时候通过
主机地址+文件路径
便可获取
但是现在我不想把图片存储在服务器文件夹内,我想把前端代码、后端代码、数据分离出来
这样不管换部署环境还是管理,它们都有自己的空间,耦合性非常低
实现
环境
-
前端:JavaScript
-
后端:Nodejs(express)
-
数据库:MongoDB
数据库使用了
NoSQL
类型的MongoDB
,因为它几乎可以保存任何东西,并且以BSON
类型存储,它类似与JSON
。
这样前端、后端、甚至数据库都可以用一种标准化的数据结构。
MongDB
对文档(记录)没有严格的字段限制,也就是说,你可以在一张集合(表)里存储任何东西。
比如,下图中,第一条文档是一个json
对象,里边包含了id、md5、index、data、size这些字段,但你完全可以在下一条文档中只有id、name、age等,每条文档可以不用包含一组固定的字段。但对于一般信息,非常不建议这样做。
也得益于这个数据库支持存储二进制数据,才使得它可以存储任何东西。
但是一个文档最大只支持16M
空间,要想存储超大文件,可以使用官方自带的模块GridFS
,或者把文件分段存储。
前端上传部分
上传图片主要用到了
Form-Data
类型的请求头
<body>
<form>
<p>file: <input name="file" type="file" id="fileInput"></p>
<p><button type="button" id="submit">提交</button></p>
</form>
</body>
<script>
const formData = new FormData()
fileInput.addEventListener('change', (e) => {
const file = fileInput.files[0]
formData.append(file.name, file)
})
submit.addEventListener('click', () => {
const xhr = new XMLHttpRequest()
xhr.open('POST', '/file')
xhr.send(formData)
})
</script>
代码是我直接粘过来的
前端主要是把数据放到FormData
中,然后用Ajax
发送到服务器
后端接收部分
问题主要在这块,一般的图片或文件接收都有现成的框架,我们并不知道它是如何实现把数据接收并保存的,我找了一篇资料1,这位博主写的很好,详细原理过程可以看下边引用中的链接
- 请求路由模块
post('/upload', (req, res) => {
// 最大尺寸
const maxsize = 15000000
// 请求头分隔符
const separtor = `--${req.headers['content-type'].split('boundary=')[1]}`
// 获取一块 Buffer 空间
let data = Buffer.alloc(0)
let isMax = false
req.on('data', (chunk) => {
// 把每次上传的数据包拼接到 data
data = Buffer.concat([data, chunk])
// 判断尺寸
if (data.length >= maxsize) {
res.status(400).json({ result: false })
isMax = true
return
}
})
if (isMax) return
// 上传完成操作
req.on('end', async () => {
// 这里用到了解析请求头的函数,具体实现看下一个代码片段
data = modules.parseFormData(data, separtor)
// 选择集合(表),集合名为 pic
// 其中 db 为连接数据库后的实体,不在这里累赘
let picc = db.collection('pic')
// 这里的 data 实际上是一个数组,包含请求头中的所有数据块
// 如果我们只传图片的话,解析请求头后,data 就只有一个元素
// 类似与
// data {
// name: ...
// filename: ...
// body: 二进制 Buffer (如果是图片的话就是二进制Buffer,否则是普通字符串)
// }
// 此时 data 里包含两个元素,一个数据体信息对象,一个数据体
// 具体请移步至下方引用中的链接
// 这里插入时多加了一条 body 的 md5,用来索引
let result = await picc.insertOne({data: data, md5: md5(data.body)})
res.json(result)
res.end()
})
})
- 解析请求头模块
{
parseFormData: function (data, separator) {
if (!Buffer.isBuffer(data))
return false
let dataArr = []
const bufArr = this.split(data, separator).slice(1, -1)
for (let d of bufArr) {
let headerVal = {}
const [head, body] = this.split(d, '\r\n\r\n')
const headArr = this.split(head, '\r\n').slice(1)
const [name, value] = headArr[0].toString().split(': ')
value.split('; ').forEach(item => {
const [key, val = ''] = item.split('=')
headerVal[key] = val && JSON.parse(val)
})
dataArr.push({ ...headerVal, body })
}
return dataArr
},
split: function (data, separator) {
const dataArr = []
let offset = 0
let index = data.indexOf(separator, 0)
while (index != -1) {
dataArr.push(data.slice(offset, index))
offset = index + separator.length
index = data.indexOf(separator, index + separator.length)
}
dataArr.push(data.slice(offset))
return dataArr
}
}
解析请求头模块
内的代码不做解释,可直接使用。它主要是把请求头分解成数组,每一个内容包含信息头对象,以及信息体。
存储过程
在后端接收那块,先把请求头内的内容放在数组内,数组里包含每一条请求对象
每一条请求对象又包含信息,以及信息体
信息体使用Nodejs
中的Buffer
存储
Buffer
是存储二进制数据的
然后打包插入到MongoDB
信息就直接用BSON
存储了,Buffer
信息体就以二进制存储在BSON
内了
获取过程
getPic: async function (md5) {
let result
// 同样,db是数据库实例
let picdc = db.collection('pic')
// 通过 md5 码找到数据
result = await picdc.findOne({ md5: md5 })
// 如果找到了,结果应该是这样
// {
// data {
// name: ...
// filename: ...
// body: 二进制 Buffer (如果是图片的话就是二进制Buffer,否则是普通字符串)
// },
// md5: ...
// }
return result ? result : false
}
- 这里是发送到客户端
.get('/:md5', async (req, res) => {
let picinfo = await mdbctl.getPic(req.params.md5)
// 需要声明回应头,为 image 类型,png 格式
res.writeHead(200, { 'Content-Type': 'image/png' })
// 这里直接发送返回的二进制数据
res.write(picinfo.data.body)
res.end()
})
对于向数据库插入及获取,我只是大概写了一下,用于理解
我的源代码对他们进行了模块化和标准化,牵扯的太多了,就没有贴源代码
思考
- 对于直接存放到磁盘,和存放到数据库,他们之间的性能及内存占用不知道孰优孰劣
直接放到磁盘,服务器发送数据时也应该会把数据放到内存,
而数据库方式是从数据库获取数据,也是放到内存,然后发送
应该性能差不多