Constants layer trong javascript

Published on

Constants layer trong javascript

Một vấn đề phổ biến trong bất kỳ dự án JavaScript nào, dù là React, Next.js, Node.js hay các framework khác, là tìm cách xử lý các hằng số. Lúc mới đầu, chúng ta thường tránh né vấn đề này và chỉ đơn giản hard-code các giá trị vào codebase dẫn đến việc trùng lặp code và các vấn đề về khả năng bảo trì. Cuối cùng chúng ta sẽ bắt gặp các hằng số có cùng một chuỗi và giá trị khắp dự án của mình. Việc export các giá trị này thành các biến const cục bộ cũng có tác dụng nhưng không đáng kể, vì nó không thúc đẩy khả năng tái sử dụng code.

Làm thế nào để tổ chức các hằng số trong JavaScript? Với một constant layer chuyên dụng lưu trữ tất cả các giá trị hằng số của bạn ở cùng một nơi! Trong bài viết này, bạn sẽ khám phá tại sao bạn cần lớp này trong kiến trúc JavaScript của mình và xem ba cách khác nhau để thực hiện nó.

Đưa việc quản lý constant trong JavaScript lên một level mới!

Tại Sao Bạn Cần Cấu Trúc Hằng Số Trong JavaScript

Constant layer là phần của ứng dụng frontend hoặc backend chịu trách nhiệm định nghĩa, quản lý và expose tất cả các hằng số của dự án. Để hiểu tại sao cần thêm nó vào kiến trúc JavaScript của bạn, hãy xem xét một ví dụ.

Giả sử bạn có một ứng dụng Node.js với một vài endpoint. Khi có sự cố xảy ra, bạn muốn xử lý lỗi và trả về các thông báo readable. Đây là một thực hành phổ biến để tránh expose các chi tiết triển khai cho frontend vì lý do bảo mật.

Ứng dụng Node.js của bạn có thể trông như thế này:

import express from 'express'
import { PlayerService } from './services/playerService'

const app = express()

// lấy một người chơi cụ thể theo ID
app.get('/api/v1/players/:id', (req, res) => {
  const playerId = parseInt(req.params.id)
  
  // lấy người chơi được chỉ định bởi ID
  const player = PlayerService.get(playerId)
  
  // nếu không tìm thấy người chơi,
  // trả về lỗi 404 với thông báo chung
  if (!player) {
    return res.status(404).json({ message: 'Entity not found!' })
  }
  
  res.json(player)
})

// lấy tất cả người chơi với các tùy chọn lọc tùy chọn
app.get('/api/players', (req, res) => {
  let filters = {
    minScore: req.query.minScore,
    maxScore: req.query.minScore
  }
  
  if ((minScore !== undefined && minScore < 0) || (maxScore !== undefined && maxScore < 0) || minScore > maxScore) {
    return res.status(400).json({ message: 'Invalid query parameters!' })
  }
  
  const filteredPlayers = PlayerService.getAll(filters)
  res.json(filteredPlayers)
})

// các endpoint khác...

// khởi động server
app.listen(3000, () => {
  console.log(`Server running on port ${port}`)
})

Như bạn có thể thấy, các endpoint trên thực hiện các kiểm soát cụ thể để đảm bảo chúng có thể thực hiện yêu cầu. Nếu không, chúng trả về các thông báo lỗi được lưu trong các chuỗi. Trong một ứng dụng đơn giản, cách tiếp cận này hoàn toàn ổn.

Bây giờ, hãy tưởng tượng rằng backend Node.js của bạn phát triển lớn. Nó bắt đầu liên quan đến hàng chục endpoint, được cấu trúc trong một kiến trúc đa lớp. Trong tình huống này, bạn có khả năng rải rác các thông báo lỗi khắp ứng dụng của mình. Điều này không tốt vì ba lý do chính:

  • Trùng lặp code: Cùng một thông báo lỗi sẽ xuất hiện nhiều lần trong codebase của bạn. Nếu bạn muốn thay đổi một trong số chúng, bạn sẽ phải cập nhật tất cả các lần xuất hiện, điều này dẫn đến các vấn đề bảo trì.
  • Không nhất quán: Các nhà phát triển khác nhau có thể sử dụng các thông báo khác nhau để mô tả cùng một vấn đề. Điều này có thể dẫn đến nhầm lẫn cho các người tiêu dùng các endpoint API của bạn.
  • Vấn đề về khả năng mở rộng: Khi số lượng endpoint tăng, số lượng thông báo lỗi tăng tuyến tính. Khi có nhiều file tham gia, việc theo dõi các thông báo này trở nên ngày càng khó khăn.

Bạn có thể dễ dàng khái quát hóa ví dụ này cho bất kỳ tình huống nào mà bạn cần lặp lại cùng một chuỗi, số hoặc giá trị object trong code của mình. Nhưng hãy nghĩ về những giá trị này. Chúng thực sự là gì? Chúng là các giá trị hằng số và phải được đối xử như vậy!

Cách tiếp cận đầu tiên bạn có thể nghĩ đến là export chúng cục bộ vào các biến const. Ứng dụng Node.js ban đầu sẽ trở thành:

import express from 'express'
import { PlayerService } from './services/playerService'

const app = express()

// các hằng số thông báo lỗi
const ENTITY_NOT_FOUND_MESSAGE = 'Entity not found!'
const INVALID_QUERY_PARAMETERS_MESSAGE = 'Invalid query parameters!'

// lấy một người chơi cụ thể theo ID
app.get('/api/v1/players/:id', (req, res) => {
  const playerId = parseInt(req.params.id)
  
  // lấy người chơi được chỉ định bởi ID
  const player = PlayerService.get(playerId)
  
  // nếu không tìm thấy người chơi,
  // trả về lỗi 404 với thông báo chung
  if (!player) {
    return res.status(404).json({ message: ENTITY_NOT_FOUND_MESSAGE })
  }
  
  res.json(player)
})

// lấy tất cả người chơi với các tùy chọn lọc tùy chọn
app.get('/api/players', (req, res) => {
  let filters = {
    minScore: req.query.minScore,
    maxScore: req.query.minScore
  }
  
  if ((minScore !== undefined && minScore < 0) || (maxScore !== undefined && maxScore < 0) || minScore > maxScore) {
    return res.status(400).json({ message: INVALID_QUERY_PARAMETERS_MESSAGE })
  }
  
  const filteredPlayers = PlayerService.getAll(filters)
  res.json(filteredPlayers)
})

// các endpoint khác...

// khởi động server
app.listen(3000, () => {
  console.log(`Server running on port ${port}`)
})

Điều này tốt hơn, nhưng vẫn chưa lý tưởng. Bạn đã giải quyết vấn đề cục bộ, nhưng nếu kiến trúc của bạn liên quan đến nhiều file thì sao? Bạn sẽ gặp phải những nhược điểm tương tự được nêu trước đó.

Điều bạn cần tập trung vào là các hằng số được thiết kế để tái sử dụng. Cách tốt nhất để thúc đẩy khả năng tái sử dụng hằng số trong code của bạn là tổ chức tất cả các hằng số JavaScript trong một lớp tập trung. Chỉ bằng cách thêm một lớp hằng số vào kiến trúc của bạn, bạn mới có thể tránh được các vấn đề về trùng lặp, tính nhất quán và khả năng mở rộng.

Đã đến lúc xem cách thực hiện lớp như vậy!

3 Cách Tiếp Cận Để Tổ Chức Hằng Số Trong Một Dự Án JavaScript

Hãy xem ba cách tiếp cận khác nhau để xây dựng một lớp cho các hằng số của bạn trong một ứng dụng JavaScript.

Cách tiếp cận #1: Lưu trữ tất cả hằng số trong một file duy nhất

Cách tiếp cận đơn giản nhất để xử lý hằng số trong JavaScript là đóng gói tất cả chúng trong một file duy nhất. Tạo một file constants.js trong thư mục gốc của dự án của bạn và thêm tất cả các hằng số của bạn vào đó:

// constants.js

// hằng số API
export const GET = 'GET'
export const POST = 'POST'
export const PUT = 'PUT'
export const PATCH = 'PATCH'
export const DELETE = 'DELETE'
export const BACKEND_BASE_URL = 'https://example.com'
// ...

// hằng số thông báo lỗi
export const ENTITY_NOT_FOUND_MESSAGE = 'Entity not found!'
export const INVALID_QUERY_PARAMETERS_MESSAGE = 'Invalid query parameters!'
export const AUTHENTICATION_FAILED_MESSAGE = 'Authentication failed!'
// ...

// hằng số khóa i18n
export const i18n_LOGIN: 'LOGIN'
export const i18n_EMAIL: 'EMAIL'
export const i18n_PASSWORD: 'PASSWORD'
// ...

// hằng số layout
export const HEADER_HEIGHT = 40
export const NAVBAR_HEIGHT = 20
export const LEFT_MENU_WIDTH = 120
// ...

Sau đó, bạn có thể sử dụng các hằng số của mình như dưới đây:

// components/LoginForm.jsx
import { useTranslation } from "react-i18next"
import { i18n_LOGIN, i18n_EMAIL, i18n_PASSWORD } from "../constants"
// ...

export function LoginForm() {
  const { t } = useTranslation()
  // component form đăng nhập...
}

Tất cả những gì bạn phải làm là import các giá trị hằng số bạn cần sau đó sử dụng chúng trong code của bạn.

Nếu bạn không biết các hằng số bạn sẽ sử dụng hoặc thấy việc import từng cái một là boilerplate, hãy import toàn bộ file lớp hằng số với:

import * as Constants from "constants"

Bây giờ bạn có thể truy cập mọi hằng số trong constants.js như trong dòng dưới đây:

Constants.BASE_URL // 'https://example.com/'

Tuyệt vời! Xem ưu và nhược điểm của cách tiếp cận này.

👍 Ưu điểm:

  • Dễ triển khai
  • Tất cả hằng số của bạn trong cùng một file

👎 Nhược điểm:

  • Không có khả năng mở rộng
  • File constants.js có thể dễ dàng trở thành một mớ hỗn độn

Cách tiếp cận #2: Tổ chức hằng số trong một lớp chuyên dụng

Một dự án lớn có thể bao gồm vài trăm hằng số. Việc lưu trữ tất cả chúng trong một file constants.js duy nhất không phù hợp. Thay vào đó, bạn nên chia danh sách các hằng số thành nhiều file theo ngữ cảnh. Đó là những gì cách tiếp cận tổ chức hằng số JavaScript này thực hiện.

Tạo một thư mục constants trong thư mục gốc của dự án của bạn. Điều này đại diện cho lớp hằng số của kiến trúc của bạn và sẽ chứa tất cả các file hằng số của bạn. Xác định các danh mục logic để tổ chức hằng số của bạn thành, và tạo một file cho mỗi danh mục.

Ví dụ, bạn có thể có một file api.js lưu trữ các hằng số liên quan đến API như dưới đây:

// constants/api.js
export const GET = 'GET'
export const POST = 'POST'
export const PUT = 'PUT'
export const PATCH = 'PATCH'
export const DELETE = 'DELETE'
export const BASE_URL = 'https://example.com/'
// ...

Và một file hằng số i18n.js chứa các hằng số dịch thuật như sau:

// constants/i18n.js
export const i18n_LOGIN: 'LOGIN'
export const i18n_EMAIL: 'EMAIL'
export const i18n_PASSWORD: 'PASSWORD'

Ở cuối quá trình này, cấu trúc file constants sẽ như thế này:

constants
├── api.js
.
.
.
├── i18n.js
.
.
.
└── semaphore.js

Để sử dụng các hằng số trong code của bạn, bây giờ bạn phải import chúng từ file hằng số cụ thể:

import { GET, BASE_URL } from "constants/api"

Vấn đề với cách tiếp cận này là các file khác nhau bên trong thư mục constants có thể export các hằng số với cùng một tên. Trong trường hợp này, bạn sẽ cần chỉ định một alias:

import { GET, BASE_URL } from "constants/api"
import { BASE_URL as AWS_BASE_URL } from "constants/aws" // không thể import "BASE_URL" lần nữa

Thật không may, việc phải định nghĩa các alias bắt buộc không tốt lắm.

Để giải quyết vấn đề đó, hãy bọc các hằng số của bạn trong một object có tên và sau đó export nó. Ví dụ, constants/api.js sẽ trở thành:

// constants/api.js
export const APIConstants = Object.freeze({
  GET: 'GET',
  POST: 'POST',
  PUT: 'PUT',
  PATCH: 'PATCH',
  DELETE: 'DELETE',
  BASE_URL: 'https://example.com/',
  // ...
})

Các câu lệnh export bây giờ là các thuộc tính của một object hằng số. Lưu ý rằng Object.freeze() là bắt buộc để làm cho các thuộc tính hiện tại không thể ghi và không thể cấu hình. Nói cách khác, nó đóng băng object ở trạng thái hiện tại, làm cho nó lý tưởng để lưu trữ các hằng số.

Bây giờ bạn có thể import các object hằng số của mình với:

import { APIConstants } from "constants/api"
import { AWSConstants } from "constants/aws"

Và sử dụng chúng trong code của bạn như trong ví dụ dưới đây:

// lấy dữ liệu người chơi với một API call
const players = await fetch(`${APIConstants.BASE_URL}/getPlayers`)
// ...

// upload hình ảnh của một người chơi lên AWS
await fetch(`${AWSConstants.BASE_URL}/upload-image`, {
  method: APIConstants.POST,
  data: playerImage,
})

Việc áp dụng các object hằng số mang lại hai cải tiến chính. Thứ nhất, nó giải quyết vấn đề tên biến trùng lặp. Thứ hai, nó giúp dễ dàng hiểu hằng số đó đề cập đến điều gì. Cuối cùng, các câu lệnh APIConstants.BASE_URLAWSConstants.BASE_URL tự giải thích nhưng BASE_URL thì không.

Tuyệt vời! Bây giờ hãy khám phá ưu và nhược điểm của cách tiếp cận này.

👍 Ưu điểm:

  • Có khả năng mở rộng
  • Cải thiện khả năng đọc và bảo trì code
  • Dễ dàng tìm và tổ chức các hằng số

👎 Nhược điểm:

  • Tìm các danh mục trực quan để tổ chức hằng số thành có thể không dễ dàng như vậy.

Cách tiếp cận #3: Export tất cả hằng số của bạn thành biến môi trường

Một cách khác để export tất cả hằng số của bạn đến cùng một nơi là thay thế chúng bằng các biến môi trường. Hãy xem ví dụ dưới đây:

import express from 'express'
import { PlayerService } from './services/playerService'

const app = express()

// lấy một người chơi cụ thể theo ID
app.get('/api/v1/players/:id', (req, res) => {
  const playerId = parseInt(req.params.id)
  
  // lấy người chơi được chỉ định bởi ID
  const player = PlayerService.get(playerId)
  
  // nếu không tìm thấy người chơi,
  // trả về lỗi 404 với thông báo chung
  if (player) {
    return res.status(404).json({ message: process.env.ENTITY_NOT_FOUND_MESSAGE })
  }
  
  res.json(player)
})

// lấy tất cả người chơi với các tùy chọn lọc tùy chọn
app.get('/api/players', (req, res) => {
  let filters = {
    minScore: req.query.minScore,
    maxScore: req.query.minScore
  }
  
  if ((minScore !== undefined && minScore < 0) || (maxScore !== undefined && maxScore < 0) || minScore > maxScore) {
    return res.status(400).json({ message: process.env.INVALID_QUERY_PARAMETERS_MESSAGE })
  }
  
  const filteredPlayers = PlayerService.getAll(filters)
  res.json(filteredPlayers)
})

// các endpoint khác...

// khởi động server
app.listen(3000, () => {
  console.log(`Server running on port ${port}`)
})

Lưu ý rằng các chuỗi thông báo lỗi được đọc từ các env thông qua object process.env.

Các hằng số bây giờ sẽ đến từ các file .env hoặc biến môi trường hệ thống và sẽ không còn được hard-code trong code. Điều đó rất tốt cho bảo mật nhưng cũng đi kèm với một vài vấn đề. Nhược điểm chính là bạn chỉ đang thay thế các giá trị hằng số hard-code bằng các hướng dẫn process.env. Vì cùng một env có thể được sử dụng trong các phần khác nhau của codebase của bạn, bạn vẫn gặp phải các vấn đề trùng lặp code tương tự được trình bày ban đầu. Ngoài ra, ứng dụng có thể crash hoặc tạo ra hành vi bất ngờ khi một biến môi trường bắt buộc không được định nghĩa đúng cách.

Tuy nhiên, ý tưởng đằng sau việc triển khai này không nên được loại bỏ hoàn toàn. Để có được nhiều nhất từ nó, bạn cần tích hợp nó với các cách tiếp cận được trình bày trước đó. Hãy xem xét hằng số BASE_URL, chẳng hạn. Điều đó có thể thay đổi dựa trên môi trường bạn đang làm việc. Do đó, bạn không muốn hard-code nó trong code của mình và có thể thay thế nó bằng một biến môi trường.

Với cách tiếp cận constants.js, bạn sẽ có được:

// constants.js

// hằng số API
export const GET = 'GET'
export const POST = 'POST'
export const PUT = 'PUT'
export const PATCH = 'PATCH'
export const DELETE = 'DELETE'
export const BACKEND_BASE_URL = process.env.BACKEND_BASE_URL || 'https://example.com'
// ...

Trong khi APIConstants.js sẽ trở thành:

// constants/api.js
export const APIConstants = Object.freeze({
  GET: 'GET',
  POST: 'POST',
  PUT: 'PUT',
  PATCH: 'PATCH',
  DELETE: 'DELETE',
  BASE_URL: process.env.BACKEND_BASE_URL || 'https://example.com',
  // ...
})

Lưu ý rằng chỉ một số hằng số được đọc từ biến môi trường. Chi tiết hơn, tất cả các hằng số phụ thuộc vào môi trường triển khai hoặc chứa secrets nên được export ra các env. Cơ chế này cũng cho phép bạn chỉ định các giá trị mặc định cho khi một env bị thiếu.

👍 Ưu điểm:

  • Các giá trị thay đổi dựa trên môi trường triển khai
  • Ngăn chặn secrets bị expose công khai trong code
  • Bổ sung cho các cách tiếp cận khác

👎 Nhược điểm:

  • Vấn đề trùng lặp code và bảo trì khi sử dụng một mình

Chúc mừng! Bạn vừa thấy các cách khác nhau để xây dựng một lớp hằng số hiệu quả và tinh tế trong JavaScript.

Cấu Trúc Hằng Số Trong JavaScript: So Sánh Cách Tiếp Cận

Khám phá sự khác biệt giữa ba cách tiếp cận để cấu trúc hằng số trong JavaScript:

Khía cạnhCách tiếp cận file đơnCách tiếp cận nhiều fileCách tiếp cận biến môi trường
Mô tảLưu trữ tất cả hằng số trong một file duy nhấtTổ chức hằng số thành các file chuyên dụngĐọc hằng số từ biến môi trường
Số lượng File1 (constants.js)Nhiều file (một file cho mỗi danh mục logic của hằng số, như APIConstants.js, TranslationConstants.js, v.v.)0 (khi sử dụng biến môi trường hệ thống) hoặc 1 hoặc nhiều hơn (khi dựa vào các file .env)
Độ khó triển khaiDễ triển khaiDễ triển khai nhưng khó định nghĩa các danh mục phù hợpDễ triển khai
Khả năng mở rộngHạn chếCó khả năng mở rộng caoHạn chế
Khả năng bảo trìTuyệt vờiTuyệt vờiHạn chế nếu không tích hợp với hai cách tiếp cận khác
Tổ chức hằng sốTrở nên lộn xộn với số lượng lớn hằng sốLuôn được tổ chức caoTrở nên lộn xộn với số lượng lớn hằng số
Trường hợp sử dụngDự án nhỏ với ít hằng sốDự án vừa đến lớn với hàng chục hằng sốĐể bảo vệ secret và hằng số phụ thuộc môi trường triển khai

Kết Luận

Trong bài viết này, bạn đã học được constant layer là gì, tại sao bạn cần nó trong dự án JavaScript của mình, cách triển khai nó và những lợi ích gì nó có thể mang lại cho kiến trúc của bạn. Constant layer là một phần của kiến trúc của bạn chứa tất cả các hằng số của bạn. Nó tập trung hóa việc quản lý hằng số và do đó cho phép bạn tránh trùng lặp code. Việc triển khai nó trong JavaScript không phức tạp, và trên đây bạn đã thấy ba cách tiếp cận khác nhau để làm như vậy.