Nhiều ứng dụng cần giải quyết cùng một nhóm vấn đề chung, hoặc tái sử dụng một component dạng module trong nhiều context khác nhau. Nest có vài cách để giải quyết việc này, nhưng mỗi cách hoạt động ở một cấp độ khác nhau nhằm giải quyết vấn đề theo hướng phù hợp với các mục tiêu kiến trúc và tổ chức khác nhau.
Các module của Nest hữu ích cho việc cung cấp một execution context cho phép các component được chia sẻ trong cùng một ứng dụng. Module cũng có thể được đóng gói bằng npm để tạo thành một library có thể tái sử dụng, cài đặt trong nhiều dự án khác nhau. Đây có thể là một cách hiệu quả để phân phối các library có thể cấu hình, tái sử dụng được, dùng bởi các tổ chức khác nhau, ít liên kết hoặc không liên quan với nhau (ví dụ, bằng cách phân phối/cài đặt các library bên thứ ba).
Đối với việc chia sẻ code trong các nhóm được tổ chức chặt chẽ (ví dụ, trong phạm vi công ty/dự án), sẽ hữu ích nếu có một cách tiếp cận gọn nhẹ hơn để chia sẻ component. Monorepo ra đời như một cấu trúc để đáp ứng điều đó, và trong một monorepo, một library cung cấp một cách để chia sẻ code một cách dễ dàng, gọn nhẹ. Trong một monorepo Nest, việc dùng library giúp dễ dàng lắp ráp các ứng dụng có chia sẻ component. Trên thực tế, điều này khuyến khích việc phân rã các ứng dụng nguyên khối (monolithic) và quy trình phát triển tập trung vào việc xây dựng và ghép nối các component dạng module.
Ghi chú người dịch Nếu bạn từng làm với PHP/Laravel, một Nest library trong monorepo hơi giống một package Composer local (path repository) mà bạn require trực tiếp từ source thay vì qua Packagist — code luôn “tươi” theo commit mới nhất, không cần publish/version riêng. Còn “library” đóng gói qua npm như một package thông thường thì giống hệt việc publish một package Composer lên Packagist để dùng ở nhiều project độc lập.
Nest libraries
Một Nest library là một dự án Nest, khác với ứng dụng ở chỗ nó không thể tự chạy độc lập. Một library phải được import vào một ứng dụng chứa nó (containing application) thì code của nó mới thực thi được. Phần hỗ trợ tích hợp sẵn cho library được mô tả trong phần này chỉ khả dụng cho monorepo (các dự án standard mode có thể đạt được chức năng tương tự bằng cách dùng npm package).
Ví dụ, một tổ chức có thể phát triển một AuthModule quản lý việc xác thực bằng cách triển khai các chính sách của công ty áp dụng cho mọi ứng dụng nội bộ. Thay vì build module đó riêng cho từng ứng dụng, hoặc đóng gói vật lý code bằng npm và yêu cầu từng dự án cài đặt nó, một monorepo có thể định nghĩa module này như một library. Khi được tổ chức theo cách này, mọi bên tiêu thụ (consumer) của library module đều thấy phiên bản cập nhật nhất của AuthModule ngay khi nó được commit. Điều này có thể mang lại lợi ích đáng kể cho việc điều phối phát triển component và lắp ráp, cũng như đơn giản hóa việc test end-to-end.
Tạo library (Creating libraries)
Bất kỳ chức năng nào phù hợp để tái sử dụng đều là ứng viên để quản lý dưới dạng library. Quyết định thứ gì nên là library, và thứ gì nên là một phần của ứng dụng, là một quyết định thiết kế kiến trúc. Việc tạo library không chỉ đơn giản là copy code từ một ứng dụng có sẵn sang một library mới. Khi được đóng gói thành library, code của library phải được tách rời (decoupled) khỏi ứng dụng. Điều này có thể đòi hỏi nhiều thời gian hơn ngay từ đầu và buộc bạn phải đưa ra một số quyết định thiết kế mà bạn có thể không gặp phải với code liên kết chặt chẽ hơn. Nhưng công sức bỏ thêm này có thể được đền đáp khi library có thể được dùng để lắp ráp ứng dụng nhanh hơn trên nhiều ứng dụng khác nhau.
Để bắt đầu tạo một library, chạy lệnh sau:
$ nest g library my-library
Khi bạn chạy lệnh này, schematic library sẽ hỏi bạn về một prefix (còn gọi là alias) cho library:
What prefix would you like to use for the library (default: @app)?
Lệnh này tạo một dự án mới trong workspace của bạn tên là my-library.
Một dự án kiểu library, giống như dự án kiểu ứng dụng, được sinh vào một folder có tên bằng một schematic. Library được quản lý dưới folder libs ở root của monorepo. Nest tạo folder libs vào lần đầu tiên một library được tạo.
Các file được sinh ra cho một library hơi khác so với các file sinh ra cho một ứng dụng. Đây là nội dung folder libs sau khi chạy lệnh trên:
File nest-cli.json sẽ có thêm một entry mới cho library dưới key "projects":
...
{
"my-library": {
"type": "library",
"root": "libs/my-library",
"entryFile": "index",
"sourceRoot": "libs/my-library/src",
"compilerOptions": {
"tsConfigPath": "libs/my-library/tsconfig.lib.json"
}
}
...
Có hai điểm khác biệt trong metadata nest-cli.json giữa library và ứng dụng:
- thuộc tính
"type"được đặt là"library"thay vì"application" - thuộc tính
"entryFile"được đặt là"index"thay vì"main"
Những khác biệt này giúp quá trình build xử lý library đúng cách. Ví dụ, một library export các hàm của nó thông qua file index.js.
Giống như dự án kiểu ứng dụng, mỗi library có file tsconfig.lib.json riêng, kế thừa từ file tsconfig.json gốc (toàn monorepo). Bạn có thể chỉnh sửa file này nếu cần, để cung cấp các option compiler riêng cho library.
Bạn có thể build library bằng lệnh CLI:
$ nest build my-library
Dùng library (Using libraries)
Với các file cấu hình được sinh tự động sẵn có, việc dùng library khá đơn giản. Làm sao để chúng ta import MyLibraryService từ library my-library vào ứng dụng my-project?
Trước tiên, lưu ý rằng dùng module của library cũng giống như dùng bất kỳ module Nest nào khác. Điều mà monorepo làm là quản lý đường dẫn (path) theo cách khiến việc import library và sinh build trở nên trong suốt. Để dùng MyLibraryService, ta cần import module khai báo nó. Ta có thể chỉnh sửa my-project/src/app.module.ts như sau để import MyLibraryModule.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
import { MyLibraryModule } from '@app/my-library';
@Module({
imports: [MyLibraryModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Lưu ý ở trên, chúng ta đã dùng một path alias là @app trong dòng import của ES module, đây chính là prefix chúng ta đã cung cấp cùng lệnh nest g library ở trên. Bên dưới, Nest xử lý việc này thông qua tsconfig path mapping. Khi thêm một library, Nest cập nhật key "paths" trong file tsconfig.json (toàn monorepo) như sau:
"paths": {
"@app/my-library": [
"libs/my-library/src"
],
"@app/my-library/*": [
"libs/my-library/src/*"
]
}
Cảnh báo Việc mapping paths chỉ được TypeScript compiler hiểu. Các test runner tự phân giải module theo cách riêng — nếu bạn chạy e2e test bằng Jest, hãy phản chiếu (mirror) từng alias trong option moduleNameMapper của cấu hình Jest cho e2e, ví dụ: "moduleNameMapper": { "^@app/my-library(|/.*)$": "<rootDir>/../../libs/my-library/src/$1" }. Nếu không, các import qua alias sẽ lỗi khi chạy test dù dự án vẫn compile bình thường.
Ghi chú người dịch Đây chính là cạm bẫy hay gặp nhất khi mới dùng Nest monorepo: TypeScript compile qua được (vì nó đọc tsconfig.json), nhưng Jest lại crash với lỗi “Cannot find module ‘@app/my-library’” vì Jest không biết gì về paths của TypeScript — nó cần cấu hình moduleNameMapper riêng. Tương tự như khi bạn cấu hình alias import trong Webpack/Vite nhưng quên đồng bộ với Jest ở phía frontend.
Vậy, tóm lại, sự kết hợp giữa tính năng monorepo và library đã giúp việc đưa các library module vào ứng dụng trở nên dễ dàng và trực quan.
Cơ chế tương tự cho phép bạn build và deploy các ứng dụng có ghép library. Sau khi bạn đã import MyLibraryModule, chạy nest build sẽ tự động xử lý toàn bộ việc phân giải module và đóng gói ứng dụng cùng với mọi library dependency, sẵn sàng cho việc deploy. Compiler mặc định cho monorepo là Rspack, nên file phân phối kết quả là một file duy nhất gộp tất cả các file JavaScript đã transpile lại thành một file. Bạn cũng có thể chuyển sang tsc như mô tả tại đây.