1 文章起源
本文給大家分享一個(gè)關(guān)于Node+Mongodb
的附件上傳下載的項(xiàng)目,這個(gè)項(xiàng)目比較簡(jiǎn)單,看完的同學(xué)就可以很快的搭建一個(gè)小網(wǎng)盤(pán)或者圖床了。
2 起手式
2.1 概念
首先我們得先去了解一下Mongodb
的文件儲(chǔ)存(GridFS)是啥,因?yàn)槲覀兌际腔?GridFS
來(lái)進(jìn)行文件儲(chǔ)存
2.2 我們需要什么
了解了大概概念后就可以著手安裝我們必須的插件了
express
(這是啥不用我多說(shuō))body-parser
(Node解析body的中間件)ejs
(模板引擎,快速開(kāi)發(fā)就不搞前后端分離了,有興趣的小伙伴可以用Vue/React來(lái)搭建小網(wǎng)盤(pán))gridfs-stream
(輕松地與MongoDB GridFS之間傳輸文件。)method-override
(我們用form表單簡(jiǎn)單上傳,因?yàn)閒orm表單不支持put/delete請(qǐng)求方式,所以把它安排上了,小伙伴可自行使用Ajax,就不需要這么麻煩了)mongoose
(用于連接mongodb必不可少的插件)multer
(Multer是用于處理多部分/表單數(shù)據(jù)的node.js中間件,主要用于上傳文件。它被編寫(xiě)在busboy之上,以實(shí)現(xiàn)最大效率。)multer-gridfs-storage
(Multer的GridFS存儲(chǔ)引擎可將上傳的文件直接存儲(chǔ)到MongoDb。)nodemon
(熱更新)
(推薦教程:Node入門(mén))
以上就是我們需要準(zhǔn)備的東西了
npm install express body-parser ejs gridfs-stream method-override mongoose multer multer-gridfs-storage // or yarn add express body-parser ejs gridfs-stream method-override mongoose multer multer-gridfs-storage
2.3 初始化一個(gè)項(xiàng)目
// 可自行補(bǔ)充信息 // npm init
然后在根目錄新建一個(gè)入口文件app.js
,和頁(yè)面 views/index.ejs
3 現(xiàn)在項(xiàng)目開(kāi)始了
3.1 先將基礎(chǔ)部分完事
將我們安裝的包引入,再跑跑看看
const express = require('express') const path = require('path') const crypto = require('crypto') const mongoose = require('mongoose') const multer = require('multer') const GridFsStorage = require('multer-gridfs-storage') const GridFsStream = require('gridfs-stream') const methodOverride = require('method-override') const bodyParser = require('body-parser')
const app = express()
app.set('view engine', 'ejs') // 設(shè)置模板引擎
app.use(bodyParser.json())
app.use(methodOverride('_method'))
app.get('/', (req, res) => {
res.render('index')
})
})
const port = 5000
app.listen(port, () => {
console.log(`App listering on port ${port}`)
})
一般來(lái)說(shuō)啟動(dòng)了app.js
的話我們?cè)跒g覽器訪問(wèn) http://localhost:5000
就能看到 views/index.ejs
中的界面了,如果沒(méi)有,自行查看控制臺(tái)是否報(bào)錯(cuò)
3.2 連接我們的Mongodb數(shù)據(jù)庫(kù)
我這邊用的本地mongodb
數(shù)據(jù)庫(kù),線上也是一樣的,我們可以用NoSQL manager for mongdb
來(lái)查看我們數(shù)據(jù)庫(kù)里面的數(shù)據(jù),我們新建一個(gè)新的集合,我這邊叫 grid_uploads
。所以連接的話也是連接這個(gè)集合
// 數(shù)據(jù)庫(kù)的鏈接 const mongoURL = 'mongodb://localhost:27017/grid_uploads'
const connect = mongoose.createConnection(mongoURL, {
useNewUrlParser: true,
useUnifiedTopology: true
})
可以嘗試在NoSQL
寫(xiě)入一些數(shù)據(jù),具體使用可以參考博客【MongoDB】NoSQL Manager for MongoDB
教程
3.3 美化一下界面(views/index.ejs)
作為一個(gè)小兩年的前端工程師,已經(jīng)練就了像素眼了,我們肯定不能把界面做的辣么丑對(duì)吧,眼睛過(guò)不去啊,所以我們簡(jiǎn)單的用bootstrap4
來(lái)做個(gè)界面好了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上傳</title>
<link rel="stylesheet" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<style>
img {
width: 100%;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 m-auto">
<h2 class="text-center display-4 my-4">Mongo文件上傳</h2>
<form action="/upload" method="POST" enctype="multipart/form-data">
<div class="custom-file mb-3">
<input type="file" name="file" id="file" class="custom-file-input">
<label for="file" class="custom-file-label">選擇文件</label>
</div>
<input class="btn btn-primary btn-block" type="submit" value="提交">
</form>
<hr>
</div>
</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
</html>
那么我們請(qǐng)求 http://localhost:5000
的話我們看到的應(yīng)該是這樣子的
(推薦教程:Node.js教程)
3.4 做一些必須的處理
// 定義gfs變量,后續(xù)我們進(jìn)行數(shù)據(jù)庫(kù)文件操作的時(shí)候可不能少 let gfs; connect.once('open', () => { // 監(jiān)聽(tīng)數(shù)據(jù)庫(kù)開(kāi)啟,通過(guò) gridfs-stream 中間件和數(shù)據(jù)庫(kù)進(jìn)行文件的出入控制 gfs = GridFsStream(connect.db, mongoose.mongo) gfs.collection('upload') // 它會(huì)在我們數(shù)據(jù)庫(kù)中建立 upload.files(記錄文件信息) upload.chunks(存儲(chǔ)文件塊) })
// 使用 multer-gridfs-storage Multer 中間件來(lái)講我們上傳的附件直接存儲(chǔ)到MongoDb
const storage = new GridFsStorage({
url: mongoURL,
file: (req, file) => {
return new Promise((resolve, reject) => {
// 下面注釋部分是給文件進(jìn)行重命名的,如果想要原文件名稱可以自行使用 file.originalname 返回,
// 建議有時(shí)間的小伙伴存儲(chǔ)兩個(gè)文檔,一個(gè)記錄原文件名,一個(gè)記錄加密文件名,然后返回到頁(yè)面的時(shí)候可以將中文名返回去
// crypto.randomBytes(16, (err, buf) => {
// if (err) {
// return reject(err)
// }
// const filename = buf.toString('hex') + path.extname(file.originalname)
// const fileinfo = {
// filename,
// bucketName: 'upload'
// }
// resolve(fileinfo)
// })
const fileinfo = {
filename: new Date() + '-' + file.originalname,
bucketName: 'upload'
}
resolve(fileinfo)
})
}
})
const upload = multer({ storage })
3.5 寫(xiě)我們上傳第一個(gè)文件的接口
app.post('/upload', upload.single('file'), (req, res) => { res.redirect('/') })
看起來(lái)簡(jiǎn)簡(jiǎn)單單,請(qǐng)記著這么幾件事
- 在
views/index.ejs
中 (input type=file
指定的name
得和接口的upload.single
('file') 一樣 - 上傳完文件我們重定向回我們的首頁(yè) 此時(shí)我們就可以在
NoSql
看到我們的兩個(gè)文檔有數(shù)據(jù)了
(推薦微課:Node.js微課)
這是upload.chunks
這是upload.files
3.6 獲取我們所有的文件信息
獲取我們所有的文件
app.get('/files', (req, res) => { // 通過(guò)查找返回一個(gè)數(shù)組對(duì)象回去 gfs.files.find().toArray((err, files) => { if (!files || files.length === 0) { return res.status(404).json({ err: '文件不存在!' }) } return res.json(files) }) })
我們可以進(jìn)行一些美化操作,比如我們可以將上傳是圖片的,返回到界面的話以圖片顯示,其他則以 a
標(biāo)簽的格式顯示(可點(diǎn)擊下載),所以我們可以將 views/index.ejs
的界面進(jìn)行美化改造(ejs
語(yǔ)法用起來(lái)確實(shí)蠻麻煩的),進(jìn)行重新排版以及添加刪除按鈕
<!DOCTYPE html> <html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上傳</title>
<link rel="stylesheet"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<style>
img {
width: 100%;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 m-auto">
<h2 class="text-center display-4 my-4">Mongo文件上傳</h2>
<form action="/upload" method="POST" enctype="multipart/form-data">
<div class="custom-file mb-3">
<input type="file" name="file" id="file" class="custom-file-input">
<label for="file" class="custom-file-label">選擇文件</label>
</div>
<input class="btn btn-primary btn-block" type="submit" value="提交">
</form>
<hr>
</div>
</div>
<div class="row">
<% if(files){ %>
<% files.forEach(function(file){ %>
<div class="col-sm card card-body m-3 col-md-2">
<% if(file.isImage){ %>
<img src="image/<%= file.filename %>" />
<% } else { %>
<a href="download/<%= file.filename %>"><%= file.filename %></a>
<%}%>
<form action="/files/<%= file._id%>?_method=DELETE" method="POST">
<button class="btn btn-danger btn-block mt-4">刪除</button>
</form>
</div>
<% }) %>
<% }else { %>
<p class="card card-body text-center display-4 my-4">文件不存在</p>
<% } %>
</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/js/bootstrap.min.js"
integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI"
crossorigin="anonymous"></script>
</html>
(推薦教程:MongoDB教程)
要將 ejs
中的files
變量獲取到我們應(yīng)該重寫(xiě)一下 get('/')
接口,使其在訪問(wèn)localhost:5000
的時(shí)候先去讀取一下數(shù)據(jù)庫(kù)文件信息并輸出到頁(yè)面中去
app.get('/', (req, res) => { gfs.files.find().toArray((err, files) => { if (!files || files.length === 0) { res.render('index', { files: false }) return } files.map(file => { // 如果是以下圖片類型我們就在前端展示出來(lái),其余一律按附件處理,通過(guò) isImage 來(lái)區(qū)分圖片和非圖片 const imageType = ['image/png', 'image/jpg', 'image/gif', 'image/jpeg'] if (imageType.includes(file.contentType)) { file.isImage = true } else { file.isImage = false } }) res.render('index', { files: files }) }) })
完成上述的情況我們?cè)L問(wèn)首頁(yè)的話就行該是如下情況
3.7 單個(gè)文件下載
在這里我們通過(guò)a標(biāo)簽訪問(wèn) /download/:filename
接口,filename
是文件名,當(dāng)然可以用其他的比如_id
,當(dāng)查找到有該附件的時(shí)候就將它合并成可讀留,通過(guò)管道返回,這樣在前端界面上點(diǎn)擊文件標(biāo)題就可以直接下載了
app.get('/download/:filename', (req, res) => { gfs.files.findOne({ filename: req.params.filename }, (err, file) => { if (!file) { return res.status(404).json({ err: '文件不存在!' }) } const readstream = gfs.createReadStream(file.filename) readstream.pipe(res) }) })
3.8 單個(gè)文件刪除
在這里我們通過(guò)a
標(biāo)簽訪問(wèn) /files/:id
接口,id
對(duì)應(yīng),點(diǎn)擊刪除按鈕,就直接刪除了,并重定向到首頁(yè)
app.delete('/files/:id', (req, res) => { gfs.remove({ _id: req.params.id, root: 'upload' }, (err) => { if (err) { return res.status(404).json({ err: '刪除的文件不存在!' }) } res.redirect('/') }) })
由于我們一直用form
做請(qǐng)求,但是form
表單沒(méi)有delete
請(qǐng)求方式,所以我們用到了method-override
插件,當(dāng)然要是用Ajax
就沒(méi)關(guān)系了,我們項(xiàng)目畢竟速成嘛,主要看效果和過(guò)程。
(推薦微課:MongoDB入門(mén)與案例分析)
4 完結(jié)撒花了
簡(jiǎn)簡(jiǎn)單單的告一段落了,學(xué)了的小伙伴們可以嘗試更加深入的操作,所以我們就可以用此項(xiàng)目來(lái)做一個(gè)圖床或者小網(wǎng)盤(pán)盤(pán),還是簡(jiǎn)簡(jiǎn)單單滴。當(dāng)然該附件上傳也有一定的限制問(wèn)題,比如大文件可能上傳時(shí)間更久,我們就需要采用文件分片方式上傳了。
以上就是個(gè)關(guān)于如何使用用Node+MongoDB
搭建簡(jiǎn)單的圖床或者網(wǎng)盤(pán)的相關(guān)介紹了,希望對(duì)大家有所幫助。