Skip to content

Latest commit

 

History

History
312 lines (263 loc) · 14.1 KB

File metadata and controls

312 lines (263 loc) · 14.1 KB

Kiến Trúc Dự Án Product Management (Node.js + Express + Pug + MongoDB)

Tài liệu này đặc tả chi tiết cấu trúc thư mục, luồng hoạt động, các helper, mixin, và thiết lập cấu hình của dự án Product Management. Đây là tài liệu quy chuẩn giúp hỗ trợ cấu trúc và định hình lại các dự án Node.js tương tự.


1. Cấu Trúc Thư Mục Tổng Quan

product-management/
├── .env                        # Biến môi trường (PORT, MONGO_URL)
├── package.json                # Định nghĩa dependencies & scripts khởi chạy
├── index.js                    # File khởi tạo và cấu hình Express app (Entry Point)
├── config/                     # Cấu hình kết nối DB & Hệ thống
│   ├── database.js             # Mongoose kết nối MongoDB
│   └── system.js               # Biến cấu hình (ví dụ: prefixAdmin)
├── helpers/                    # Các hàm bổ trợ xử lý dữ liệu chung
│   ├── filterStatus.js         # Xử lý trạng thái nút lọc bộ lọc
│   ├── pagination.js           # Xử lý tính toán phân trang
│   └── search.js               # Xử lý từ khóa tìm kiếm (Regex)
├── models/                     # Lớp Mongoose Schemas & Models đại diện cho DB
│   └── products.models.js      # Model Sản phẩm (Schema cấu trúc dữ liệu)
├── routes/                     # Định nghĩa các tuyến đường (Endpoints)
│   ├── admin/                  # Routes dành cho Trang Quản Trị
│   │   ├── index.routes.js     # Tổng hợp và cấu hình prefix routes admin
│   │   ├── dashboard.routes.js # Route dashboard admin
│   │   └── products.routes.js  # Route quản lý sản phẩm admin
│   └── client/                 # Routes dành cho Giao Diện Người Dùng
│       ├── index.routes.js     # Tổng hợp routes client
│       ├── home.routes.js      # Route trang chủ client
│       └── product.routes.js   # Route danh sách sản phẩm client
├── controllers/                # Logic nghiệp vụ xử lý dữ liệu và render view
│   ├── admin/                  # Controllers trang admin
│   │   ├── dashboard.controller.js
│   │   └── products.controller.js
│   └── client/                 # Controllers trang client
│       ├── home.controller.js
│       └── products.controller.js
├── views/                      # Template động sử dụng Pug
│   ├── admin/                  # Giao diện quản trị
│   │   ├── layouts/            # Layouts mẫu chung (default.pug)
│   │   ├── mixins/             # Các khối giao diện tái sử dụng (Pagination, Lọc, Thay đổi hàng loạt)
│   │   ├── pages/              # Giao diện chi tiết các trang
│   │   └── partials/           # Các phần giao diện cố định (Header, Sider, Footer)
│   └── client/                 # Giao diện phía khách hàng (tương tự admin)
└── public/                     # Tài nguyên tĩnh phục vụ phía client
    ├── admin/                  # Styles & Scripts của admin
    └── (client static files)   # Styles & Scripts của client

2. Chi Tiết Các File Cốt Lõi

2.1 Cấu Hình Khởi Chạy (index.js)

Khởi tạo Express app, cấu hình cổng, tích hợp công cụ template Pug, xử lý dữ liệu gửi lên (body-parser), ghi đè HTTP method (method-override), kết nối Database, cấu hình thư mục tĩnh và kết nối hệ thống Routes.

require("dotenv").config();
const express = require("express");
const database = require("./config/database");
const adminRoutes = require("./routes/admin/index.routes.js");
const clientRoutes = require("./routes/client/index.routes.js");
const systemConfig = require("./config/system");
const bodyParser = require("body-parser");
const methodOverride = require("method-override");

const app = express();
const port = process.env.PORT;

// Cấu hình Parser & Method Override để hỗ trợ PATCH/DELETE từ Form
app.use(bodyParser.urlencoded({ extended: false }));
app.use(methodOverride("_method"));

// Kết nối Cơ sở dữ liệu MongoDB
database.connect();

// Cấu hình View Engine (Pug)
app.set("views", "./views");
app.set("view engine", "pug");

// Cấu hình biến dùng chung trong toàn bộ các file Pug template
app.locals.prefixAdmin = systemConfig.prefixAdmin;

// Cấu hình thư mục chứa file tĩnh
app.use(express.static("public"));

// Kết nối Routes
clientRoutes(app);
adminRoutes(app);

app.listen(port, () => {
  console.log(`Ứng dụng đang chạy trên cổng ${port}`);
});

2.2 Quản Lý Routes Đa Cấp (Multi-level Routing)

Hệ thống route chia rõ ràng theo hai phân hệ adminclient giúp quản lý phân quyền và nhóm đường dẫn tốt hơn.

  • Routes Tổng Admin (routes/admin/index.routes.js):

    const dashboardRoutes = require("./dashboard.routes");
    const systemConfig = require("../../config/system");
    const productsRoutes = require("./products.routes");
    
    module.exports = (app) => {
        app.use(`${systemConfig.prefixAdmin}/dashboard`, dashboardRoutes);
        app.use(`${systemConfig.prefixAdmin}/products`, productsRoutes);
    };
  • Routes Chi Tiết Sản Phẩm Admin (routes/admin/products.routes.js):

    const express = require("express");
    const router = express.Router();
    const controller = require("../../controllers/admin/products.controller.js");
    
    router.get("/", controller.index);
    router.patch("/change-status/:status/:id", controller.changeStatus); // Đổi trạng thái đơn lẻ
    router.patch("/change-multi", controller.changeMulti);             // Đổi trạng thái hàng loạt
    
    module.exports = router;

3. Kiến Trúc Bộ Helper Xử Lý Dữ Liệu (Helpers)

Bộ Helpers giúp giảm tải logic lặp đi lặp lại trong các Controller để lọc, tìm kiếm và phân trang dữ liệu.

3.1 Bộ Lọc Trạng Thái (helpers/filterStatus.js)

Tạo mảng trạng thái nút lọc, tự động gán class active cho nút trạng thái hiện tại dựa trên query.status.

module.exports = (query) => {
    let filterStatus = [
        { name: "Tất cả", status: "", class: "" },
        { name: "Hoạt động", status: "active", class: "" },
        { name: "Dừng hoạt động", status: "inactive", class: "" }
    ];
    if (query.status) {
        const index = filterStatus.findIndex(item => item.status == query.status);
        filterStatus[index].class = "active";
    } else {
        filterStatus[0].class = "active";
    }
    return filterStatus;
};

3.2 Phân Trang Dữ Liệu (helpers/pagination.js)

Tính toán chỉ số skip để truy vấn MongoDB và số lượng trang (totalPage).

module.exports = (objectPagination, query, countProducts) => {
    if (query.page) {
        objectPagination.currentPage = parseInt(query.page);
    }
    objectPagination.skip = (objectPagination.currentPage - 1) * objectPagination.limitItems;
    objectPagination.totalPage = Math.ceil(countProducts / objectPagination.limitItems);
    return objectPagination;
};

3.3 Tìm Kiếm (helpers/search.js)

Khởi tạo biểu thức Regex tìm kiếm không phân biệt chữ hoa/thường (i) dựa trên từ khóa người dùng nhập.

module.exports = (query) => {
    let objectSearch = { keyword: "" };
    if (query.keyword) {
        objectSearch.keyword = query.keyword;
        objectSearch.regex = new RegExp(objectSearch.keyword, "i");
    }
    return objectSearch;
};

4. Tương Tác Giữa Giao Diện Pug & Logic JavaScript

Dự án sử dụng cơ chế Form ẩn (Hidden Form) kết hợp Vanilla JS để thực hiện các yêu cầu thay đổi dữ liệu mà không cần tải lại toàn trang thủ công.

4.1 Cập Nhật Trạng Thái Đơn Lẻ (Single Toggle Status)

  • Ý tưởng: Khi click nút Trạng thái, JS lấy thuộc tính current-statusdata-id, tính toán trạng thái mới, điền vào action của Form ẩn và gửi đi.

  • HTML Pug (views/admin/pages/products/index.pug):

    a(
      href="javascript:;"
      button-change-status
      current-status=item.status
      data-id=item.id
    ) #{item.status == "active" ? "Hoạt động" : "Dừng hoạt động"}
    
    // Form ẩn dùng chung cho việc gửi PATCH request thay đổi trạng thái
    form(
        action=""
        method="POST"
        id="form-change-status"
        data-path=`${prefixAdmin}/products/change-status`
    )
  • JavaScript Client (public/admin/js/product.js):

    const buttonChangeStatus = document.querySelectorAll("[button-change-status]");
    buttonChangeStatus.forEach(button => {
        const formChangeStatus = document.querySelector("#form-change-status");
        const path = formChangeStatus.getAttribute("data-path");
    
        button.addEventListener("click", () => {
            const status = button.getAttribute("current-status");
            const id = button.getAttribute("data-id");
            let newStatus = status == "active" ? "inactive" : "active";
    
            // Ghi đè phương thức POST sang PATCH nhờ _method=PATCH và method-override
            formChangeStatus.action = path + `/${newStatus}/${id}?_method=PATCH`;
            formChangeStatus.submit();
        });
    });
  • Xử lý Controller (controllers/admin/products.controller.js):

    module.exports.changeStatus = async (req, res) => {
        const status = req.params.status;
        const id = req.params.id;
        await Product.updateOne({ _id: id }, { status: status });
        res.redirect(req.get("Referer") || "/admin/products");
    };

4.2 Cập Nhật Trạng Thái Hàng Loạt (Bulk Update Status)

  • Ý tưởng: Quản trị viên chọn các sản phẩm qua checkbox, chọn hành động trong thẻ Select, nhấn áp dụng. JS sẽ thu thập danh sách _id được chọn, ngăn chặn submit mặc định, gán chuỗi ID ngăn cách bởi dấu phẩy vào input ẩn của form rồi thực hiện submit.

  • Mixin Giao Diện Form Multi (views/admin/mixins/formChangeMulti.pug):

    mixin form-change-multi(path)    
        form(action=path, method="POST", form-change-multi)
            .d-flex.align-items-start 
                .form-group 
                    select(name="type" class="form-control")
                        option(value="active") Hoạt động 
                        option(value="inactive") Dừng hoạt động 
                .form-group 
                    input(
                        type="text"
                        name="ids"
                        class="form-control d-none"
                    )
                button(type="submit" class="btn btn-primary") Áp dụng
  • JavaScript Client (public/admin/js/scripts.js):

    // 1. Quản lý trạng thái Checkbox Toàn bộ (Check All) và các Checkbox Con
    const checkboxMulti = document.querySelector("[checkbox-multi]");
    if (checkboxMulti) {
        const inputCheckAll = checkboxMulti.querySelector("input[name='checkall']");
        const inputIds = checkboxMulti.querySelectorAll("input[name='id']");
    
        inputCheckAll.addEventListener("change", () => {
            inputIds.forEach(input => { input.checked = inputCheckAll.checked; });
        });   
        
        inputIds.forEach(input => {
            input.addEventListener("change", () => {
                const countChecked = checkboxMulti.querySelectorAll('input[name="id"]:checked').length;
                inputCheckAll.checked = (countChecked === inputIds.length);
            });
        });
    }
    
    // 2. Thu thập IDs khi Submit Form thay đổi nhiều phần tử
    const formChangeMulti = document.querySelector("[form-change-multi]");
    if (formChangeMulti) {
        formChangeMulti.addEventListener("submit", (e) => {
            e.preventDefault();
            const inputChecked = document.querySelectorAll("input[name='id']:checked");
            if (inputChecked.length > 0) {
                let ids = [];
                const inputIds = formChangeMulti.querySelector("input[name='ids']");
                inputChecked.forEach(input => { ids.push(input.value); });
                inputIds.value = ids.join(",");
                formChangeMulti.submit();
            } else {
                alert("Vui lòng chọn ít nhất 1 sản phẩm!");
            }
        });
    }
  • Xử lý Controller (controllers/admin/products.controller.js):

    module.exports.changeMulti = async (req, res) => {
        const type = req.body.type;
        const ids = req.body.ids.split(",");
        await Product.updateMany({ _id: { $in: ids } }, { status: type });
        res.redirect(req.get("Referer") || "/admin/products");
    };

5. Các Điểm Cần Lưu Ý Khi Nhân Bản Cấu Trúc Dự Án

Khi cấu trúc lại một dự án mới dựa trên mô hình này, hãy chắc chắn tuân thủ các bước thiết lập cốt lõi:

  1. Thiết lập package.json và npm packages: Cài đặt các gói phụ thuộc chính: express, mongoose, pug, dotenv, body-parser, method-overridenodemon cho môi trường phát triển.
  2. Kích hoạt middleware cần thiết trong entrypoint: Đừng quên khai báo body-parser (hoặc express.urlencoded()) và method-override trước khi định tuyến routes trong file chạy chính (ví dụ index.js).
  3. Cấu hình Form Method khi ghi đè PATCH/DELETE: HTML Form phải thiết lập method="POST" và gán query parameter ?_method=PATCH ở hành động gửi đi để Express có thể ghi đè sang PATCH request.
  4. Phân bổ View & Partial hợp lý: Sử dụng cơ chế kế thừa (extends) của Pug kết hợp với include các phần tử giao diện như sidebar (sider.pug), header (header.pug), footer (footer.pug) để giao diện dễ bảo trì và mở rộng.