Hướng Dẫn Xây Dựng Pagination Với NodeJS, Express và MongoDB

Hướng Dẫn Xây Dựng Pagination Với NodeJS, Express và MongoDB

Giới Thiệu Chung

Hôm nay mình sẽ hướng dẫn các bạn làm một trong những chức năng khá phổ biến trên các trang web hiện nay đó là pagination.
Pagination hay còn gọi là phân trang, là quá trình phân chia tài liệu thành các trang riêng biệt, hoặc trang điện tử hoặc trang in.
Tuy bài này không có gì gọi là quá mới lạ hay khác biệt nhưng mình sẽ viết một cách tận tình và chu đáo mong các bạn xem đầy đủ và kỹ càng.

Bắt Đầu Làm Thôi Nào

Lên Ý Tưởng

Ý tưởng cái này cũng đơn giản thôi thì như các bạn đã biết thì bây giờ mình sẽ tạo một trang page chứa chứa tất cả sản phẩm đúng không nào. Nhưng bây giờ nếu như chúng ta có 200 sản phẩm mà chỉ đặt chúng lên một trang thì đó không phải là cách tối ưu và trải nghiệm người dùng không được tốt. Nên chúng ta sẽ sử dụng kỹ thuật phân trang(pagination).

Hướng để làm cái thì mình bắt buộc phải có sản phẩm(data) gì đó rồi mới làm phân trang cho nó đúng không nào. Bây giờ mình muốn phân làm 4 trang mỗi trang 16 sản phẩm(data) thế là chúng ta cần phải tạo 64 sản phẩm à tạo khi nào cho xong nên ta cần phải fake data cho chúng =))

Bài này mình chỉ hướng dẫn các bạn làm về pagination nên mới fake data chớ vào thực tế thì các bạn phải tạo sản phẩm đầy đủ nha.
Sau khi mà fake data thì đây là bước quan trọng nhất đó là phân trang các bạn xem tiếp để biết thêm chi tiết nha. Đó cũng chính là ý tưởng của mình và bây giờ chúng ta sẽ biến ý tưởng thành hiện thực.

Cài Đặt Và Thiết Lập

Trước tiên các bạn tạo cho mình một folder trong folder đó là nơi chứa các thư mục dùng để mình code pagination.
Để mà cài các module trước hết các bạn phải cài đặt NodeJS tại đây.
Khởi tạo ứng dụng với file package.json
Trong thư mục gốc của ứng dụng của bạn và nhập npm init để khởi tạo ứng dụng của bạn với tệp package.json.
npm init
Sau đó các bạn cài đặt các module ở dưới để thiết lập ứng dụng nha.

  • npm install express --save
    Module dùng để cài đặt framework express của nodejs giúp các bạn code một cách tối giản cũng như nhanh chóng hơn.
  • npm install nodemon --save
    Tự động reload lại server khi bạn thay đổi code. Để khởi chạy server các bạn thêm "dev": "nodemon src/app.js" vào file package.json nha. Các bạn mở terminal lên sau đó gõ npm run dev để khởi chạy server nha.
  • npm install mongoose --save
    Mongoose là một Object Document Mapper (ODM). Điều này có nghĩa là Mongoose cho phép bạn định nghĩa các object (đối tượng) với một schema được định nghĩa rõ ràng.
  • npm install dotenv --save
    Dotenv là một biến môi trường dùng để bảo mật các thông tin quan trọng như username, password, url database,...Nếu chúng ta không lưu những thông tin mật vào file .env thì khi push source code lên github thì ai cũng có thể vào xem được và ai cũng biết username, password thì sẽ bị người khác chiếm đoạt và đánh cắp tài liệu rất là nguy hiểm nha.
  • npm install faker --save
    Faker là một package dùng để tạo các dữ liệu fake(giả) bao gồm rất nhiều API methods khác nhau như address, image, name,... các bạn xem thêm tại đây nha
  • npm install ejs --save
    EJS nó chỉ là một template engine mà thôi hôm nay mình đổi giớ một tí thay vì dùng pug thì mình dùng ejs.
    Và đây cũng là các package mà mình khi đã cài đặt cũng như cấu hình xong.

Cấu Trúc Thư Mục Cho Dự Án

Sau khi hoàn thành việc cài đặt module các bạn tạo cho mình một thư mục src, thư mục này dùng để chứa tất cả các file trong quá trình mình code project.
Mình khuyên các bạn nên tạo thêm một folder controllers và file để chứa các logic khi mình code. Do mình code để làm demo cho mọi người nên mình viết chung vào thằng routes luôn.

Bắt Đầu Code Thôi Nào

Thiết Lập Web Server

Trong thư mục gốc các bạn tạo cho mình file app.js, file này là file chính dùng để điều khiển mọi hoạt động chính của server.

const express = require("express")
const app = express()
const path = require("path")
const port = 3333

// Import routes
const indexRoutes = require("./routes/index.routes")

// Settings
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');


// routes
app.use(indexRoutes);

app.listen(port, function(){
    console.log(`Server listening ${port}`)
})

Trong thư mục routes có file index.routes.js, các bạn tạo cho nó một Route handlers và thử chạy sever xem sau. Mục đích để các bạn test xem server có chạy không nha.

router.get('/', (req, res, next) => {
   res.send('Hello Everyone')
})

Thiết Lập Database

Để mà có thể thiết lập được database thì các bạn đăng nhập tài khoản mongodb atlas tại đây nha. Sau khi các bạn bạn đã có tài khoản mongodb atlas thì tiếp đến cài đặt cấu hình để có thể kết nối mongodb atlas với ứng dụng của chúng ta. Để biết thêm chi tiết các bạn xem bước 3 trong bài viết của mình hướng dẫn deploy project nodejs lên heroku tại đây nha.
Sau đó các bạn copy link database mongodb đã tạo rồi paste vào project của mình là được nha. Các bạn tạo cho mình một file .env bên trong thư mục gốc.
Trong file này các bạn tạo một đường dẫn là DATABASE_URL=mongodb+srv://long:<password>@cluster0-b7d8c.mongodb.net/test?retryWrites=true&w=majority
Trong file app.js các bạn khai báo module dotenv với module mongoose và tạo đường dẫn mongodb.
// Khai báo dotenv
require('dotenv').config()
// Khai báo mongoose
var mongoose = require('mongoose')

// Connect database
mongoose.connect(process.env.DATABASE_URL, 
    {useNewUrlParser: true, useUnifiedTopology: true}).then(function() {
        console.log("Successfully connected to the database");    
    }).catch(function(err) {
        console.log('Could not connect to the database. Exiting now...', err);
        process.exit();
    });

Để biết nó đã kết nối hay chưa các bạn khởi động lại server sẽ biết nha.

Thiết Kế Giao Diện

Nói thiết kế thì nó hơi quá chớ thật ra làm cái này cũng đơn giản. Không cần làm mà vẫn có ăn^^, đùa chút thôi nha. Trước tiên mình sẽ code giao diện trang chủ như các bạn đã thấy ở trên phần cấu trúc thư mục thì ta thấy trong folder views chứa folder layout chứa các file như: header.ejs, footer.ejs là nơi mà chúng ta dùng để code giao diện cho app. Mình sử dụng template engine ejs nên nó khá quen thuộc với chúng ta bởi vì nó khá giống html thuần.
Trong file header.ejs mình sẽ tạo cho nó một thanh nav sẽ bao gồm logo và menu. Và trong menu sẽ chữa một nav link đó là Fake data product để khi chúng ta click vào đó sẽ tạo các data fake cái này mình sẽ nói rõ ở phần sau bạn không cần quan tâm quá nhiều.

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pagination</title>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
</head>

<body>

  <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container">
      <a class="navbar-brand" href="/">
      <img src="https://bizweb.dktcdn.net/100/228/168/themes/715878/assets/logo.png?1594693671116" alt="logo Linh Kiện Điện Tử 3M" width="50%">
      </a>
      <button class="navbar-toggler collapsed" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
      <div class="navbar-collapse collapse justify-content-end" id="navbarNavDropdown">
        <ul class="navbar-nav">

          <li class="nav-item">
            <a class="nav-link" href="/generate-fake-data">
              Fake data product
            </a>
          </li>
         
        </ul>
      </div>
    </div>
  </nav>
  <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>

Và đây là giao diện cho trang chủ của chúng ta

Định Nghĩa Các Schema Product 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 product.model.js.
Trong file product.model.js cần tạo một đối tượng book và sẽ chứa ba fields là name, price và cover.

const mongoose = require("mongoose")
const Schema = mongoose.Schema;

const productSchema = new Schema({
    name: {type: String, required: true},
    price: {type: Number, required: true},
    cover: {type: String, required: true}
})
// Biên dịch mô hình từ schema
module.exports =  mongoose.model('product', productSchema)

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.
Sau khi chúng ta đã setup xong hệ thống thì bây giờ mình sẽ đi đến chức năng là tạo data fake và lưu vào DB như mong muốn của mình.

Tạo Dữ Liệu Fake

Nhìn tiêu đề hơi củ chuối các ông nhờ =)). Nhưng mà không sao củ chuối nhiều lúc cũng thú vị, mình không có đi vòng vo nữa mà là vào thẳng vẫn đề chính luôn đó là tạo data giả(fake). Lý do tại sao chúng ta lại phải fake data mục đích của nó đơn giản là có tạo ra nhiều data một cách nhanh chóng để thực hiện công việc đó là phân trang(pagination).
Trong thư mục src các bạn tạo cho mình thư mục routes, bên trong thư mục routes các bạn tạo cho mình file index.routes.js.
Trong file index.routes.js các bạn cần import và khai báo một số module như model, faker,...
Tiếp đến bạn tạo cho mình một routes.get('/generate-fake-data', ()=>{...}) để khi chúng ta click vào router đó nó sẽ tự động tạo cho ta hàng loạt các data fake và lưu vào database như ta mong muốn.
Mình sẽ giải thích qua một chút đó là chúng ta tạo một vòng lặp để làm gì để nó có thể tự động tạo số lượng data mà mình muốn fake. Trong vòng lặp đó tạo một object empty để chứa các thuộc tính của một sản phẩm như name, price, coverImg và lưu vào database rồi redirect trang chủ. Còn các API để tạo dữ liệu fake các bạn xem tại đây nha nó cũng khá là đơn giản.

const express = require("express")
const router = express.Router()
const faker = require('faker');
const Product = require("../models/product.model")

.............
.............
.............
  // Fake data products
router.get('/generate-fake-data', async(req, res, next) =>{
    for(let i = 0; i < 96; i++) {
    const newprd = new Product();
    newprd.name = faker.commerce.productName()
    newprd.price = faker.commerce.price()
    newprd.cover = faker.image.image()
    
    newprd.save((err)=>{
        if (err) { return next(err); }
      });
    }
    res.redirect('/');    
}) 

............
............
............

module.exports = router

Chúng ta cùng xem thử kết quả để biết nó có tự động fake data và lưu vào DB không nha

Hướng Dẫn Phân Trang

Sau khi đã data fake xong bây giờ chúng ta sẽ bắt đầu phân trang cho nó nha. Để phân trang mình cần làm như nhờ =)) mình sẽ nói sơ qua chút về quy trình hoạt động nha.
Thì khi chúng ta đã fake dữ liệu cho nó xong thì công việc bây giờ chính là phân trang. Có rất nhiều cách để phân trang nhưng mình sẽ sử dụng req.params và sử dụng nó như thế nào thì các bạn xem tiếp nha.
Mục đích khi mình sử dụng req.params đó là lấy thông số các page.
VD: router.get('/news/:page', (req, res, next) => {} ) thì page được gọi là params ngoài các này các bạn cũng có thể sử dụng req.query để truy vấn vào các trang được pagination.
Code Thôi Nào
Trong quá trình code mình sẽ nói thêm cho các bạn dễ hiểu hơn.
Bên trong thư mục routes có file index.routes.js chúng ta sẽ tạo một router.get('/news/:page', (req, res, next) => {} )

// pagination
router.get('/news/:page', (req, res, next) => {
    let perPage = 16; // số lượng sản phẩm xuất hiện trên 1 page
    let page = req.params.page || 1; 
  
    Product
      .find() // find tất cả các data
      .skip((perPage * page) - perPage) // Trong page đầu tiên sẽ bỏ qua giá trị là 0
      .limit(perPage)
      .exec((err, products) => {
        Product.countDocuments((err, count) => { // đếm để tính có bao nhiêu trang
          if (err) return next(err);
           res.send(products) // Trả về dữ liệu các sản phẩm theo định dạng như JSON, XML,...
        });
      });
  });

Và đây là kết quả khi chúng ta truy cập url localhost:3333/news/1 thì nó sẽ trả về dữ liệu các sản phẩm xuất hiện trên một trang đây là mình chỉ test thử thôi nha.

Sau khi chúng ta đã test thành công bằng cách gửi dữ liệu về, bây giờ ta cần làm là biến chúng thành những giao diện đẹp mắt =))
Thay vì res.send để trả về dữ liệu theo dạng json thì bây giờ chúng ta sẽ thay đổi thành res.render để trả về dữ liệu dưới dạng html nha.

// pagination
router.get('/news/:page', (req, res, next) => {

     .............
     .............
     .............
          res.render('product/index_product', {
            products, // sản phẩm trên một page
            current: page, // page hiện tại
            pages: Math.ceil(count / perPage) // tổng số các page
          });
          
      ..............
      ..............
      ..............

Trong file index_product.ejs ta sẽ code giao diện pagination cho nó. Trước tiên mình sẽ phải include thằng header và footer vào cái đã, để mà có thể hiển thị được sản phẩm ta chỉ cần lặp qua rồi chia cột cho nó là được. Nếu muốn hiển thị 3 sản phẩm thì col-4 còn muốn hiển thị 4 sản phẩm thì col-3 có thể thôi ấy mà :))

Tiếp đến phần khá là quan trọng đó là phân trang

<%- include('../layout/header'); -%>


<div class="container" style="margin-top: 50px">
  <div class="row">

    <!-- ITEMS -->
    <% for(var i = 0; i < products.length; i++) { %>
    <div class="col-md-3">
      <div class="p-1">
        <div class="card">
          <img src="<%= products[i].cover %>" class="card-img-top">
          <div class="card-body">
            <h4 class="card-title"><%= products[i].name %></h4>
            <p class="card-text"><%= products[i].category %></p>
            <p class="card-text"><%= products[i].price %>$</p>
          </div>
        </div>
      </div>
    </div>
    <% } %>
  </div>

 <!-- pagination -->
  <div class="row">
    <!-- hiển thị phân trang nếu có trang -->
    <% if(pages > 0) { %>
    <nav class="mx-auto">
      <ul class="pagination" style="margin-top: 2rem;">
      
        <!-- FIRST ITEM -->  
        <% if(current == 1) { %> 
      // Nếu thông số http://localhost:3333/news/1/ là 1 thì First sẽ bị disabled
            <li class="page-item disabled">
              <a class="page-link" href="/">First</a>
            </li>
        <% } else { %>
        // Nếu thông số http://localhost:3333/news/2/ từ 2 trở lên thì First sẽ bỏ disabled
            <li class="page-item">
              <a class="page-link" href="/">First</a>
            </li>
        <% } %>
        
        <!-- ITEMS  -->
        <% var i = (Number(current) > 3 ? Number(current) - 2 : 1) %> 
  // Toán tử điều kiện 3 ngôi syntax: < điều_kiện ? giá_trị_1 : giá_trị_2 >
 // Nếu điều_kiện trả về true, toán tử có giá trị giá_trị_1. Ngược lại toán tử có giá trị giá_trị_2.
        <% if(i !== 1) { %>
            <li class="page-item disabled">
              <a class="page-link" href="#">...</a>
            </li>
        <% } %>
        <% for(; i <= (Number(current) + 2) && i <= pages; i++) { %>
        // Sử dụng vòng lặp để lặp qua từng trang và tăng số trang 
        // Trong vòng lặp for có ba biểu thức tùy chọn nên chỉ cần các bạn tách mỗi biểu thức bằng dấu `;` là oke rồi
          <% if(i == current) { %>
            <li class="page-item active">
              <a class="page-link" href="/news/<%= i %>/">
                <%= i %>
              </a>
            </li>
        <% } else { %>
            <li class="page-item">
              <a class="page-link" href="/news/<%= i %>/">
                <%= i %>
              </a>
            </li>
        <% } %>
        <% if (i == Number(current) + 2 && i < pages) { %>
            <li class="page-item disabled">
              <a class="page-link" href="#">...</a>
            </li>
        <% } %>
        <% } %>
        
        <!-- LAST ITEM -->
        <% if(current == pages) { %> 
       // Nếu số current( chỉ số phân trang ) là 6 và tổng số page là 6 thì Last bị disabled
            <li class="page-item disabled">
              <a class="page-link" href="#">
                Last
              </a>
            </li>
        <% } else { %>
        // Ngược lại, thì Last là trang cuối cùng pages ở đây là 6 đồng nghĩa với Last là trang số 6
            <li class="page-item">
              <a class="page-link" href="/news/<%= pages %>/">
                Last
              </a>
            </li>
        <% } %>
      </ul>
    </nav>
    <% } %>
  </div>
</div>


<%- include('../layout/footer'); -%>

Và đây là kết quả khi chúng ta đã hoàn thành pagination cho nó:))

Từ từ mọi người đừng mừng vội còn một việc nữa đó là chả lẻ giờ muốn xem pagination thì phải ghi params /news/1/ vào url như này http://localhost:3333/news/1/ nhìn là biết trải nghiệm người dùng không tốt rồi =))
Bây giờ mình sẽ chuyển pagination ra trang home khi người dùng vào là sẽ thấy xuất hiện phân trang luôn. Đơn giản thôi bây giờ cũng trong file index.rotes.js bạn chỉ cần copy nguyên code từ router.get('/news/:page') sang router.get('/') là được thôi mà.

// home page
router.get('/', (req, res, next)=>{
  let perPage = 16; // số lượng sản phẩm xuất hiện trên 1 page
  let page = req.params.page || 1; 

  Product
    .find() // find tất cả các data
    .skip((perPage * page) - perPage) // Trong page đầu tiên sẽ bỏ qua giá trị là 0
    .limit(perPage)
    .exec((err, products) => {
      Product.countDocuments((err, count) => { // đếm để tính xem có bao nhiêu trang
        if (err) return next(err);
        res.render('product/index_product', {
          products, // sản phẩm trên một page
          current: page, // page hiện tại
          pages: Math.ceil(count / perPage) // tổng số các page
        });
      });
    });
})

Và đây là kết quả khi chúng ta chuyển pagination ra ngoài home page nha:

Vậy là xong rồi nha, các bạn có thể tham khảo code mà mình đã push lên github tại đây nha.

Lời Kết

Vậy Là Xong bài Hướng Dẫn Xây Dựng Pagination Với NodeJS, Express và MongoDB rồi nhé. Mình mong muốn sau bài topic này các bạn có thể biết và hiểu thêm về nodejs, express và biết pagination là gì?, từ topic này các bạn có thể mở rộng thêm ý tưởng và cách làm mới. Có thể tự tay mình làm những project không cần phải quá đặc biệt nhưng nó do chính bạn làm thì cũng coi như là thành quả trong quá trình bạn học được.

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!!