Hướng Dẫn Xây Dựng Books Store Với NodeJS, Express và MongoDB - Phần 2

Chức Năng Hiển Thị Và Tạo Mới Books
Định Nghĩa Router Tạo Mới Trong Books
Trong thư mục routes có file books.routes.js
thì trong này sẽ là nơi chứa toàn bộ các source liên quan đến books.
Trước tiên các bạn tạo cho mình router create books trước đã nha, trong router này nó sẽ bao gồm 2 router đó là router.get('/new')
và router.post('/')
. Router.get nó thực hiện nhiệm vụ get data và render ra views trả về phía client còn router.post sẽ gửi dữ liệu của books lên server. Các bạn chỉ cần tạo và render ra views trước cho mình đã nha và require model author vào nha model này là mình đã định nghĩa trước đó rồi nha. Mình cũng nói sơ qua một chút là trong books này mình sẽ tạo các fields như: title, description, author, cover image, page count, publishDate.
Đấy các bạn xem bức hình ở dưới thì sẽ hiểu rõ hơn nha, bức hình đấy mình chỉ mô tả sơ qua thôi nha.

const express = require("express")
const router = express.Router()
const Author = require("../models/author.model")
router.get('/new', async (req, res, next) => {
try {
const authors = await Author.find()// find author với mục đích là để tạo books bao gồm việc chọn author trong tạo books
const book = new Book()// Tạo một object rỗng để có thể truyền data và render
res.render('books/new', {
authors: authors,
book: book
})
} catch{
res.redirect('/books')
}
});
// Create New Books
router.post('/', async (req, res) => {
.......
.......
);
Giao Diện Tạo Mới Trong Books
Trong views
bạn tạo cho mình thư mục là books
sẽ bao gồm 4 file index.pug
, new.pug
, show.pug
, edit.pug
.
Trước tiên các bạn nhớ extends layout vào nha và tạo cho mình một form sẽ chứa các fields mà mình đã nói ở trên nha.
books/new.pug
extends ../layouts/layout
block content
.container
h2 New Books
form(action="/books" method="POST")
div
div
label(for="name" ) Title
input(placeholder="Title", type="text" name="title" value=book.title )
div
label(for="author") Author
select(name="author")
each author in authors
if author.id === book.author
option(selected label=author.name, value=author.id)
else
option( label=author.name, value=author.id)
div
div
label(for="publish date") Publish Date
input(type="date" name="publishDate"
value= book.publishDate == null ? '' :
book.publishDate.toISOString().split('T')[0]) // Cái này mình sử dụng toán tử 3 ngôi nha, Nếu publishDate không có giá trị(true) thì nó sẽ có dạng một empty string và ngược lại
div
label(for="page count") Page Count
input(placeholder="Page Count",type="number" name="pageCount" min="1" value= book.pageCount )
div
div
label(for="coverImg") Image Cover
input(type="file" name="ImageUrl" required)
div
label(for="description") Description
textarea(name="description", book.description )
div
a(href="/books") Cancel
button(type="submit") Create
Và đây là giao diện khi căn bản khi chúng ta tạo books

Định Nghĩa Các Schema Books Cho Mongoose
Như các bạn đã đọc ở trên thì cấu trúc thư mục cho dự án bên trong thư mục src
các bạn tạo cho mình thư mục models, bên trong thư mục models các bạn tạo cho mình file books.model.js
. Trong file books.model.js
cần tạo một đối tượng book và sẽ chứa các fields như title, description, publishDate, pageCount, createdAT, ImageUrl, author của books.
Ref là một option trong mongoose có thể hiểu là nó sẽ liên kết với một model nào đó.
CreatedAT và publishDate nó khác nhau ở chỗ là publishDate sẽ trả về date mà bạn đã chọn còn createdAT nó sẽ trả về đúng ngày đúng thời gian mà mình đã tạo books.
const mongoose = require('mongoose');
const bookSchema = new mongoose.Schema({
title:{type: String, required:true},
description:{type: String, required:true},
publishDate:{type: Date, required:true},
pageCount:{type: Number, required:true},
createdAT:{type: Date, required:true, default: Date.now},
ImageUrl:{type:String, required:true},
author:{type:mongoose.Schema.Types.ObjectId, required:true, ref:'Author'}
})
module.exports = mongoose.model('Book', bookSchema)
Tham số thứ nhất là tên riêng cho collection sắp được tạo ra cho mô hình của bạn, và tham số thứ hai là schema mà bạn muốn dùng để tạo ra mô hình.
Controller Tạo Mới Trong Books
Cũng trong file books.routes.js
ta có router.post('/',..) dùng để tạo books. Các bạn nhớ require model của book mà mình đã tạo vào nha.
Nó sẽ tạo một object rộng chứa dữ liệu book được truyền vào. Rồi lưu dữ liệu vào database đã được khởi tạo.
Chúng ta sử dụng try{} catch{} nên khi có lỗi thì sẽ render ra lỗi khi tạo book.
const Book = require("../models/books.model")
// Create New Books
router.post('/', async (req, res) => {
const authors = await Author.find()
const book = new Book({
title: req.body.title,
author: req.body.author,
publishDate: new Date(req.body.publishDate),
pageCount: req.body.pageCount,
description: req.body.description,
})
try {
const newBook = await book.save()
res.redirect('/books')
} catch{
res.render('books/new', {
authors: authors,
book: book,
errorMessage: 'Error Creating Book'
})
}
});
Khi mình tạo book thành công thì lưu vào DB và redirect sang trang books, bây giờ trong router.get('/',..) các bạn res.send một tin nhắn gì đấy khi tạo books thành công chẳng hạn.
router.get('/', async (req, res, next) => {
res.send('All books')
})
Nếu mà có lỗi khi tạo book thì trong books/new.pug các bạn thêm dòng code ở dưới vào phía sau thẻ h2 New Books là được nha.
if locals.errorMessage
= errorMessage
Đừng vội mừng chưa xong mô nạ :)))
Khi các bạn tạo books xong rồi mới nhớ lại là Cover image mình chưa có thì lấy đâu ra ảnh mà hiển thị cho books đây. Các bạn cứ yên tâm mình đã có cách =))
Upload File Sử Dụng Cloudinary
Mình sẽ sử dụng kết hợp multer + cloudinary để thực hiện việc upload ảnh cho books Các bạn xem ảnh ở dưới để biết thêm về nguyên lí hoạt động nha:

Client(user) upload file(ảnh) thì file(ảnh) được gửi lên server(multer). Sau đó server sẽ upload file(ảnh) lên cloudinary và cloudinary sẽ trả về một url của file(ảnh) đấy rồi được lưu vào DB. Từ đó ta chỉ cần get url của image trong DB rồi render ra phía client.
Đầu tiên các bạn nên chỉnh sửa trong views trước đã nha, trong phần views của books/new.pug các bạn thêm enctype="multipart/form-data"
vào trong form nha form(action="/books" method="POST" enctype="multipart/form-data")
, bởi vì multer nó chỉ xử lý biểu mẫu nào liên quan đến multipart/form-data
Tiếp theo như các bạn để ý ở trong phần cấu trúc thư mục có folder handlers trong folder này sẽ chứa file upload.multer.js
. Trong file này là nơi mà chúng ta dùng để code phần multer cho việc upload file(ảnh).
const multer = require('multer') // khai báo multer
module.exports = multer({
storage: multer.diskStorage({}),
fileFilter: (req, file, cb) => { // filFilter nó sẽ kiểm soát việc file nào nên tải lên và file nào không
if (!file.mimetype.match(/jpe|jpeg|png|gif$i/)) { // Nếu không đúng loại file ảnh thì sẽ không cho upload file và ngược lại
cb(new Error('File is not supported'), false)
return
}
cb(null, true)
}
})
Trong file books.routes.js
thì trước tiên các bạn nhớ khai báo file upload.multer.js và module cloudinary.
const cloudinary = require('cloudinary')
const upload = require('../handlers/upload.multer')
Setup Cloudinary
Trước tiên các bạn vào website cloudinary tại đây để đăng ký tài khoản, sau khi đăng ký xong thì login thành công sẽ được vào trang dashboard của cloudinary.
Tại đây các bạn chú ý cho mình phần Account Details sẽ là nơi chứa các thông số mà bạn dùng để có thể sử dụng để upload ảnh lên cloudinary.

Bây giờ cũng trong file books.routes.js
các bạn cấu hình cloudinary vào nha. CLOUD_NAME, API_ID(API Key) và API_SECRET là các thông số mật nên chúng ta cần lưu ở biến môi trường nha .env để nó có tính bảo mật.
// Setup Cloudinary
cloudinary.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_ID,
api_secret: process.env.API_SECRET
})
Trong router.post('/',...) các bạn chỉ cần khởi tạo middleware upload file với cấu hình như trên. Sau khi xác định các thông số cấu hình Cloudinary thì bây giờ việc upload lên cloudinary khá đơn giản, nếu các bạn muốn biết thêm về phần upload thì xem tại đây nha.
Khi upload lên cloudinary thành công thì url image sẽ được lưu vào DB với key là ImageUrl đã được cấu hình sẵn.
// Create New Books
router.post('/', upload.single('ImageUrl'), async (req, res) => {
const result = await cloudinary.v2.uploader.upload(req.file.path)
const book = new Book({
...........
...........
...........
ImageUrl: result.secure_url
})
........
........
........
});
Vậy là xong phần upload file cũng như xong phần tạo books rồi nha
Cùng xem kết quả nào:

Và đây là hình ảnh sau khi được upload lên cloudinary và được lưu vào DB

Định Nghĩa Và Controller Router Hiển Thị Trong Books
Trong thư mục routes có file books.routes.js
thì trong này sẽ là nơi chứa toàn bộ các source code liên quan đến books.
Chúng ta tạo router.get('/')
đây chính là router của trang books, mục đích của trang này là tìm kiếm và hiển thị books.
Cách làm chức năng tìm kiếm và hiển thị của thèn books này cũng không khác thèn author cho lắm chỉ khác là mình sẽ có thêm chức năng tìm kiếm theo ngày xuất bản sau và ngày xuất bản trước mà thôi.
Thì trước tiên các bạn viết cho mình hàm try{}catch{}, trong try{} là nơi mà ta sẽ find Book trong DB và render ra author còn catch{} sẽ redirect sang trang homepage nếu có lỗi xảy ra. Có .exec () hay không có thì về mặt chức năng hai cái này tương đương nhau các bạn có thể xem thêm tại đây nha.
Bây giờ mình sẽ giới thiệu cho các bạn về hai thèn đó là publishedAfter và publishedBefore là như nào.
publishedAfter có nghĩa là xuất bản sau bởi vậy nên chúng ta sẽ sử dụng gte là một toán tử truy vấn, gte có nghĩa là lớn hơn hoặc bằng.
publishedBefore có nghĩa là xuất bản trước bởi vậy nên chúng ta sẽ sử dụng lte là một toán tử truy vấn, lte có nghĩa là nhỏ hơn hoặc bằng.
VD: Bây giờ bạn publish books vào ngày 28 khi tìm kiếm bạn chọn publishedAfter là ngày 27 và publishedBefore là ngày 29. Thì có phải là publishedAfter ta sử dụng gte thì nó sẽ trả về các ngày 27,28,29,30... Còn publishedBefore ta sử dụng lte thì nó sẽ trả về các ngày 29,28,27,26... Cái này giống bài toán liệt kê bây giờ chúng ta chỉ cần lấy giao của 2 thèn thôi =))).
Vì vậy nên khi tìm kiếm thì sẽ hiển thị kết quả từ ngày 27->29 các bạn tìm hiểu thêm tại đây nha.
// GET Books Page
router.get('/', async (req, res, next) => {
let query = Book.find()
if (req.query.title != null && req.query.title != '') {
query = query.regex('title', new RegExp(req.query.title, 'i'))
}
if (req.query.publishedAfter != null && req.query.publishedAfter != '') {
query = query.gte('publishDate', req.query.publishedAfter)
}
if (req.query.publishedBefore != null && req.query.publishedBefore != '') {
query = query.lte('publishDate', req.query.publishedBefore)
}
try {
const books = await query.exec()
res.render('books/index', {
books: books,
searchOptions: req.query
})
} catch{
res.redirect('/')
}
});
Giao Diện Hiển Thị Trong Books
Trong views
bạn tạo cho mình thư mục là books sẽ bao gồm 4 file index.pug
, new.pug
, show.pug
, edit.pug
.
Trước tiên các bạn extends layout vào nha và lặp qua các books để hiển thị ra cover image và title của books, nhớ bọc ở ngoài là một thẻ a nha các bạn đừng quan tâm cái books id đó vội nha chỉ cần biết là nó sẽ làm chức năng show detail là được rồi. Tạo một form để làm ô tìm kiếm cho author với method="GET" nha.
SearchOptions này là một object do mình đã định nghĩa req.query ở trên bây giờ muốn lấy value thì ta cần .title, .publishedAfter và .publishedBefore để có thể lấy được value của chúng.
books/index.pug
extends ../layouts/layout
block content
.container
h2 Search Books
form(action="/books" method="GET")
div
label(for="title") Title
input(placeholder="Title Book",type="text" name="title" value=searchOptions.title)
div
div
label(for="published-after") Published After
input( type="date" name="publishedAfter" value=searchOptions.publishedAfter)
div
label(for="published-before") Published Before
input(type="date" name="publishedBefore" value=searchOptions.publishedBefore)
div
button(type="submit") Search
div
each book in books
a(href='/books/' + book.id)
img(src=book.ImageUrl, width="250" height="280")
h3= book.title
Lưu ý: Mình có cập nhật lại code tại một số chỗ nên khi bạn thấy trên ảnh gif sẽ không được chính xác cho lắm nha.
Và đây là kết quả khi chúng ta hiển thị và tìm kiếm books nha:

Vậy là kết thúc phần 2 rồi nha mời các bạn xem tiếp phần 3
To Be Continued !!
Lời Kết
Vậy là xong bài Hướng Dẫn Xây Dựng Books Store Với NodeJS, Express và MongoDB - Phần 2 rồi nhé. Các bạn xem phần tiếp theo nha mình chia thành nhiều phần ra để các bạn có thể đọc và làm một cách dễ dàng hơn rành mạch và dễ hiểu hơn.
Các bạn nhớ like và theo dõi fanpage Thanh Long Dev để nhận những thông báo về bài viết mới nhất nha.
Nếu các bạn cảm thấy bài viết của mình hay thì các bạn có thể ủng hộ mình để mình có thêm động lực để ra những bài topic hay và chất lượng hơn ủng hộ mình tại đây nha.
Chúc Các Bạn Thành Công!!