Tôi viết cái package @ltv/env như thế nào - phần 2
Tổng quan
Sau bài viết trước, tôi đã giới thiệu về cách tôi viết package @ltv/env
và cách tôi sử dụng nó trong dự án của mình. Trong bài viết này, tôi sẽ giới thiệu về cách tôi optimize package này để nó có thể sử dụng được trong nhiều dự án hơn và phù hợp với TypeScript hơn.
Bài viết gốc: https://lucduong.me/p/toi-viet-cai-package-ltvenv-nhu-the-nao-p2
Các vấn đề cần giải quyết
1. Không phải lúc nào cũng sử dụng dotenv
dotenv
là một công cụ giúp ta quản lý các biến môi trường trong ứng dụng bằng cách lưu trữ chúng trong một tệp định dạng plain text, sau đó đọc tệp này và đặt các biến môi trường tương ứng khi chạy ứng dụng.Tuy nhiên, khi sử dụng dotenv ở
production
, có một số rủi ro tiềm ẩn như sau:
Bảo mật: Nếu tệp
.env
chứa thông tin nhạy cảm như mật khẩu hoặc khóa bí mật, nó có thể bị lộ khi triển khai ứng dụng ởproduction
.Hiệu suất: Đọc tệp
.env
có thể làm chậm hiệu suất của ứng dụng, đặc biệt là khi có nhiều biến môi trường.Quản lý biến môi trường: Sử dụng dotenv có thể làm cho việc quản lý các biến môi trường trở nên phức tạp hơn, đặc biệt là khi có nhiều môi trường (ví dụ: staging, production, development).
Trong môi trường production, thay vì sử dụng dotenv, nên sử dụng các biến môi trường được cấu hình trực tiếp trên server. Điều này giúp đảm bảo bảo mật và hiệu suất, và giúp quản lý biến môi trường dễ dàng hơn.
Ở phiên bản trước của package này tôi có sử dụng dotenv
để load các biến môi trường. Tuy nhiên, sau khi sử dụng trong một số dự án, tôi đã nhận ra rằng dotenv
không phải lúc nào cũng phù hợp với mọi dự án. Đặc biệt là khi dự án của bạn sẽ được deploy lên môi trường production
, dotenv
sẽ không phù hợp với môi trường này.
Thực ra cách mà dotenv
hoạt động là rất đơn giản, nó chỉ là một module đơn giản, nó sẽ đọc tệp .env
và đặt các biến môi trường tương ứng. Sau khi load các biến môi trường, nó sẽ thêm process.env
vào require.cache
để các module sau có thể sử dụng được.
Nhưng khi sử dụng dotenv
trong môi trường production
, nó sẽ làm chậm hiệu suất của ứng dụng, đặc biệt là khi có nhiều biến môi trường. Điều này là không mong muốn, vì nó sẽ làm chậm hiệu suất của ứng dụng.
Trong môi trường production
, nên sử dụng các biến môi trường được cấu hình trực tiếp trên server. Điều này giúp đảm bảo bảo mật và hiệu suất, và giúp quản lý biến môi trường dễ dàng hơn.
2. Nếu env
không được set thì type của env
sẽ là gì?
Hãy xem ví dụ sau:
import env from '@ltv/env'
const PORT = env.int('PORT')
Trường hợp này, chúng ta đang muốn lấy PORT
từ environment
, tuy nhiên chúng ta không set default value cho PORT
, nên nếu PORT
không được set thì env.int('PORT')
sẽ trả về undefined
. Điều này sẽ gây ra lỗi khi chúng ta sử dụng PORT
. VD:
import env from '@ltv/env'
const PORT = env.int('PORT')
app.listen(PORT)
Kiểu dữ liệu của PORT
là number
, nhưng
Ở đây, nếu PORT
không được set thì app.listen(PORT)
sẽ gây ra lỗi.
Tuy nhiên, điều chúng ta mong muốn ở đây không phải là sau khi chạy thì mới biết lỗi, mà là ngay khi development, chúng ta có thể biết được lỗi tiềm ẩn ở đây là gì.
Chính vì vậy, chúng ta cần phải mô tả rõ ràng kiểu dữ liệu của env
để khi chúng ta sử dụng env
mà không set default value thì nó sẽ báo lỗi ngay lập tức.
-> Ở trường hợp trên, kiểu dữ liệu của PORT
sẽ là number | undefined
. Điều này sẽ giúp chúng ta biết được nếu PORT
không được set thì app.listen(PORT)
sẽ gây ra lỗi. (Thực ra là PORT
khả năng sẽ là undefined
).
-> Tôi cần sửa lại: env.int('PORT')
sẽ trả về number | undefined
thay vì number
.
Lúc này, vấn đề lại phát sinh, nếu tôi set default value cho PORT
thì kiểu dữ liệu của PORT
không thể là number | undefined
nữa, vì nó sẽ luôn luôn là number
.
Vậy làm sao để tôi có thể mô tả rõ ràng kiểu dữ liệu của env
để khi chúng ta sử dụng env
mà không set default value thì nó sẽ báo lỗi ngay lập tức, và khi set default value thì nó sẽ luôn luôn là kiểu dữ liệu đó?
3. Sử dụng nhiều dependencies quá
Mục đích của ứng dụng rất đơn giản là để load các biến môi trường, nhưng tôi lại sử dụng nhiều dependencies quá.
Tôi sử dụng lodash để check xem fields có tồn tại trong object hay không, hoặc merge object cũng dùng lodash, nhưng tôi thấy rằng lodash quá nặng, nó có nhiều tính năng không cần thiết cho package này. Vì vậy, tôi đã thay thế lodash.has
và lodash.merge
bằng cách viết lại bằng tay.
VD: lodash.has
:
/**
* Checks if `key` is a direct property of `object`.
*
* @param {Object} object The object to query.
* @param {string} key The key to check.
* @returns {boolean} Returns `true` if `key` exists, else `false`.
* @example
*
* const object = { 'a': { 'b': 2 } }
* const other = create({ 'a': create({ 'b': 2 }) })
*
* has(object, 'a')
* // => true
*
* has(other, 'a')
* // => false
*/
export function has<T>(object: T, key: PropertyKey): boolean {
return object != null && hasOwnProperty.call(object, key)
}
Hoặc lodash.trim
:
/**
* Removes leading and trailing whitespace or specified characters from `string`.
*
* @param {string} [string=''] The string to trim.
* @param {string} [chars=whitespace] The characters to trim.
* @returns {string} Returns the trimmed string.
* @example
*
* trim(' abc ')
* // => 'abc'
*
* trim('-_-abc-_-', '_-')
* // => 'abc'
*/
export function trim(str: string, chars?: string) {
if (str && chars === undefined) {
return str.trim()
}
if (!str || !chars) {
return str || ''
}
const strSymbols = stringToArray(str)
const chrSymbols = stringToArray(chars)
let start = 0
let end = strSymbols.length - 1
while (chrSymbols.includes(str[start])) {
start++
}
while (chrSymbols.includes(str[end])) {
end--
}
return strSymbols.slice(start, end + 1).join('')
}
Bắt tay vào refactor code và upgarde lên version mới
1. Remove dotenv
và lodash
dependency
Quá đơn giản, chỉ cần xóa ra khỏi package.json
và yarn.lock
là xong.
Cách khác là chạy lệnh yarn remove dotenv lodash
để xóa cả 2 dependencies này.
2. Refactor code
Phần số 1 chỉ là làm màu cho có chứ phần 2 này mới là quan trọng. Phần này sẽ tập trung giải quyết vấn đề làm sao để có thể mô tả rõ ràng kiểu dữ liệu của env
để khi chúng ta sử dụng env
mà không set default value thì nó sẽ báo lỗi ngay lập tức, và khi set default value thì nó sẽ luôn luôn là kiểu dữ liệu đó.
Trước khi refactor, function như thế nào?
string<string | undefined>(key: string, defaultValue?: string) {
const rtnValue = has(process.env, key) ? process.env[key] : defaultValue
return rtnValue
},
Function khá đơn giản. Nhận vào key
và defaultValue
(optional), nếu key
tồn tại trong process.env
thì trả về process.env[key]
, còn không thì trả về defaultValue
.
Và type của rtnValue
là string | undefined
.
Như mình đã mô tả ở trên, giá trị trả về luôn là string
hoặc undefined
, mặc dù có set default value hay không. Điều này khá bất tiện khi sử dụng, khi ta luôn phải cast kiểu dữ liệu trước khi sử dụng. VD:
const host = env.int('HOST', 'localhost') as string
Mặc dù biết rất rõ nếu HOST
không được set thì nó sẽ trả về localhost
, nhưng vẫn phải cast kiểu dữ liệu.
Nếu nhìn kỹ hơn, thì ta thấy giá trị trả về của rtnValue
sẽ phụ thuộc vào giá trị của defaultValue
. Nếu defaultValue
là undefined
thì rtnValue
sẽ là string | undefined
, còn nếu defaultValue
là string
thì rtnValue
sẽ là string
.
Khá là hợp lý, có nghĩa là bây giờ chúng ta chỉ cần kiểm tra xem defaultValue
có được set hay không, nếu được set thì rtnValue
sẽ luôn luôn là string
, còn không thì rtnValue
sẽ là string | undefined
.
Tuy nhiên, làm sao mà kiểm tra điều kiện cho type
được? Vì type
không phải là một biến, nó chỉ là một mô tả cho biến. Vậy làm sao để mô tả được điều kiện cho type
?
- Đầu tiên làm sao để set return type của function là
string
hoặcstring | undefined
?
Để làm được điều này, chúng ta sẽ sử dụng conditional type
của Typescript.
export type StringOrUndefined<T extends undefined | string> = T extends string ? string : string | undefined
StringOrUndefined
là một conditional type
. Nó sẽ trả về string
nếu T
là string
, còn không thì trả về undefined
.
Vậy làm sao để mô tả được điều kiện cho type
của rtnValue
?
function string<R extends undefined | string>(key: string, defaultValue?: R) {
const rtnValue = has(process.env, key) ? process.env[key] : defaultValue
return rtnValue as StringOrUndefined<R>
},
R extends undefined | string
Lúc nàyR
là một generic type, nó có thể làundefined
hoặcstring
.defaultValue?: R
Lúc nàydefaultValue
có thể làundefined
hoặcstring
.StringOrUndefined<R>
Như đã mô tả ở trên,StringOrUndefined
sẽ trả vềstring
nếuR
làundefined
, còn không thì trả vềstring | undefined
.
Vậy là chúng ta đã có thể mô tả được type
của rtnValue
là string
hoặc string | undefined
tùy thuộc vào defaultValue
được set hay không.
Ví dụ:
const host = env.string('HOST', 'localhost') // string
const user = env.string('USER') // string | undefined
Kết thúc
Chúng ta đã hoàn thành việc refactor code và upgrade lên version mới. Bây giờ chúng ta có thể sử dụng env
mà không cần cast kiểu dữ liệu nữa.
yarn add @ltv/env
Sử dụng:
import env from '@ltv/env'
const host = env.string('HOST', 'localhost')
const port = env.int('PORT', 3000)
const isProduction = env.bool('NODE_ENV', false)
console.log(host, port, isProduction)
Bài viết hôm nay khá lủng củng, mình sẽ cố gắng viết những bài viết có chất lượng hơn. Cảm ơn các bạn đã đọc.
All rights reserved