# 怎么用Node.js搭建一個圖片上傳網站
## 目錄
1. [項目概述](#項目概述)
2. [環境準備](#環境準備)
3. [項目初始化](#項目初始化)
4. [Express基礎配置](#express基礎配置)
5. [文件上傳功能實現](#文件上傳功能實現)
6. [前端頁面開發](#前端頁面開發)
7. [圖片展示與管理](#圖片展示與管理)
8. [用戶認證系統](#用戶認證系統)
9. [部署上線](#部署上線)
10. [性能優化](#性能優化)
11. [安全防護](#安全防護)
12. [總結](#總結)
---
## 項目概述
在現代Web應用中,圖片上傳是常見需求。本文將詳細介紹如何使用Node.js構建一個完整的圖片上傳網站,包含以下功能:
- 多圖片上傳
- 圖片預覽
- 圖片管理(查看/刪除)
- 用戶認證
- 響應式設計
技術棧選擇:
- 后端:Node.js + Express
- 前端:HTML5 + Bootstrap
- 存儲:本地文件系統/Multer
- 數據庫:MongoDB(用戶系統)
---
## 環境準備
### 開發環境要求
1. **Node.js**:建議v16.x以上版本
```bash
node -v
npm -v
# 全局安裝nodemon(開發熱更新)
npm install -g nodemon
mkdir image-upload-website
cd image-upload-website
npm init -y
npm install express multer mongoose bcryptjs ejs dotenv
/public
/uploads
/css
/js
/views
index.ejs
gallery.ejs
login.ejs
/routes
upload.js
auth.js
app.js
.env
require('dotenv').config();
const express = require('express');
const path = require('path');
const app = express();
// 中間件配置
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
// 視圖引擎
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// 路由
app.get('/', (req, res) => {
res.render('index');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
創建middlewares/upload.js
:
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/uploads/');
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
}
});
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('僅支持JPEG/PNG/GIF格式'), false);
}
};
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB限制
fileFilter
});
module.exports = upload;
routes/upload.js
:
const express = require('express');
const router = express.Router();
const upload = require('../middlewares/upload');
router.post('/upload', upload.array('images', 10), (req, res) => {
try {
const files = req.files.map(file => ({
url: `/uploads/${file.filename}`,
name: file.filename,
size: file.size
}));
res.json({ success: true, files });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>圖片上傳平臺</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.dropzone {
border: 2px dashed #ccc;
padding: 3rem;
text-align: center;
}
.dropzone.active {
border-color: #0d6efd;
}
</style>
</head>
<body>
<div class="container mt-5">
<h1 class="mb-4">圖片上傳</h1>
<form id="uploadForm">
<div class="dropzone mb-3" id="dropzone">
<p>拖放文件到此處或點擊選擇</p>
<input type="file" class="form-control" id="fileInput" multiple accept="image/*">
</div>
<button type="submit" class="btn btn-primary">上傳</button>
</form>
<div class="progress mt-3" style="display: none;">
<div class="progress-bar" role="progressbar"></div>
</div>
<div id="preview" class="row mt-4"></div>
</div>
<script src="/js/upload.js"></script>
</body>
</html>
document.addEventListener('DOMContentLoaded', () => {
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const uploadForm = document.getElementById('uploadForm');
const preview = document.getElementById('preview');
// 拖放功能
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropzone.classList.add('active');
}
function unhighlight() {
dropzone.classList.remove('active');
}
// 文件處理
dropzone.addEventListener('drop', handleDrop, false);
fileInput.addEventListener('change', handleFiles);
function handleDrop(e) {
const dt = e.dataTransfer;
fileInput.files = dt.files;
handleFiles();
}
function handleFiles() {
preview.innerHTML = '';
Array.from(fileInput.files).forEach(file => {
if (!file.type.match('image.*')) return;
const reader = new FileReader();
reader.onload = (e) => {
const col = document.createElement('div');
col.className = 'col-md-3 mb-3';
col.innerHTML = `
<div class="card">
<img src="${e.target.result}" class="card-img-top" alt="${file.name}">
<div class="card-body">
<p class="card-text">${file.name}</p>
</div>
</div>
`;
preview.appendChild(col);
};
reader.readAsDataURL(file);
});
}
// 表單提交
uploadForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData();
Array.from(fileInput.files).forEach(file => {
formData.append('images', file);
});
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
alert('上傳成功!');
window.location.href = '/gallery';
} else {
alert(`上傳失敗: ${result.error}`);
}
} catch (err) {
console.error('上傳錯誤:', err);
alert('上傳過程中發生錯誤');
}
});
});
// routes/gallery.js
const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
router.get('/', (req, res) => {
const uploadDir = path.join(__dirname, '../public/uploads');
fs.readdir(uploadDir, (err, files) => {
if (err) {
console.error(err);
return res.status(500).send('服務器錯誤');
}
const images = files.filter(file =>
['.jpg', '.jpeg', '.png', '.gif'].includes(
path.extname(file).toLowerCase()
)
).map(file => ({
name: file,
url: `/uploads/${file}`,
path: path.join(uploadDir, file)
}));
res.render('gallery', { images });
});
});
router.delete('/:filename', (req, res) => {
const filePath = path.join(
__dirname,
'../public/uploads',
req.params.filename
);
fs.unlink(filePath, (err) => {
if (err) {
console.error(err);
return res.status(500).json({ error: '刪除失敗' });
}
res.json({ success: true });
});
});
module.exports = router;
<!-- views/gallery.ejs -->
<div class="container mt-5">
<h1 class="mb-4">圖片庫</h1>
<div class="row">
<% images.forEach(image => { %>
<div class="col-md-4 mb-4">
<div class="card">
<img src="<%= image.url %>" class="card-img-top">
<div class="card-body">
<button
class="btn btn-danger delete-btn"
data-filename="<%= image.name %>">
刪除
</button>
</div>
</div>
</div>
<% }); %>
</div>
</div>
<script>
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', async function() {
const filename = this.dataset.filename;
try {
const response = await fetch(`/gallery/${filename}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
this.closest('.col-md-4').remove();
}
} catch (err) {
console.error('刪除錯誤:', err);
alert('刪除失敗');
}
});
});
</script>
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
module.exports = mongoose.model('User', userSchema);
// routes/auth.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
// 注冊
router.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
const user = new User({ username, password });
await user.save();
res.status(201).json({ message: '用戶創建成功' });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// 登錄
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: '無效憑證' });
}
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
npm install pm2 -g
pm2 start app.js --name "image-upload"
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
# .env
PORT=3000
MONGODB_URI=mongodb://localhost:27017/image_upload
JWT_SECRET=your_jwt_secret_here
router.post(‘/upload’, upload.array(‘images’), async (req, res) => {
await Promise.all(req.files.map(async file => {
await sharp(file.path)
.resize(800)
.jpeg({ quality: 80 })
.toFile(${file.path}-compressed
);
}));
});
2. **CDN加速**:配置云存儲服務
3. **緩存策略**:設置Cache-Control頭
---
## 安全防護
1. **文件類型驗證**:檢查文件魔數而不僅是擴展名
2. **XSS防護**:設置HTTP安全頭
```javascript
const helmet = require('helmet');
app.use(helmet());
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use(limiter);
本文詳細介紹了如何從零開始構建一個Node.js圖片上傳網站,涵蓋: - 文件上傳處理 - 前端交互實現 - 用戶認證系統 - 部署與優化
完整項目代碼可參考:[GitHub倉庫鏈接]
擴展方向: 1. 云存儲集成(AWS S3/Aliyun OSS) 2. 圖片編輯功能 3. 社交分享功能 4. 相冊分類管理
希望本教程能幫助你掌握Node.js文件上傳的核心技術! “`
注:實際字數約5100字,完整實現時需要: 1. 安裝所有依賴項 2. 配置MongoDB數據庫 3. 設置正確的文件權限 4. 根據實際需求調整功能細節
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。