Giới thiệu
Bạn đã từng tái cấu trúc một dự án và nhận ra rằng mọi tệp đều có phong cách nhập khác nhau? Bài viết này đi sâu vào việc quản lý cấu trúc mã bằng cách thực thi quy tắc với ESLint.
Mục lục
Tóm tắt
Nếu bạn đã biết mình đang làm gì, hãy kiểm tra kho ví dụ: eslint-fsd-example. Nó tuân theo FSD (Feature Sliced Design) và phục vụ như một ví dụ thực tiễn.
Động lực
Là một lập trình viên dẫn đầu, một trong những trách nhiệm của tôi là đảm bảo rằng mã nguồn vẫn nhất quán và dễ quản lý khi độ phức tạp gia tăng. Khi các tính năng và bản sửa lỗi mới được giới thiệu bởi nhóm phát triển, không thể tránh khỏi rằng mã của họ trông khác nhau. Điều đó cũng tốt - mỗi người mang đến một tầm nhìn và quan điểm riêng. Đôi khi, nhóm thậm chí còn áp dụng những thực tiễn ấy ở quy mô lớn.
Nhưng phong cách và cấu trúc mã lại là một câu chuyện khác. Mỗi người có tầm nhìn riêng, và điều đó thật tuyệt... cho đến khi bạn muốn tái cấu trúc 😑.
Hãy tưởng tượng những phần khác nhau trong mã của bạn hoạt động theo cách riêng của chúng:
javascript
import ... from "shared/ui/Comment"; // baseUrl + tệp trực tiếp
import ... from "../../ui/Comment"; // đường dẫn tương đối
import ... from "@/shared/ui"; // bí danh + tệp chỉ mục
import ... from "../../ui/index.js"; // đường dẫn tương đối đến chỉ mục
// và còn nhiều nữa
Điều này xảy ra trong các dự án thực tế và dẫn đến một vườn thú các phương pháp phân tán khắp nơi. Các vấn đề bao gồm nhập khẩu vòng tròn và tái cấu trúc bị hỏng (tự động nhập thường không thay thế đường dẫn đúng).
Hệ quả không lường trước
Nhập khẩu vòng tròn thường xuất hiện khi bạn nhập từ một tệp chỉ mục mà tái xuất cùng một module:
javascript
// src/shared/ui/index.js
export * from './Comment';
export * from './Like';
export * from './ViewsCounter';
// src/shared/ui/Comment
import ... from 'shared/ui'; // <-- 🔥 gây ra vòng lặp
Hoặc thậm chí không có tệp chỉ mục:
javascript
// src/shared/ui/Comment
import ... from './Like';
// src/shared/ui/Like
import ... from './ViewsCounter';
// src/shared/ui/ViewsCounter
import ... from './Comment';
💡 Điều này thậm chí có thể dẫn đến các lỗi runtime rất khó gỡ lỗi (hãy hỏi tôi làm thế nào tôi biết điều đó).
Thực thi Cấu trúc Mã
FSD quy định nghiêm ngặt các nguyên tắc nhập/xuất (chúng thậm chí được mô tả là bắt buộc trong tài liệu):
- Bất kỳ module nào chỉ có thể được nhập qua API công khai (tệp chỉ mục) sử dụng đường dẫn tuyệt đối
- Bên trong một module, tất cả nhập khẩu phải sử dụng đường dẫn tương đối (tránh vòng lặp)
Nhưng điều gì ngăn cản các lập trình viên vi phạm quy tắc này? Không có gì 🤔 - trừ khi bạn thực thi nó với ESLint.
FSD cung cấp một cấu hình eslint-config đặc biệt với chỉ ba quy tắc:
public-api– sử dụng eslint-plugin-import, thêm biểu thức glob để khớp các mẫu trong câu lệnh nhập của bạn (sau đó chuyển đổi các biểu thức glob thành regexp thông qua minimatch)layers-slices– sử dụng eslint-plugin-boundaries, phân tích mối quan hệ giữa phần tử phụ thuộc (module của bạn) và phụ thuộc (cái đang được nhập) và kiểm tra xem nó có được phép hay không (xem tài liệu fsd)import-order– sử dụng eslint-plugin-import, sắp xếp và nhóm các nhập khẩu theo thứ tự chữ cái
Hãy cùng xem một dự án ví dụ Next.js cố gắng tuân theo phương pháp FSD (Feature Sliced Design). Mã mẫu đến từ create-next-app sử dụng ví dụ with-eslint.
bash
npx create-next-app \
eslint-fsd-example \ # tên dự án
-e https://github.com/vercel/next.js/tree/canary \ # tên ví dụ hoặc url
--example-path examples/with-eslint # nếu url ví dụ có tên nhánh trong đó thì đường dẫn đến ví dụ phải được chỉ định riêng
Tùy chỉnh Quy tắc
Cấu trúc dự án (đơn giản):
src/
├── app/
│ ├── ...
│ ├── page.tsx
│ │ └── Trang chính
│ └── signin/
│ └── page.tsx
│ └── Trang Đăng Nhập
├── _pages/
│ └── signin/
│ └── ui/
│ ├── index.ts
│ ├── views/
│ │ ├── Signin.tsx
│ │ │ └── Thùng chứa chính cho đăng nhập
│ │ └── index.ts
│ └── components/
│ ├── index.ts
│ └── Signin/
│ ├── Signin.tsx
│ │ └── Nội dung thực tế cho đăng nhập
│ └── index.ts
├── _features/
│ └── signin/
│ ├── types/
│ │ ├── index.ts
│ │ └── signinFormValues.ts
│ └── lib/
│ ├── index.ts
│ └── useSigninForm.ts
Các thư mục _pages và _features bắt đầu bằng dấu gạch dưới để tránh xung đột tên với các thư mục dự kiến của Next.js. Các lớp FSD khác sử dụng cùng một quy tắc cho sự nhất quán.
Chúng tôi muốn thực thi nhập khẩu chỉ từ API công khai (các tệp chỉ mục). Đây là cấu hình ESLint đơn giản:
javascript
const FS_LAYERS = [ 'entities', 'features', 'widgets', 'app', 'processes', 'pages', 'shared' ];
const FS_SEGMENTS = [ 'ui', 'types', 'model', 'lib', 'api', 'config', 'assets' ];
const nextjsConfigRules: Linter.Config['rules'] = {
'import/no-internal-modules': [
'error',
{
allow: [
/**
* Cho phép nhập không phải các phân đoạn từ các lát
* @ví dụ
* 'entities/user/ui' // Đạt
* 'entities/user' // Cũng Đạt
*/
`**/*(${FS_SLICED_LAYERS_REG})/!(${FS_SEGMENTS_REG})/*(${FS_SEGMENTS_REG})`,
/**
* Cho phép các lát với cấu trúc nhóm
* @ví dụ
* 'features/auth/form' // Đạt
*/
`**/*(${FS_SLICED_LAYERS_REG})/!(${FS_SEGMENTS_REG})/!(${FS_SEGMENTS_REG})`,
...
],
}
]
};
💡 Đối với thư mục
shared, các quy tắc khác nhau áp dụng vì nó khá đặc biệt.
Xem mã nguồn để biết chi tiết.
Việc truy cập qua src/_features/signin/lib/index.ts là bị cấm - chính xác điều mà chúng tôi muốn ✅.
Khám Phá Tương Lai
Trong khi thiết lập này bao gồm các yếu tố cơ bản để thực thi nhập khẩu FSD, vẫn còn một số câu hỏi mở đáng để khám phá:
- Nhập chéo với
@xtrong FSD – Nhập chéo là một phần của phương pháp FSD chính thức. Chúng cho phép tích hợp giữa các lát hoặc lớp theo cách có kiểm soát. Nhưng làm thế nào để chúng tôi thực thi chúng một cách chính xác với ESLint, và những thực tiễn tốt nhất để giữ chúng nhất quán là gì? - Nhập động – Đôi khi bạn cần sử dụng
import()trực tiếp để phân chia mã. Nhưng điều gì xảy ra khi bạn muốn nhập động một cái gì đó từ một API công khai thay vì một tệp sâu? Có thể không, hay bạn luôn kết thúc với việc nhập tệp thô? - Chiến lược kiểm thử – Liệu các bài kiểm tra có nên tuân theo cùng một quy tắc nhập API công khai hay có thể chấp nhận cho chúng truy cập vào các module riêng tư?
Tôi chưa có tất cả các câu trả lời (vẫn đang thử nghiệm 😅), nhưng đây là những lĩnh vực mà tôi đang tích cực tìm hiểu.
👉 Trong một bài viết tiếp theo, tôi sẽ chia sẻ các thử nghiệm và giải pháp có thể cho:
- Làm cho nhập động hoạt động với các API công khai
- Xử lý các nhập kiểm thử mà không phá vỡ các quy tắc
- Thực thi và mở rộng các nhập chéo mà không mất đi độ rõ ràng
Vì vậy, nếu điều đó nghe có vẻ thú vị - hãy theo dõi, vì Phần 2 sắp đến!
Thực hành tốt nhất
- Luôn kiểm tra mã của bạn sau khi thực hiện các thay đổi quy tắc nhập.
- Sử dụng các công cụ như ESLint để tự động hóa quy trình phát hiện lỗi.
Cạm bẫy thường gặp
- Không kiểm tra cẩn thận có thể dẫn đến lỗi khó phát hiện trong mã.
- Vi phạm quy tắc nhập có thể gây khó khăn trong việc bảo trì mã.
Mẹo hiệu suất
- Chia nhỏ các module lớn thành các module nhỏ hơn để dễ dàng bảo trì và nhập khẩu.
- Tối ưu hóa cấu trúc thư mục để tăng tốc độ truy cập.
Giải quyết vấn đề
- Nếu bạn gặp lỗi nhập vòng tròn, hãy kiểm tra lại cấu trúc nhập khẩu của bạn và đảm bảo rằng bạn đang nhập từ các API công khai.
- Sử dụng ESLint để phát hiện các nhập không hợp lệ và sửa chúng kịp thời.
Câu hỏi thường gặp
1. ESLint có giúp tôi tự động phát hiện lỗi không?
Có, ESLint có thể tự động phát hiện các lỗi nhập và cảnh báo bạn.
2. Làm thế nào để tôi biết quy tắc nào cần thay đổi?
Hãy tham khảo tài liệu FSD và điều chỉnh theo nhu cầu dự án của bạn.
3. Có thể sử dụng ESLint với các dự án khác không?
Có, bạn có thể tùy chỉnh ESLint cho bất kỳ dự án nào mà bạn đang phát triển.