FreeCodeCamp 中对 Basic Node 和 Express 进行了简短介绍之后,我正在尝试建立一些后端开发知识(因为我需要它!)课程 我现在正在尝试遵循 MDN Express Locallibrary 教程。一切都很顺利,直到出现图书列表页面。
当我单击“所有书籍”链接时,应用程序应呈现所有书籍的列表以及每本书的作者姓名。我可以正常获取图书清单,但缺少作者姓名。
我本身没有收到任何错误消息,但是,当我将鼠标悬停在 allBooks
async handler
变量上(我正在使用 VS Code)时,我注意到
author
值似乎有些不对劲,因为
author
键后面有一个问号,值有
Types.ObjectID
后跟
null | undefined
const allBooks: Omit<Document<unknown, {}, {
title: string;
summary: string;
isbn: string;
genre: Types.ObjectId[];
author?: Types.ObjectId | null | undefined;
}> & {
title: string;
summary: string;
isbn: string;
genre: Types.ObjectId[];
author?: Types.ObjectId | null | undefined;
} & {
...;
}, never>[]
现在,我最初手动输入了大部分代码,只是为了习惯使用 Mongoose ODM以及
PugJS 模板引擎的格式和语法,并且在一天左右未能找到问题根源之后我从一开始就完成了本教程,并将代码复制并粘贴到应用程序中的每个单独文件中,但我仍然遇到完全相同的问题。 此论坛上发布了许多有关本教程的问题,但没有一个专门针对此案例。我确实发现这个问题以前曾出现过:作者姓名未渲染
,但这似乎已在 2020 年修复。我在 git Hub 讨论中注意到当时有人担心数据库的状态。考虑到这一点,我删除了 Mongo DB 中的所有表,并用原始数据重新填充它,因为我认为我可能在某个时候损坏了数据。这并没有解决问题,我不知道从这里该去哪里。我觉得如果我只是忽略这个问题,它就不会消失,并且很可能会在以后回来咬我。
我发现,如果我在 #{book.author}
#{book.author.name}
而不是
book_list.pug
,则应用程序会从数据库中呈现作者的
_Id
。这告诉我它必须与虚拟类型
name
有关,它是在
AuthorSchema
上定义的,但是,这就是我的技能所能达到的程度。我只是对这个堆栈技术不够熟悉,不知道如何解决这个问题。
我的应用程序代码如下。
型号
author.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const AuthorSchema = new Schema({
first_name: { type: String, required: true, maxLength: 100 },
family_name: { type: String, required: true, maxLength: 100 },
date_of_birth: { type: Date },
date_of_death: { type: Date },
});
AuthorSchema.virtual("name").get(function () {
// To avoid erros where an author doesn't have a family name or first name
// we want to make sure we handle the exception by returning an empty string for that case
let fullname = "";
if (this.first_name && this.last_name) {
fullname = `${this.family_name}, ${this.first_name}`;
}
return fullname;
});
// Virtual for author's URL returns an absolute URL
// we'll use this property in our templates whenever we want to get a link for an author
AuthorSchema.virtual("url").get(function () {
// We don't want to use arrow functions because we'll need to us this object
return `/catalog/author/${this._id}`;
});
// Export the `Author` model
module.exports = mongoose.model("Author", AuthorSchema);
book.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const BookSchema = new Schema({
title: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: "Author", required: true },
summary: { type: String, required: true },
isbn: { type: String, required: true },
genre: [{ type: Schema.Types.ObjectId, ref: "Genre" }],
});
// Virtual for book's URL
BookSchema.virtual("url").get(function () {
// We don't want to use arrow functions as we'll need the `this` object
return `/catalog/book/${this._id}`;
});
module.exports = mongoose.model("Book", BookSchema);
控制器
bookController.js
// book controller
// The controllers use the `express-async-handler module` which will
// catch any exceptions thrown in our route handler functions,
// this needs to be installed using `npm install express-async-handler`
const Book = require("../models/book");
const Author = require("../models/author");
const Genre = require("../models/genre");
const BookInstance = require("../models/bookinstance");
const asyncHandler = require("express-async-handler");
const author = require("../models/author");
exports.index = asyncHandler(async (req, res, next) => {
// Get details of books, book instances, authors and genre counts (in parallel)
const [
numBooks,
numBookInstances,
numAvailableBookInstances,
numAuthors,
numGenres,
] = await Promise.all([
Book.countDocuments({}).exec(),
BookInstance.countDocuments({}).exec(),
BookInstance.countDocuments({ status: "Available" }).exec(),
Author.countDocuments({}).exec(),
Genre.countDocuments({}).exec(),
]);
res.render("index", {
title: "Local Library Home",
book_count: numBooks,
book_instance_count: numBookInstances,
book_instance_available_count: numAvailableBookInstances,
author_count: numAuthors,
genre_count: numGenres,
});
});
// Display list of all books.
exports.book_list = asyncHandler(async (req, res, next) => {
const allBooks = await Book.find({}, "title author")
.sort({ title: 1 })
.populate("author")
.exec();
res.render("book_list", { title: "Book List", book_list: allBooks });
});
// Display detail page for a specific book.
exports.book_detail = asyncHandler(async (req, res, next) => {
res.send(`NOT IMPLEMENTED: Book detail: ${req.params.id}`);
});
// Display book create form on GET.
exports.book_create_get = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Book create GET");
});
// Handle book create on POST.
exports.book_create_post = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Book create POST");
});
// Display book delete form on GET.
exports.book_delete_get = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Book delete GET");
});
// Handle book delete on POST.
exports.book_delete_post = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Book delete POST");
});
// Display book update form on GET.
exports.book_update_get = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Book update GET");
});
// Handle book update on POST.
exports.book_update_post = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Book update POST");
});
authorController
const Author = require("../models/author");
const asyncHandler = require("express-async-handler");
// Display list of all Authors.
exports.author_list = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Author list");
});
// Display detail page for a specific Author.
exports.author_detail = asyncHandler(async (req, res, next) => {
res.send(`NOT IMPLEMENTED: Author detail: ${req.params.id}`);
});
// Display Author create form on GET.
exports.author_create_get = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Author create GET");
});
// Handle Author create on POST.
exports.author_create_post = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Author create POST");
});
// Display Author delete form on GET.
exports.author_delete_get = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Author delete GET");
});
// Handle Author delete on POST.
exports.author_delete_post = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Author delete POST");
});
// Display Author update form on GET.
exports.author_update_get = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Author update GET");
});
// Handle Author update on POST.
exports.author_update_post = asyncHandler(async (req, res, next) => {
res.send("NOT IMPLEMENTED: Author update POST");
});
路线
catalog.js
// Import the Express application & create a router
const express = require("express");
// The routes are all set up on the router which is then exported
const router = express.Router();
// Require controller modules.
const book_controller = require("../controllers/bookController");
const author_controller = require("../controllers/authorController");
const genre_controller = require("../controllers/genreController");
const book_instance_controller = require("../controllers/bookinstanceController");
/// BOOK ROUTES ///
// GET catalog home page.
router.get("/", book_controller.index);
// GET request for creating a Book. NOTE This must come before routes that display Book (uses id).
router.get("/book/create", book_controller.book_create_get);
// POST request for creating Book.
router.post("/book/create", book_controller.book_create_post);
// GET request to delete Book.
router.get("/book/:id/delete", book_controller.book_delete_get);
// POST request to delete Book.
router.post("/book/:id/delete", book_controller.book_delete_post);
// GET request to update Book.
router.get("/book/:id/update", book_controller.book_update_get);
// POST request to update Book.
router.post("/book/:id/update", book_controller.book_update_post);
// GET request for one Book.
router.get("/book/:id", book_controller.book_detail);
// GET request for list of all Book items.
router.get("/books", book_controller.book_list);
/// AUTHOR ROUTES ///
// GET request for creating Author. NOTE This must come before route for id (i.e. display author).
router.get("/author/create", author_controller.author_create_get);
// POST request for creating Author.
router.post("/author/create", author_controller.author_create_post);
// GET request to delete Author.
router.get("/author/:id/delete", author_controller.author_delete_get);
// POST request to delete Author.
router.post("/author/:id/delete", author_controller.author_delete_post);
// GET request to update Author.
router.get("/author/:id/update", author_controller.author_update_get);
// POST request to update Author.
router.post("/author/:id/update", author_controller.author_update_post);
// GET request for one Author.
router.get("/author/:id", author_controller.author_detail);
// GET request for list of all Authors.
router.get("/authors", author_controller.author_list);
/// GENRE ROUTES ///
// GET request for creating a Genre. NOTE This must come before route that displays Genre (uses id).
router.get("/genre/create", genre_controller.genre_create_get);
//POST request for creating Genre.
router.post("/genre/create", genre_controller.genre_create_post);
// GET request to delete Genre.
router.get("/genre/:id/delete", genre_controller.genre_delete_get);
// POST request to delete Genre.
router.post("/genre/:id/delete", genre_controller.genre_delete_post);
// GET request to update Genre.
router.get("/genre/:id/update", genre_controller.genre_update_get);
// POST request to update Genre.
router.post("/genre/:id/update", genre_controller.genre_update_post);
// GET request for one Genre.
router.get("/genre/:id", genre_controller.genre_detail);
// GET request for list of all Genre.
router.get("/genres", genre_controller.genre_list);
/// BOOKINSTANCE ROUTES ///
// GET request for creating a BookInstance. NOTE This must come before route that displays BookInstance (uses id).
router.get(
"/bookinstance/create",
book_instance_controller.bookinstance_create_get
);
// POST request for creating BookInstance.
router.post(
"/bookinstance/create",
book_instance_controller.bookinstance_create_post
);
// GET request to delete BookInstance.
router.get(
"/bookinstance/:id/delete",
book_instance_controller.bookinstance_delete_get
);
// POST request to delete BookInstance.
router.post(
"/bookinstance/:id/delete",
book_instance_controller.bookinstance_delete_post
);
// GET request to update BookInstance.
router.get(
"/bookinstance/:id/update",
book_instance_controller.bookinstance_update_get
);
// POST request to update BookInstance.
router.post(
"/bookinstance/:id/update",
book_instance_controller.bookinstance_update_post
);
// GET request for one BookInstance.
router.get("/bookinstance/:id", book_instance_controller.bookinstance_detail);
// GET request for list of all BookInstance.
router.get("/bookinstances", book_instance_controller.bookinstance_list);
// Export the router
module.exports = router;
index.js
var express = require("express");
var router = express.Router();
/* GET home page. */
// router.get("/", function (req, res, next) {
// res.render("index", { title: "Express Yourself!" });
// });
// GET home page.
router.get("/", function (req, res) {
res.redirect("/catalog");
});
module.exports = router;
浏览量
book_list.pug
extends layout
block content
h1= title
if book_list.length
ul
each book in book_list
li
a(href=book.url) #{book.title}
| (#{book.author.name})
else
p There are no books.
index.pug
extends layout
block content
h1= title
p Welcome to #[em LocalLibrary], a very basic Express website developed as a tutorial example on the Mozilla Developer Network.
h1 Dynamic content
p The library has the following record counts:
ul
li #[strong Books:] !{book_count}
li #[strong Copies:] !{book_instance_count}
li #[strong Copies available:] !{book_instance_available_count}
li #[strong Authors:] !{author_count}
li #[strong Genres:] !{genre_count}
布局.pug
doctype html
html(lang='en')
head
title= title
meta(charset='utf-8')
meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css", integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N", crossorigin="anonymous")
script(src="https://code.jquery.com/jquery-3.5.1.slim.min.js", integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj", crossorigin="anonymous")
script(src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js", integrity="sha384-+sLIOodYLS7CIrQpBjl+C7nPvqq+FbNUBDunl/OZv93DB7Ln/533i8e/mZXLi/P+", crossorigin="anonymous")
link(rel='stylesheet', href='/stylesheets/style.css')
body
div(class='container-fluid')
div(class='row')
div(class='col-sm-2')
block sidebar
ul(class='sidebar-nav')
li
a(href='/catalog') Home
li
a(href='/catalog/books') All books
li
a(href='/catalog/authors') All authors
li
a(href='/catalog/genres') All genres
li
a(href='/catalog/bookinstances') All book-instances
li
hr
li
a(href='/catalog/author/create') Create new author
li
a(href='/catalog/genre/create') Create new genre
li
a(href='/catalog/book/create') Create new book
li
a(href='/catalog/bookinstance/create') Create new book instance (copy)
div(class='col-sm-10')
block content
申请
app.js
const createError = require("http-errors");
const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
// Import the routes
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
const catalogRouter = require("./routes/catalog"); //Import routes for "catalog" area of site
const app = express();
// DB connection
// Set up mongoose connection
// NB: just for dev purposes. Do this more safely for production
const mongoose = require("mongoose");
mongoose.set("strictQuery", false);
const mongoDB =
"mongodb+srv://username:[email protected]/local_library?retryWrites=true&w=majority";
main().catch((err) => console.log(err));
async function main() {
await mongoose.connect(mongoDB);
}
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
// make Express serve all the files in the .public directory
app.use(express.static(path.join(__dirname, "public")));
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/catalog", catalogRouter); // Add catalog routes to middleware chain.
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.render("error");
});
module.exports = app;
www
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('express-locallibrary-tutorial:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
代码几乎是正确的,只是作者模型中有一个拼写错误,其中使用了
family_name
例如
if (this.first_name && this.last_name) {
...
}
应该是
if (this.first_name && this.family_name) {
...
}