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

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')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à publishedAfterpublishedBefore 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.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!!