基于Node.js的迷你图书馆网站借还功能故障排查求助
我基于Node.js开发了一个带借还POST接口的迷你图书馆网站,首页需求如下:
- 左侧用缩进式复选框表单展示可借书籍标题,搭配Borrow按钮
- 右侧用缩进式复选框表单展示已借书籍标题,搭配Return按钮
- 勾选书籍点击按钮后,书籍状态更新:借走的书籍从左侧消失并出现在右侧,归还的书籍反之
目前已完成对应的.hbs模板文件、/home页面的GET方法,以及/borrow、/return功能的POST方法,但借还功能未正常工作:点击按钮后页面仅刷新,无法更新书籍的available属性。
相关代码
1. Handlebars模板(Home.hbs)
{{> Heading}} <div class="container"> <div class="left-container"> <h2>Available Books</h2> <form action="/borrow" method="POST"> {{#each availableBooks}} {{#if available}} <div class="book"> <input type="checkbox" name="books[]" value="{{title}}"> <span>{{title}}</span> </div> {{/if}} {{/each}} <button type="submit" name="action" value="borrow" id="borrowButton" disabled>Borrow</button> </form> </div> <div class="right-container"> <h2>Borrowed Books</h2> <form action="/return" method="POST"> {{#each borrowedBooks}} {{#unless available}} <div class="book"> <input type="checkbox" name="books[]" value="{{title}}"> <span>{{title}}</span> </div> {{/unless}} {{/each}} <button type="submit" name="action" value="return" id="returnButton" disabled>Return</button> </form> </div> </div> <script> // Enable borrow button when at least one checkbox is selected in the left container const borrowButton = document.getElementById('borrowButton'); const leftContainerCheckboxes = document.querySelectorAll('.left-container input[type="checkbox"]'); leftContainerCheckboxes.forEach((checkbox) => { checkbox.addEventListener('change', () => { const anyChecked = Array.from(leftContainerCheckboxes).some((checkbox) => checkbox.checked); borrowButton.disabled = !anyChecked; }); }); // Enable return button when at least one checkbox is selected in the right container const returnButton = document.getElementById('returnButton'); const rightContainerCheckboxes = document.querySelectorAll('.right-container input[type="checkbox"]'); rightContainerCheckboxes.forEach((checkbox) => { checkbox.addEventListener('change', () => { returnButton.disabled = !Array.from(rightContainerCheckboxes).some((checkbox) => checkbox.checked); }); }); </script>
2. Node.js后端代码
const HTTP_PORT = process.env.PORT || 3000; const express = require("express"); const exphbs = require("express-handlebars"); const path = require('path'); const fs = require('fs'); const session = require('express-session'); const bodyParser = require('body-parser'); const app = express(); app.use(express.static(path.join(__dirname, '/public'))); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: false })); function onHttpStart() { console.log("Express http server listening on: " + HTTP_PORT); } let booksData = []; fs.readFile('books.json', 'utf8', (err, data) => { if (err) { console.error(err); return; } booksData = JSON.parse(data); }); app.engine(".hbs", exphbs.engine({ extname: ".hbs", defaultLayout: false, layoutsDir: path.join(__dirname, "/views") })); app.set("view engine", ".hbs"); // Middleware to check session authentication function authenticateSession(req, res, next) { if (req.session.username) { next(); // User is authenticated, continue to the next middleware } else { res.redirect('/LandingPage'); // User is not authenticated, redirect to LandingPage } } app.get('/', (req, res) => { res.render('Landing Page', { layout: false }); }); app.get("/LandingPage", function(req, res) { res.render('Landing Page', { layout: false }); }); app.get("/SignIn", function(req, res) { res.render('Sign In', { heading: 'uLibrary', layout: false }); }); app.post('/signin', express.urlencoded({ extended: false }), (req, res) => { const { username, password } = req.body; // Read the users.json file fs.readFile('./users.json', 'utf8', (err, data) => { if (err) { console.error(err); return res.status(500).send('Internal Server Error'); } const users = JSON.parse(data); // Check if the entered username exists in the users.json file if (!users.hasOwnProperty(username)) { return res.redirect('/LandingPage'); } // Check if the entered password matches the stored password for the username if (users[username] !== password) { return res.redirect('/LandingPage'); } // Successful sign in, set username in session req.session.username = username; // Redirect to the Home Webpage res.redirect('/home'); }); }); app.get('/home', authenticateSession, (req, res) => { const username = req.session.username; const availableBooks = booksData.filter((book) => book.available); const borrowedBooks = booksData.filter((book) => !book.available); res.render('Home', { heading: 'Home', availableBooks: availableBooks, borrowedBooks: borrowedBooks, layout: false }); console.log(booksData); }); app.post('/borrow', (req, res) => { const selectedBooks = req.body.books || []; // Array of selected book titles console.log(selectedBooks); // Update the availability property for selected books selectedBooks.forEach((selectedBook) => { const book = booksData.find((item) => item.title === selectedBook); console.log("book selected:") console.log(book); if (book) { console.log(book.available); book.available = false; } }); fs.writeFile('books.json', JSON.stringify(booksData), 'utf8', (err) => { if (err) { console.error(err); } else { console.log('books.json updated successfully.'); } }); res.redirect('/home'); }); // Handle Return button click app.post('/return', (req, res) => { const selectedBooks = req.body.books || []; // Array of selected book titles console.log(selectedBooks); // Update the availability property for selected books selectedBooks.forEach((selectedBook) => { const book = booksData.find((item) => item.title === selectedBook); console.log(book); console.log(book.available); if (book) { book.available = true; } }); fs.writeFile('books.json', JSON.stringify(booksData), 'utf8', (err) => { if (err) { console.error(err); } else { console.log('books.json updated successfully.'); } }); res.redirect('/home'); }); app.listen(HTTP_PORT, onHttpStart);
3. books.json数据
[ { "title": "The Shining", "author": "Stephen King", "available": true }, { "title": "Rainbow Six", "author": "Tom Clancy", "available": false }, { "title": "Steve Jobs", "author": "Walter Isaacson", "available": true }, { "title": "Elon Musk", "author": "Ashley Vance", "available": false } ]
问题分析与修复方案
1. 核心问题:表单数组参数无法解析
你的Express配置中使用了express.urlencoded({ extended: false }),extended: false会调用Node.js内置的querystring模块解析表单数据,但该模块不支持解析books[]这种数组格式的参数,导致req.body.books始终为undefined,无法获取选中的书籍列表,自然无法更新书籍状态。
修复:将extended改为true,使用qs模块解析表单数据,支持数组参数:
app.use(express.urlencoded({ extended: true }));
2. 模板冗余判断导致潜在展示问题
后端已经通过filter得到了availableBooks(仅包含available: true的书籍)和borrowedBooks(仅包含available: false的书籍),但模板中又额外添加了{{#if available}}和{{#unless available}}的判断,完全冗余,甚至可能在数据异常时导致展示错误。
修复:删除模板中的冗余判断:
{{!-- 左侧可借书籍表单 --}} {{#each availableBooks}} <div class="book"> <input type="checkbox" name="books[]" value="{{title}}"> <span>{{title}}</span> </div> {{/each}} {{!-- 右侧已借书籍表单 --}} {{#each borrowedBooks}} <div class="book"> <input type="checkbox" name="books[]" value="{{title}}"> <span>{{title}}</span> </div> {{/each}}
3. 异步文件写入未完成即跳转
当前代码中fs.writeFile是异步操作,但你在调用它之后立刻执行了res.redirect('/home'),可能出现文件还未完成写入就跳转的情况(虽然内存中的booksData已经更新,但如果服务器重启,未写入的数据会丢失)。
修复:将res.redirect放在fs.writeFile的回调函数中,确保文件写入完成后再跳转:
// /borrow接口修改 fs.writeFile('books.json', JSON.stringify(booksData), 'utf8', (err) => { if (err) { console.error(err); } else { console.log('books.json updated successfully.'); } res.redirect('/home'); // 写入完成后再跳转 }); // /return接口同理修改 fs.writeFile('books.json', JSON.stringify(booksData), 'utf8', (err) => { if (err) { console.error(err); } else { console.log('books.json updated successfully.'); } res.redirect('/home'); });
4. 借还接口缺乏登录验证
当前/borrow和/return接口没有添加authenticateSession中间件,未登录用户也能调用这些接口,存在安全隐患。
修复:给这两个接口添加登录验证:
app.post('/borrow', authenticateSession, (req, res) => { // 原有代码 }); app.post('/return', authenticateSession, (req, res) => { // 原有代码 });
内容的提问来源于stack exchange,提问作者bubs

