# Feature-Sliced Design ## documentation - [Ví dụ](/examples.md): Danh sách các website được xây dựng với Feature-Sliced Design - [🧭 Điều hướng](/nav.md): Feature-Sliced Design Navigation help page - [Tìm kiếm](/search.md) - [Các phiên bản Feature-Sliced Design](/versions.md): Feature-Sliced Design Versions page listing all documented site versions - [💫 Community](/community.md): Community resources, additional materials - [Team](/community/team.md): Core-team - [Các phương án thay thế](/docs/about/alternatives.md): Lịch sử các cách tiếp cận kiến trúc - [Sứ mệnh](/docs/about/mission.md): Ở đây chúng tôi mô tả các mục tiêu và hạn chế của khả năng áp dụng của phương pháp - những gì chúng tôi được hướng dẫn khi phát triển phương pháp - [Động lực](/docs/about/motivation.md): Ý tưởng chính của Feature-Sliced Design là tạo điều kiện thuận lợi và giảm chi phí phát triển các dự án phức tạp và được phát triển, dựa trên việc kết hợp kết quả nghiên cứu, thảo luận kinh nghiệm của các loại developer khác nhau trong phạm vi rộng. - [Thúc đẩy trong công ty](/docs/about/promote/for-company.md): Dự án và công ty có cần methodology không? - [Thúc đẩy trong team](/docs/about/promote/for-team.md): - Onboard những người mới - [Các khía cạnh tích hợp](/docs/about/promote/integration.md): Tóm tắt - [Ứng dụng từng phần](/docs/about/promote/partial-application.md): Làm thế nào để áp dụng methodology từng phần? Có hợp lý không? Nếu tôi bỏ qua thì sao? - [Abstraction](/docs/about/understanding/abstractions.md): Quy luật của abstraction bị rò rỉ - [Về kiến trúc](/docs/about/understanding/architecture.md): Các vấn đề - [Các loại kiến thức trong dự án](/docs/about/understanding/knowledge-types.md): Có thể phân biệt các "loại kiến thức" sau trong bất kỳ dự án nào: - [Đặt tên](/docs/about/understanding/naming.md): Các developer khác nhau có kinh nghiệm và ngữ cảnh khác nhau, có thể dẫn đến hiểu lầm trong team khi cùng một entity được gọi khác nhau. Ví dụ: - [Hướng nhu cầu](/docs/about/understanding/needs-driven.md): — Không thể đưa ra mục tiêu mà tính năng mới sẽ giải quyết? Hoặc có thể vấn đề là bản thân task không được đưa ra? **Điềm mấu chốt cũng là methodology giúp rút ra định nghĩa có vấn đề của các task và mục tiêu** - [Signal của kiến trúc](/docs/about/understanding/signals.md): Nếu có giới hạn về phía kiến trúc, thì có những lý do rõ ràng cho điều này, và hậu quả nếu chúng bị bỏ qua - [Hướng dẫn Thương hiệu](/docs/branding.md): Bản sắc thị giác của FSD dựa trên các khái niệm cốt lõi: Layered, Sliced self-contained parts, Parts & Compose, Segmented. - [Decomposition cheatsheet](/docs/get-started/cheatsheet.md): Sử dụng làm tài liệu tham khảo nhanh khi bạn quyết định cách decompose UI của mình. Phiên bản PDF cũng có sẵn bên dưới, để bạn có thể in ra và giữ một bản dưới gối. - [FAQ](/docs/get-started/faq.md): Bạn có thể đặt câu hỏi trong Telegram chat, Discord community, và GitHub Discussions của chúng tôi. - [Tổng quan](/docs/get-started/overview.md): Feature-Sliced Design (FSD) là một phương pháp thiết kế kiến trúc để xây dựng các ứng dụng frontend. Nói đơn giản, đây là tập hợp các quy tắc và quy ước để tổ chức code. Mục đích chính của phương pháp này là làm cho dự án trở nên dễ hiểu và ổn định hơn khi đối mặt với những yêu cầu kinh doanh liên tục thay đổi. - [Tutorial](/docs/get-started/tutorial.md): Phần 1. Trên giấy - [Xử lý API Requests](/docs/guides/examples/api-requests.md): handling-api-requests} - [Authentication](/docs/guides/examples/auth.md): Nói chung, authentication bao gồm các bước sau: - [Autocomplete](/docs/guides/examples/autocompleted.md): Về decomposition theo layers - [Browser API](/docs/guides/examples/browser-api.md): Về việc làm việc với Browser API: localStorage, audio Api, bluetooth API, v.v. - [CMS](/docs/guides/examples/cms.md): Features có thể khác nhau - [Feedback](/docs/guides/examples/feedback.md): Errors, Alerts, Notifications, ... - [i18n](/docs/guides/examples/i18n.md): Đặt ở đâu? Làm thế nào để làm việc với điều này? - [Metric](/docs/guides/examples/metric.md): About ways to initialize metrics in the application - [Monorepositories](/docs/guides/examples/monorepo.md): About applicability for mono repositories, about bff, about microapps - [Page layouts](/docs/guides/examples/page-layout.md): Hướng dẫn này xem xét abstraction của một page layout — khi nhiều pages chia sẻ cùng một cấu trúc tổng thể, và chỉ khác nhau ở main content. - [Desktop/Touch platforms](/docs/guides/examples/platforms.md): About the application of the methodology for desktop/touch - [SSR](/docs/guides/examples/ssr.md): About the implementation of SSR using the methodology - [Theme](/docs/guides/examples/theme.md): Where should I put my work with the theme and palette? - [Types](/docs/guides/examples/types.md): Hướng dẫn này liên quan đến data types từ các typed languages như TypeScript và mô tả chúng phù hợp ở đâu trong FSD. - [White Labels](/docs/guides/examples/white-labels.md): Figma, brand uikit, templates, adaptability to brands - [Cross-imports](/docs/guides/issues/cross-imports.md): Cross-imports xuất hiện khi layer hoặc abstraction bắt đầu đảm nhận quá nhiều trách nhiệm hơn nó nên. Đó là lý do tại sao phương pháp luận xác định các layers mới cho phép bạn tách rời những cross-imports này - [Desegmented](/docs/guides/issues/desegmented.md): Tình huống - [Routing](/docs/guides/issues/routes.md): Tình huống - [Migration từ custom architecture](/docs/guides/migration/from-custom.md): Hướng dẫn này mô tả cách tiếp cận có thể hữu ích khi migration từ custom self-made architecture sang Feature-Sliced Design. - [Migration từ v1 sang v2](/docs/guides/migration/from-v1.md): Tại sao v2? - [Migration từ v2.0 sang v2.1](/docs/guides/migration/from-v2-0.md): Thay đổi chính trong v2.1 là mental model mới để phân tách interface — pages first. - [Sử dụng với Electron](/docs/guides/tech/with-electron.md): Các ứng dụng Electron có kiến trúc đặc biệt gồm nhiều process với các trách nhiệm khác nhau. Việc áp dụng FSD trong bối cảnh như vậy yêu cầu phải thích nghi cấu trúc với các đặc điểm của Electron. - [Sử dụng với Next.js](/docs/guides/tech/with-nextjs.md): FSD tương thích với Next.js trong cả phiên bản App Router và Pages Router nếu bạn giải quyết được xung đột chính — thư mục app và pages. - [Sử dụng với NuxtJS](/docs/guides/tech/with-nuxtjs.md): Có thể triển khai FSD trong dự án NuxtJS, nhưng xảy ra xung đột do sự khác biệt giữa yêu cầu cấu trúc dự án của NuxtJS và các nguyên tắc FSD: - [Sử dụng với React Query](/docs/guides/tech/with-react-query.md): Vấn đề "nên đặt các key ở đâu" - [Sử dụng với SvelteKit](/docs/guides/tech/with-sveltekit.md): Có thể triển khai FSD trong dự án SvelteKit, nhưng xảy ra xung đột do sự khác biệt giữa yêu cầu cấu trúc của dự án SvelteKit và các nguyên tắc của FSD: - [Tài liệu cho LLMs](/docs/llms.md): Trang này cung cấp các liên kết và hướng dẫn cho các LLM crawler. - [Layer](/docs/reference/layers.md): Layer là cấp độ đầu tiên của hệ thống phân cấp tổ chức trong Feature-Sliced Design. Mục đích của chúng là phân tách code dựa trên mức độ trách nhiệm cần thiết và số lượng module khác trong app mà nó phụ thuộc vào. Mỗi layer mang ý nghĩa ngữ nghĩa đặc biệt để giúp bạn xác định mức độ trách nhiệm mà bạn nên phân bổ cho code của mình. - [Public API](/docs/reference/public-api.md): Public API là một hợp đồng giữa một nhóm module, như một slice, và code sử dụng nó. Nó cũng hoạt động như một cổng kiểm soát, chỉ cho phép truy cập đến các đối tượng nhất định và chỉ thông qua public API đó. - [Slice và segment](/docs/reference/slices-segments.md): Slice - [Feature-Sliced Design](/index.md): Architectural methodology for frontend projects --- # Full Documentation Content v2 ![](/documentation/vi/assets/ideal-img/tiny-bunny.dd60f55.640.png) Tiny Bunny Mini Game Mini-game "21 points" in the universe of the visual novel "Tiny Bunny". reactredux-toolkittypescript [Website](https://sanua356.github.io/tiny-bunny/)[Source](https://github.com/sanua356/tiny-bunny) --- # 🧭 Điều hướng ## Route cũ Sau khi tái cấu trúc tài liệu, một số route đã thay đổi. Bên dưới bạn có thể tìm thấy trang mà bạn đang tìm kiếm. Nhưng có redirect từ các link cũ để đảm bảo tương thích ### 🚀 Get Started ⚡️ Simplified and merged [Tutorial](/documentation/vi/docs/get-started/tutorial.md) [**old**:](/documentation/vi/docs/get-started/tutorial.md) [/docs/get-started/quick-start](/documentation/vi/docs/get-started/tutorial.md) [**new**: ](/documentation/vi/docs/get-started/tutorial.md) [/docs/get-started/tutorial](/documentation/vi/docs/get-started/tutorial.md) [Basics](/documentation/vi/docs/get-started/overview.md) [**old**:](/documentation/vi/docs/get-started/overview.md) [/docs/get-started/basics](/documentation/vi/docs/get-started/overview.md) [**new**: ](/documentation/vi/docs/get-started/overview.md) [/docs/get-started/overview](/documentation/vi/docs/get-started/overview.md) [Decompose Cheatsheet](/documentation/vi/docs/get-started/cheatsheet.md) [**old**:](/documentation/vi/docs/get-started/cheatsheet.md) [/docs/get-started/tutorial/decompose; /docs/get-started/tutorial/design-mockup; /docs/get-started/onboard/cheatsheet](/documentation/vi/docs/get-started/cheatsheet.md) [**new**: ](/documentation/vi/docs/get-started/cheatsheet.md) [/docs/get-started/cheatsheet](/documentation/vi/docs/get-started/cheatsheet.md) ### 🍰 Alternatives ⚡️ Moved and merged to /about/alternatives as advanced materials [Architecture approaches alternatives](/documentation/vi/docs/about/alternatives.md) [**old**:](/documentation/vi/docs/about/alternatives.md) [/docs/about/alternatives/big-ball-of-mud; /docs/about/alternatives/design-principles; /docs/about/alternatives/ddd; /docs/about/alternatives/clean-architecture; /docs/about/alternatives/frameworks; /docs/about/alternatives/atomic-design; /docs/about/alternatives/smart-dumb-components; /docs/about/alternatives/feature-driven](/documentation/vi/docs/about/alternatives.md) [**new**: ](/documentation/vi/docs/about/alternatives.md) [/docs/about/alternatives](/documentation/vi/docs/about/alternatives.md) ### 🍰 Promote & Understanding ⚡️ Moved to /about as advanced materials [Knowledge types](/documentation/vi/docs/about/understanding/knowledge-types.md) [**old**:](/documentation/vi/docs/about/understanding/knowledge-types.md) [/docs/reference/knowledge-types](/documentation/vi/docs/about/understanding/knowledge-types.md) [**new**: ](/documentation/vi/docs/about/understanding/knowledge-types.md) [/docs/about/understanding/knowledge-types](/documentation/vi/docs/about/understanding/knowledge-types.md) [Needs driven](/documentation/vi/docs/about/understanding/needs-driven.md) [**old**:](/documentation/vi/docs/about/understanding/needs-driven.md) [/docs/concepts/needs-driven](/documentation/vi/docs/about/understanding/needs-driven.md) [**new**: ](/documentation/vi/docs/about/understanding/needs-driven.md) [/docs/about/understanding/needs-driven](/documentation/vi/docs/about/understanding/needs-driven.md) [About architecture](/documentation/vi/docs/about/understanding/architecture.md) [**old**:](/documentation/vi/docs/about/understanding/architecture.md) [/docs/concepts/architecture](/documentation/vi/docs/about/understanding/architecture.md) [**new**: ](/documentation/vi/docs/about/understanding/architecture.md) [/docs/about/understanding/architecture](/documentation/vi/docs/about/understanding/architecture.md) [Naming adaptability](/documentation/vi/docs/about/understanding/naming.md) [**old**:](/documentation/vi/docs/about/understanding/naming.md) [/docs/concepts/naming-adaptability](/documentation/vi/docs/about/understanding/naming.md) [**new**: ](/documentation/vi/docs/about/understanding/naming.md) [/docs/about/understanding/naming](/documentation/vi/docs/about/understanding/naming.md) [Signals of architecture](/documentation/vi/docs/about/understanding/signals.md) [**old**:](/documentation/vi/docs/about/understanding/signals.md) [/docs/concepts/signals](/documentation/vi/docs/about/understanding/signals.md) [**new**: ](/documentation/vi/docs/about/understanding/signals.md) [/docs/about/understanding/signals](/documentation/vi/docs/about/understanding/signals.md) [Abstractions of architecture](/documentation/vi/docs/about/understanding/abstractions.md) [**old**:](/documentation/vi/docs/about/understanding/abstractions.md) [/docs/concepts/abstractions](/documentation/vi/docs/about/understanding/abstractions.md) [**new**: ](/documentation/vi/docs/about/understanding/abstractions.md) [/docs/about/understanding/abstractions](/documentation/vi/docs/about/understanding/abstractions.md) ### 📚 Reference guidelines (isolation & units) ⚡️ Moved to /reference as theoretical materials (old concepts) [Decouple of entities](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/decouple-entities](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [Low Coupling & High Cohesion](/documentation/vi/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**old**:](/documentation/vi/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/concepts/low-coupling](/documentation/vi/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**new**: ](/documentation/vi/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/reference/slices-segments#zero-coupling-high-cohesion](/documentation/vi/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [Cross-communication](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/cross-communication](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [App splitting](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/concepts/app-splitting](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Decomposition](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/decomposition](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Units](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Layers](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/layers](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Layer overview](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers/overview](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [App layer](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/layers/app](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Processes layer](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/layers/processes](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Pages layer](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/layers/pages](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Widgets layer](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/layers/widgets](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Widgets layer](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers/widgets](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Features layer](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/layers/features](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Entities layer](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/layers/entities](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Shared layer](/documentation/vi/docs/reference/layers.md) [**old**:](/documentation/vi/docs/reference/layers.md) [/docs/reference/units/layers/shared](/documentation/vi/docs/reference/layers.md) [**new**: ](/documentation/vi/docs/reference/layers.md) [/docs/reference/layers](/documentation/vi/docs/reference/layers.md) [Segments](/documentation/vi/docs/reference/slices-segments.md) [**old**:](/documentation/vi/docs/reference/slices-segments.md) [/docs/reference/units/segments](/documentation/vi/docs/reference/slices-segments.md) [**new**: ](/documentation/vi/docs/reference/slices-segments.md) [/docs/reference/slices-segments](/documentation/vi/docs/reference/slices-segments.md) ### 🎯 Bad Practices handbook ⚡️ Moved to /guides as practice materials [Cross-imports](/documentation/vi/docs/guides/issues/cross-imports.md) [**old**:](/documentation/vi/docs/guides/issues/cross-imports.md) [/docs/concepts/issues/cross-imports](/documentation/vi/docs/guides/issues/cross-imports.md) [**new**: ](/documentation/vi/docs/guides/issues/cross-imports.md) [/docs/guides/issues/cross-imports](/documentation/vi/docs/guides/issues/cross-imports.md) [Desegmented](/documentation/vi/docs/guides/issues/desegmented.md) [**old**:](/documentation/vi/docs/guides/issues/desegmented.md) [/docs/concepts/issues/desegmented](/documentation/vi/docs/guides/issues/desegmented.md) [**new**: ](/documentation/vi/docs/guides/issues/desegmented.md) [/docs/guides/issues/desegmented](/documentation/vi/docs/guides/issues/desegmented.md) [Routes](/documentation/vi/docs/guides/issues/routes.md) [**old**:](/documentation/vi/docs/guides/issues/routes.md) [/docs/concepts/issues/routes](/documentation/vi/docs/guides/issues/routes.md) [**new**: ](/documentation/vi/docs/guides/issues/routes.md) [/docs/guides/issues/routes](/documentation/vi/docs/guides/issues/routes.md) ### 🎯 Examples ⚡️ Grouped and simplified into /guides/examples as practical examples [Viewer logic](/documentation/vi/docs/guides/examples/auth.md) [**old**:](/documentation/vi/docs/guides/examples/auth.md) [/docs/guides/examples/viewer](/documentation/vi/docs/guides/examples/auth.md) [**new**: ](/documentation/vi/docs/guides/examples/auth.md) [/docs/guides/examples/auth](/documentation/vi/docs/guides/examples/auth.md) [Monorepo](/documentation/vi/docs/guides/examples/monorepo.md) [**old**:](/documentation/vi/docs/guides/examples/monorepo.md) [/docs/guides/monorepo](/documentation/vi/docs/guides/examples/monorepo.md) [**new**: ](/documentation/vi/docs/guides/examples/monorepo.md) [/docs/guides/examples/monorepo](/documentation/vi/docs/guides/examples/monorepo.md) [White Labels](/documentation/vi/docs/guides/examples/white-labels.md) [**old**:](/documentation/vi/docs/guides/examples/white-labels.md) [/docs/guides/white-labels](/documentation/vi/docs/guides/examples/white-labels.md) [**new**: ](/documentation/vi/docs/guides/examples/white-labels.md) [/docs/guides/examples/white-labels](/documentation/vi/docs/guides/examples/white-labels.md) ### 🎯 Migration ⚡️ Grouped and simplified into /guides/migration as migration guidelines [Migration from V1](/documentation/vi/docs/guides/migration/from-v1.md) [**old**:](/documentation/vi/docs/guides/migration/from-v1.md) [/docs/guides/migration-from-v1](/documentation/vi/docs/guides/migration/from-v1.md) [**new**: ](/documentation/vi/docs/guides/migration/from-v1.md) [/docs/guides/migration/from-v1](/documentation/vi/docs/guides/migration/from-v1.md) [Migration from Legacy](/documentation/vi/docs/guides/migration/from-custom.md) [**old**:](/documentation/vi/docs/guides/migration/from-custom.md) [/docs/guides/migration-from-legacy](/documentation/vi/docs/guides/migration/from-custom.md) [**new**: ](/documentation/vi/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/documentation/vi/docs/guides/migration/from-custom.md) ### 🎯 Tech ⚡️ Grouped into /guides/tech as tech-specific usage guidelines [Usage with NextJS](/documentation/vi/docs/guides/tech/with-nextjs.md) [**old**:](/documentation/vi/docs/guides/tech/with-nextjs.md) [/docs/guides/usage-with-nextjs](/documentation/vi/docs/guides/tech/with-nextjs.md) [**new**: ](/documentation/vi/docs/guides/tech/with-nextjs.md) [/docs/guides/tech/with-nextjs](/documentation/vi/docs/guides/tech/with-nextjs.md) ### Rename 'legacy' to 'custom' ⚡️ 'Legacy' is derogatory, we don't get to call people's projects legacy [Rename 'legacy' to custom](/documentation/vi/docs/guides/migration/from-custom.md) [**old**:](/documentation/vi/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-legacy](/documentation/vi/docs/guides/migration/from-custom.md) [**new**: ](/documentation/vi/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/documentation/vi/docs/guides/migration/from-custom.md) ### Deduplication of Reference ⚡️ Cleaned up the Reference section and deduplicated the material [Isolation of modules](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/isolation](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/vi/docs/reference/layers.md#import-rule-on-layers) --- [Chuyển đến nội dung chính](#__docusaurus_skipToContent_fallback) [![Logo](/documentation/vi/img/brand/logo-primary.png)![Logo](/documentation/vi/img/brand/logo-primary.png)](/documentation/vi/.md) [**FSD**](/documentation/vi/.md)[📖 Tài liệu](/documentation/vi/docs/get-started/overview.md)[💫 Cộng đồng](/documentation/vi/community.md)[📝 Blog](/documentation/vi/blog)[🛠 Ví dụ](/documentation/vi/examples.md) [v2.1](/documentation/vi/docs/get-started/overview.md) * [v2.1](/documentation/vi/docs/get-started/overview.md) * [v1.0](https://feature-sliced.github.io/featureslices.dev/v1.0.html) * [v0.1](https://feature-sliced.github.io/featureslices.dev/v0.1.html) * [feature-driven](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) * [All versions](/documentation/vi/versions.md) [Tiếng Việt](#) * [Русский](/documentation/ru/search) * [English](/documentation/search) * [O'zbekcha](/documentation/uz/search) * [한국어](/documentation/kr/search) * [日本語](/documentation/ja/search) * [Tiếng Việt](/documentation/vi/search.md) * [Help Us Translate](https://github.com/feature-sliced/documentation/issues/244) [](https://discord.gg/S8MzWTUsmp)[](https://github.com/feature-sliced/documentation) Tìm kiếm # Tìm kiếm Nhập từ khóa cần tìm vào đây [](https://www.algolia.com/) Specs * [Tài liệu](/documentation/vi/docs/get-started/overview.md) * [Cộng đồng](/documentation/vi/community.md) * [Trợ giúp](/documentation/vi/nav.md) * [Thảo luận](https://github.com/feature-sliced/documentation/discussions) Cộng đồng * [Discord](https://discord.gg/S8MzWTUsmp) * [Telegram (RU)](https://t.me/feature_sliced) * [Twitter](https://twitter.com/feature_sliced) * [Open Collective](https://opencollective.com/feature-sliced) * [YouTube](https://www.youtube.com/c/FeatureSlicedDesign) Thêm * [GitHub](https://github.com/feature-sliced) * [Contribution Guide](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md) * [License](https://github.com/feature-sliced/documentation/blob/master/LICENSE) * [Docs for LLMs](/documentation/vi/docs/llms.md) [![Feature-Sliced Design - Architectural methodology for frontend projects](/documentation/vi/img/brand/logo-primary.png)![Feature-Sliced Design - Architectural methodology for frontend projects](/documentation/vi/img/brand/logo-primary.png)](https://github.com/feature-sliced) Bản quyền © 2018-2025 Feature-Sliced Design --- # Các phiên bản Feature-Sliced Design ### Feature-Sliced Design v2.1 (Current) Tài liệu cho phiên bản hiện tại có thể tìm thấy ở đây | v2.1 | [Release Notes](https://github.com/feature-sliced/documentation/releases/tag/v2.1) | [Documentation](/documentation/vi/docs/get-started/overview.md) | [Migration from v1](/documentation/vi/docs/guides/migration/from-v1.md) | [Migration from v2.0](/documentation/vi/docs/guides/migration/from-v1.md) | | ---- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- | ### Feature Slices v1 (Legacy) Tài liệu cho các phiên bản cũ của feature-slices có thể tìm thấy ở đây | v1.0 | [Documentation](https://feature-sliced.github.io/featureslices.dev/v1.0.html) | | ---- | ----------------------------------------------------------------------------- | | v0.1 | [Documentation](https://feature-sliced.github.io/featureslices.dev/v0.1.html) | ### Feature Driven (Legacy) Tài liệu cho các phiên bản cũ của feature-driven có thể tìm thấy ở đây | v0.1 | [Documentation](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) | | ------------- | --------------------------------------------------------------------------------------- | | Example (kof) | [Github](https://github.com/kof/feature-driven-architecture) | --- # 💫 Community Community resources, additional materials ## Main[​](#main "Link trực tiếp đến heading") [Awesome Resources](https://github.com/feature-sliced/awesome) [A curated list of awesome FSD videos, articles, packages](https://github.com/feature-sliced/awesome) [Team](/documentation/vi/community/team.md) [Core-team, Champions, Contributors, Companies](/documentation/vi/community/team.md) [Brandbook](/documentation/vi/docs/branding.md) [Recommendations for FSD's branding usage](/documentation/vi/docs/branding.md) [Contributing](#) [HowTo, Workflow, Support](#) --- # Team WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/192) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Core-team[​](#core-team "Link trực tiếp đến heading") ### Champions[​](#champions "Link trực tiếp đến heading") ## Contributors[​](#contributors "Link trực tiếp đến heading") ## Companies[​](#companies "Link trực tiếp đến heading") --- # Các phương án thay thế WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/62) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* Lịch sử các cách tiếp cận kiến trúc ## Big Ball of Mud[​](#big-ball-of-mud "Link trực tiếp đến heading") WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/258) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Nó là gì; Tại sao nó phổ biến; Khi nào nó bắt đầu mang lại vấn đề; Phải làm gì và FSD giúp đỡ đây như thế nào * [(Article) Oleg Isonen - Last words on UI architecture before an AI takes over](https://oleg008.medium.com/last-words-on-ui-architecture-before-an-ai-takes-over-468c78f18f0d) * [(Report) Julia Nikolaeva, iSpring - Big Ball of Mud and other problems of the monolith, we have handled](http://youtu.be/gna4Ynz1YNI) * [(Article) DD - Big Ball of mud](https://thedomaindrivendesign.io/big-ball-of-mud/) ## Smart & Dumb components[​](#smart--dumb-components "Link trực tiếp đến heading") WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/214) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Về cách tiếp cận; Về khả năng áp dụng trong frontend; Vị trí của methodology Về sự lỗi thời, về quan điểm mới từ methodology Tại sao cách tiếp cận component-container lại xấu? * [(Article) Den Abramov-Presentation and Container Components (TLDR: deprecated)](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) ## Design Principles[​](#design-principles "Link trực tiếp đến heading") WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/59) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Chúng ta đang nói về điều gì; Vị trí của FSD SOLID, GRASP, KISS, YAGNI, ... - và tại sao chúng không hoạt động tốt cùng nhau trong thực tế Và nó tổng hợp các thực hành này như thế nào * [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Design Principles)](https://youtu.be/SnzPAr_FJ7w?t=380) ## DDD[​](#ddd "Link trực tiếp đến heading") WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/1) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Về cách tiếp cận; Tại sao nó hoạt động kém trong thực tế Sự khác biệt là gì, nó cải thiện khả năng áp dụng như thế nào, nó áp dụng các thực hành ở đâu * [(Article) DDD, Hexagonal, Onion, Clean, CQRS, ... How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) * [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Clean Architecture, DDD)](https://youtu.be/SnzPAr_FJ7w?t=528) ## Clean Architecture[​](#clean-architecture "Link trực tiếp đến heading") WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/165) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Về cách tiếp cận; Về khả năng áp dụng trong frontend; Vị trí của FSD Chúng giống nhau như thế nào (đối với nhiều người), chúng khác nhau như thế nào * [(Thread) About use-case/interactor in the methodology](https://t.me/feature_sliced/3897) * [(Thread) About DI in the methodology](https://t.me/feature_sliced/4592) * [(Article) Alex Bespoyasov - Clean Architecture on frontend](https://bespoyasov.me/blog/clean-architecture-on-frontend/) * [(Article) DDD, Hexagonal, Onion, Clean, CQRS, ... How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) * [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Clean Architecture, DDD)](https://youtu.be/SnzPAr_FJ7w?t=528) * [(Article) Misconceptions of Clean Architecture](http://habr.com/ru/company/mobileup/blog/335382/) ## Frameworks[​](#frameworks "Link trực tiếp đến heading") WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/58) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Về khả năng áp dụng trong frontend; Tại sao các framework không giải quyết vấn đề; tại sao không có cách tiếp cận duy nhất; vị trí của FSD Không phụ thuộc vào framework, cách tiếp cận thông thường * [(Article) About the reasons for creating the methodology (fragment about frameworks)](/documentation/vi/docs/about/motivation.md) * [(Thread) About the applicability of the methodology for different frameworks](https://t.me/feature_sliced/3867) ## Atomic Design[​](#atomic-design "Link trực tiếp đến heading") ### Nó là gì?[​](#nó-là-gì "Link trực tiếp đến heading") Trong Atomic Design, phạm vi trách nhiệm được chia thành các layer tiêu chuẩn. Atomic Design được chia thành **5 layer** (từ trên xuống dưới): 1. `pages` - Chức năng tương tự với layer `pages` trong FSD. 2. `templates` - Các component xác định cấu trúc của trang mà không gắn với nội dung cụ thể. 3. `organisms` - Các module bao gồm các molecule và có logic kinh doanh. 4. `molecules` - Các component phức tạp hơn nói chung không chứa logic kinh doanh. 5. `atoms` - Các component UI không có logic kinh doanh. Các module ở một layer chỉ tương tác với các module ở các layer bên dưới, tương tự như FSD. Tức là, các molecule được xây dựng từ các atom, các organism từ các molecule, các template từ các organism, và các page từ các template. Atomic Design cũng bao hàm việc sử dụng Public API trong các module để cô lập. ### Khả năng áp dụng cho frontend[​](#khả-năng-áp-dụng-cho-frontend "Link trực tiếp đến heading") Atomic Design tương đối phổ biến trong các dự án. Atomic Design phổ biến hơn trong các nhà thiết kế web hơn là trong phát triển. Các nhà thiết kế web thường sử dụng Atomic Design để tạo ra các thiết kế có thể mở rộng và dễ bảo trì. Trong phát triển, Atomic Design thường được trộn lẫn với các methodology kiến trúc khác. Tuy nhiên, vì Atomic Design tập trung vào các component UI và sự kết hợp của chúng, một vấn đề phát sinh với việc triển khai logic kinh doanh trong kiến trúc. Vấn đề là Atomic Design không cung cấp một mức trách nhiệm rõ ràng cho logic kinh doanh, dẫn đến việc phân tán nó qua các component và mức khác nhau, làm phức tạp hóa bảo trì và testing. Logic kinh doanh trở nên mờ nhạt, khiến việc phân tách rõ trách nhiệm trở nên khó khăn và làm code ít modular và có thể tái sử dụng hơn. ### Nó liên quan đến FSD như thế nào?[​](#nó-liên-quan-đến-fsd-như-thế-nào "Link trực tiếp đến heading") Trong bối cảnh của FSD, một số yếu tố của Atomic Design có thể được áp dụng để tạo ra các component UI linh hoạt và có thể mở rộng. Các layer `atoms` và `molecules` có thể được triển khai trong `shared/ui` trong FSD, đơn giản hóa việc tái sử dụng và bảo trì các yếu tố UI cơ bản. ``` ├── shared │ ├── ui │ │ ├── atoms │ │ ├── molecules │ ... ``` So sánh FSD và Atomic Design cho thấy rằng cả hai methodology đều hướng tới tính modular và tái sử dụng nhưng tập trung vào các khía cạnh khác nhau. Atomic Design hướng tới các component trực quan và sự kết hợp của chúng. FSD tập trung vào việc chia chức năng của ứng dụng thành các module độc lập và các kết nối giữa chúng. * [Atomic Design Methodology](https://atomicdesign.bradfrost.com/table-of-contents/) * [(Thread) About applicability in shared / ui](https://t.me/feature_sliced/1653) * [(Video) Briefly about Atomic Design](https://youtu.be/Yi-A20x2dcA) * [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Atomic Design)](https://youtu.be/SnzPAr_FJ7w?t=587) ## Feature Driven[​](#feature-driven "Link trực tiếp đến heading") WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/219) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Về cách tiếp cận; Về khả năng áp dụng trong frontend; Vị trí của FSD Về tính tương thích, phát triển lịch sử và so sánh * [(Talk) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs) * [Feature Driven-Short specification (from the point of view of FSD)](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) --- # Sứ mệnh Ở đây chúng tôi mô tả các mục tiêu và hạn chế của khả năng áp dụng của phương pháp - những gì chúng tôi được hướng dẫn khi phát triển phương pháp * Chúng tôi xem mục tiêu của mình là sự cân bằng giữa ideology và tính đơn giản * Chúng tôi sẽ không thể tạo ra một giải pháp vạn năng phù hợp với mọi người **Tuy nhiên, phương pháp này nên gần gũi và dễ tiếp cận cho một nhóm rộng rãi các developer** ## Mục tiêu[​](#mục-tiêu "Link trực tiếp đến heading") ### Tính rõ ràng trực quan cho nhiều developer[​](#tính-rõ-ràng-trực-quan-cho-nhiều-developer "Link trực tiếp đến heading") Phương pháp này nên dễ tiếp cận - cho hầu hết thành viên trong nhóm dự án *Vì ngay cả với tất cả các công cụ tương lai, nó sẽ không đủ, nếu chỉ có những senior/lead có kinh nghiệm hiểu được phương pháp* ### Giải quyết các vấn đề hàng ngày[​](#giải-quyết-các-vấn-đề-hàng-ngày "Link trực tiếp đến heading") Phương pháp này nên đưa ra các lý do và giải pháp cho các vấn đề hàng ngày của chúng ta khi phát triển dự án **Và cũng - gắn kèm công cụ vào tất cả những điều này (cli, linter)** Để các developer có thể sử dụng một cách tiếp cận *đã được kiểm nghiệm trong thực chiến* cho phép họ bỏ qua các vấn đề lâu dài về kiến trúc và phát triển > *@sergeysova: Hãy tưởng tượng rằng, một developer viết code trong khuôn khổ của phương pháp và gặp vấn đề ít hơn 10 lần, đơn giản vì những người khác đã nghĩ ra giải pháp cho nhiều vấn đề.* ## Hạn chế[​](#hạn-chế "Link trực tiếp đến heading") Chúng tôi không muốn *ép buộc quan điểm của mình*, và đồng thời chúng tôi hiểu rằng *nhiều thói quen của chúng ta, với tư cách là developer, cản trở từ ngày này qua ngày khác* Mọi người đều có mức độ kinh nghiệm riêng trong việc thiết kế và phát triển hệ thống, **do đó, đáng làm hiểu những điều sau:** * **Sẽ không hoạt động**: rất đơn giản, rất rõ ràng, cho mọi người > *@sergeysova: Một số khái niệm không thể được hiểu một cách trực quan cho đến khi bạn gặp phải vấn đề và dành nhiều năm để giải quyết chúng.* > > * *Trong thế giới toán học: là graph theory.* > * *Trong vật lý: cơ học lượng tử.* > * *Trong lập trình: kiến trúc ứng dụng.* * **Có thể và mong muốn**: tính đơn giản, khả năng mở rộng ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [Các vấn đề kiến trúc](/documentation/vi/docs/about/understanding/architecture.md#problems) --- # Động lực Ý tưởng chính của **Feature-Sliced Design** là tạo điều kiện thuận lợi và giảm chi phí phát triển các dự án phức tạp và được phát triển, dựa trên việc [kết hợp kết quả nghiên cứu, thảo luận kinh nghiệm của các loại developer khác nhau trong phạm vi rộng](https://github.com/feature-sliced/documentation/discussions). Hiển nhiên, đây sẽ không phải là giải pháp vạn năng, và tất nhiên, phương pháp này sẽ có [giới hạn khả năng áp dụng](/documentation/vi/docs/about/mission.md) riêng của mình. Tuy nhiên, có những câu hỏi hợp lý về *tính khả thi của một phương pháp như vậy nói chung* ghi chú Chi tiết hơn [được thảo luận trong cuộc thảo luận](https://github.com/feature-sliced/documentation/discussions/27) ## Tại sao các giải pháp hiện có không đủ?[​](#tại-sao-các-giải-pháp-hiện-có-không-đủ "Link trực tiếp đến heading") > Thường là những lập luận này: > > * *"Tại sao bạn cần một methodology mới, khi đã có những phương pháp và nguyên tắc thiết kế lâu đời như `SOLID`, `KISS`, `YAGNI`, `DDD`, `GRASP`, `DRY`, v.v."* > * *"Tất cả các vấn đề được giải quyết bằng tài liệu dự án tốt, test và quy trình có cấu trúc"* > * *"Các vấn đề sẽ không xảy ra nếu tất cả developer đều tuân theo những điều trên"* > * *"Mọi thứ đã được phát minh trước bạn, bạn chỉ không biết sử dụng nó"* > * *"Hãy lấy {FRAMEWORK\_NAME} - mọi thứ đã được quyết định sẵn cho bạn ở đó"* ### Chỉ có nguyên tắc là chưa đủ[​](#chỉ-có-nguyên-tắc-là-chưa-đủ "Link trực tiếp đến heading") **Việc tồn tại các nguyên tắc là chưa đủ để thiết kế một kiến trúc tốt** Không phải ai cũng biết chúng một cách hoàn toàn, thậm chí ít người hơn hiểu và áp dụng chúng một cách chính xác *Các nguyên tắc thiết kế quá chung chung, và không đưa ra câu trả lời cụ thể cho câu hỏi: "Làm thế nào để thiết kế cấu trúc và kiến trúc của một ứng dụng có thể mở rộng và linh hoạt?"* ### Quy trình không phải lúc nào cũng hiệu quả[​](#quy-trình-không-phải-lúc-nào-cũng-hiệu-quả "Link trực tiếp đến heading") *Tài liệu/Test/Quy trình* tất nhiên là tốt, nhưng tiếc thay, ngay cả với chi phí cao cho chúng - **chúng không phải lúc nào cũng giải quyết được các vấn đề do kiến trúc đặt ra và việc đưa người mới vào dự án** * Thời gian để mỗi developer tham gia vào dự án không được giảm đáng kể, bởi vì tài liệu thường sẽ trở nên khổng lồ / lỗi thời * Liên tục đảm bảo rằng mọi người hiểu kiến trúc theo cùng một cách - điều này cũng đòi hỏi một lượng tài nguyên khổng lồ * Đừng quên về bus-factor ### Các framework hiện có không thể áp dụng ở mọi nơi[​](#các-framework-hiện-có-không-thể-áp-dụng-ở-mọi-nơi "Link trực tiếp đến heading") * Các giải pháp hiện có thường có ngưỡng vào cao, điều này khiến việc tìm kiếm developer mới trở nên khó khăn * Ngoài ra, phần lớn thời gian, việc lựa chọn công nghệ đã được xác định trước khi các vấn đề nghiêm trọng trong dự án xuất hiện, và do đó bạn cần có khả năng "làm việc với những gì có sẵn" - **mà không bị ràng buộc vào công nghệ** > Q: *"Trong dự án của tôi `React/Vue/Redux/Effector/Mobx/{YOUR_TECH}` - làm thế nào tôi có thể xây dựng tốt hơn cấu trúc của các entity và mối quan hệ giữa chúng?"* ### Kết quả[​](#kết-quả "Link trực tiếp đến heading") Chúng ta nhận được các dự án *"độc đáo như bông tuyết"*, mỗi dự án đều đòi hỏi nhân viên phải học hỏi lâu dài, và kiến thức không chắc có thể áp dụng được cho dự án khác > @sergeysova: *"Đây chính xác là tình huống hiện tại tồn tại trong lĩnh vực phát triển frontend của chúng ta: mỗi lead sẽ phát minh ra các kiến trúc và cấu trúc dự án khác nhau, trong khi không chắc rằng những cấu trúc này sẽ vượt qua được thử thách thời gian, kết quả là tối đa hai người có thể phát triển dự án ngoài anh ta, và mỗi developer mới cần phải được hướng dẫn lại từ đầu."* ## Tại sao các developer cần methodology?[​](#tại-sao-các-developer-cần-methodology "Link trực tiếp đến heading") ### Tập trung vào các tính năng kinh doanh, không phải vấn đề kiến trúc[​](#tập-trung-vào-các-tính-năng-kinh-doanh-không-phải-vấn-đề-kiến-trúc "Link trực tiếp đến heading") Methodology cho phép bạn tiết kiệm tài nguyên trong việc thiết kế một kiến trúc có thể mở rộng và linh hoạt, thay vào đó hướng sự chú ý của các developer vào việc phát triển chức năng chính. Đồng thời, các giải pháp kiến trúc được tiêu chuẩn hóa từ dự án này sang dự án khác. *Một câu hỏi riêng là methodology nên giành được sự tin tưởng của cộng đồng, để một developer khác có thể làm quen với nó và dựa vào nó trong việc giải quyết các vấn đề của dự án trong thời gian có sẵn* ### Giải pháp đã được kiểm chứng bằng kinh nghiệm[​](#giải-pháp-đã-được-kiểm-chứng-bằng-kinh-nghiệm "Link trực tiếp đến heading") Methodology được thiết kế cho các developer hướng tới *một giải pháp đã được chứng minh cho việc thiết kế logic kinh doanh phức tạp* *Tuy nhiên, rõ ràng là methodology nói chung là về một tập hợp các best-practice, bài viết giải quyết các vấn đề và trường hợp nhất định trong quá trình phát triển. Do đó, methodology cũng sẽ hữu ích cho các developer khác - những người bằng cách nào đó gặp phải vấn đề trong quá trình phát triển và thiết kế* ### Sức khỏe dự án[​](#sức-khỏe-dự-án "Link trực tiếp đến heading") Methodology sẽ cho phép *giải quyết và theo dõi các vấn đề của dự án trước, mà không đòi hỏi một lượng lớn tài nguyên* **Phần lớn thời gian, nợ kỹ thuật tích tụ và tích tụ theo thời gian, và trách nhiệm giải quyết nó nằm ở cả lead và team** Methodology sẽ cho phép bạn *cảnh báo* trước các vấn đề có thể xảy ra trong việc mở rộng và phát triển dự án ## Tại sao doanh nghiệp cần một methodology?[​](#tại-sao-doanh-nghiệp-cần-một-methodology "Link trực tiếp đến heading") ### Onboarding nhanh chóng[​](#onboarding-nhanh-chóng "Link trực tiếp đến heading") Với methodology, bạn có thể thuê một người vào dự án **đã quen thuộc với cách tiếp cận này trước đó, và không cần đào tạo lại** *Mọi người bắt đầu hiểu và đóng góp cho dự án nhanh hơn, và có thêm bảo đảm để tìm người cho các lần lặp tiếp theo của dự án* ### Giải pháp đã được kiểm chứng bằng kinh nghiệm[​](#giải-pháp-đã-được-kiểm-chứng-bằng-kinh-nghiệm-1 "Link trực tiếp đến heading") Với methodology, doanh nghiệp sẽ có *giải pháp cho hầu hết các vấn đề phát sinh trong quá trình phát triển hệ thống* Vì phần lớn thời gian doanh nghiệp muốn có một framework / giải pháp có thể giải quyết phần lớn các vấn đề trong quá trình phát triển dự án ### Khả năng áp dụng cho các giai đoạn khác nhau của dự án[​](#khả-năng-áp-dụng-cho-các-giai-đoạn-khác-nhau-của-dự-án "Link trực tiếp đến heading") Methodology có thể mang lại lợi ích cho dự án *cả ở giai đoạn hỗ trợ và phát triển dự án, và ở giai đoạn MVP* Vâng, điều quan trọng nhất đối với MVP là *"các tính năng, không phải kiến trúc được đặt nền cho tương lai"*. Nhưng ngay cả trong điều kiện deadline hạn chế, việc biết các best-practice từ methodology, bạn có thể *"làm với ít máu"*, khi thiết kế phiên bản MVP của hệ thống, tìm ra một sự thỏa hiệp hợp lý (thay vì mô hình hóa các tính năng "một cách ngẫu nhiên") *Điều tương tự có thể nói về testing* ## Khi nào methodology của chúng tôi không cần thiết?[​](#khi-nào-methodology-của-chúng-tôi-không-cần-thiết "Link trực tiếp đến heading") * Nếu dự án sẽ tồn tại trong thời gian ngắn * Nếu dự án không cần kiến trúc được hỗ trợ * Nếu doanh nghiệp không nhận thấy mối liên kết giữa code base và tốc độ phát triển tính năng * Nếu đối với doanh nghiệp việc đóng các đơn hàng càng sớm càng tốt, mà không cần hỗ trợ thêm ### Qôy mô doanh nghiệp[​](#qôy-mô-doanh-nghiệp "Link trực tiếp đến heading") * **Doanh nghiệp nhỏ** - thường cần một giải pháp có sẵn và rất nhanh. Chỉ khi doanh nghiệp phát triển (ít nhất là gần trung bình), họ mới hiểu rằng để khách hàng tiếp tục sử dụng, cần thiết, trong số những điều khác, là dành thời gian cho chất lượng và tính ổn định của các giải pháp đang được phát triển * **Doanh nghiệp vừa** - thường hiểu tất cả các vấn đề của phát triển, và ngay cả khi cần thiết *"đua tốc để có các tính năng"*, họ vẫn dành thời gian cho việc cải thiện chất lượng, refactoring và test (và tất nhiên - cho kiến trúc có thể mở rộng) * **Doanh nghiệp lớn** - thường đã có đối tượng rộng rãi, nhân sự, và một tập hợp các thực hành rộng rãi hơn nhiều, và có thể thậm chí có cách tiếp cận kiến trúc riêng của họ, nên ý tưởng sử dụng của người khác không đến với họ thường xuyên ## Kế hoạch[​](#kế-hoạch "Link trực tiếp đến heading") Phần chính của các mục tiêu [được trình bày ở đây](/documentation/vi/docs/about/mission.md#goals), nhưng ngoài ra, đáng nói về kỳ vọng của chúng tôi từ methodology trong tương lai ### Kết hợp kinh nghiệm[​](#kết-hợp-kinh-nghiệm "Link trực tiếp đến heading") Hiện tại chúng tôi đang cố gắng kết hợp tất cả kinh nghiệm đa dạng của `core-team`, và có được một methodology được rèn luyện bằng thực hành Tất nhiên, kết quả có thể là Angular 3.0, nhưng quan trọng hơn ở đây là **nghiên cứu chính vấn đề thiết kế kiến trúc của các hệ thống phức tạp** *Và vâng - chúng tôi có phàn nàn về phiên bản hiện tại của methodology, nhưng chúng tôi muốn làm việc cùng nhau để đi đến một giải pháp duy nhất và tối ưu (tính đến, trong số những điều khác, kinh nghiệm của cộng đồng)* ### Cuộc sống ngoài specification[​](#cuộc-sống-ngoài-specification "Link trực tiếp đến heading") Nếu mọi thứ điễn ra tốt, thì methodology sẽ không chỉ giới hạn trong specification và toolkit * Có thể sẽ có báo cáo, bài viết * Có thể có `CODE_MOD` cho việc migration sang các công nghệ khác của các dự án được viết theo methodology * Có thể kết quả là chúng ta sẽ có thể tiếp cận được các maintainer của các giải pháp công nghệ lớn * *Đặc biệt cho React, so với các framework khác - đây là vấn đề chính, vì nó không nói cách giải quyết những vấn đề nhất định* ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Thảo luận) Không cần methodology?](https://github.com/feature-sliced/documentation/discussions/27) * [Về sứ mệnh của methodology: mục tiêu và giới hạn](/documentation/vi/docs/about/mission.md) * [Các loại kiến thức trong dự án](/documentation/vi/docs/about/understanding/knowledge-types.md) --- # Thúc đẩy trong công ty WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/206) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Dự án và công ty có cần methodology không?[​](#dự-án-và-công-ty-có-cần-methodology-không "Link trực tiếp đến heading") > Về việc chứng minh tính ứng dụng, Những nhiệm vụ đó ## Làm thế nào để trình bày methodology cho doanh nghiệp?[​](#làm-thế-nào-để-trình-bày-methodology-cho-doanh-nghiệp "Link trực tiếp đến heading") ## Làm thế nào để chuẩn bị và chứng minh kế hoạch chuyển sang methodology?[​](#làm-thế-nào-để-chuẩn-bị-và-chứng-minh-kế-hoạch-chuyển-sang-methodology "Link trực tiếp đến heading") --- # Thúc đẩy trong team WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/182) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* * Onboard những người mới * Hướng dẫn phát triển ("tìm kiếm module N ở đâu", v.v...) * Cách tiếp cận mới cho các task ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Thread) Sự đơn giản của các cách tiếp cận cũ và tầm quan trọng của sự tỉnh táo](https://t.me/feature_sliced/3360) * [(Thread) Về sự tiện lợi của việc tìm kiếm theo layer](https://t.me/feature_sliced/1918) --- # Các khía cạnh tích hợp ## Tóm tắt[​](#tóm-tắt "Link trực tiếp đến heading") 5 phút đầu (RU): [YouTube video player](https://www.youtube.com/embed/TFA6zRO_Cl0?start=2110) ## Ngoài ra[​](#ngoài-ra "Link trực tiếp đến heading") **Ưu điểm**: * [Tổng quan](/documentation/vi/docs/get-started/overview.md) * CodeReview * Onboarding **Nhược điểm:** * Độ phức tạp về mặt tư duy * Ngưỡng vào cao * "Layer hell" * Các vấn đề điển hình của các cách tiếp cận dựa trên feature --- # Ứng dụng từng phần WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/199) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Làm thế nào để áp dụng methodology từng phần? Có hợp lý không? Nếu tôi bỏ qua thì sao? --- # Abstraction WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/186) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Quy luật của abstraction bị rò rỉ[​](#quy-luật-của-abstraction-bị-rò-rỉ "Link trực tiếp đến heading") ## Tại sao có quá nhiều abstraction[​](#tại-sao-có-quá-nhiều-abstraction "Link trực tiếp đến heading") > Abstraction giúp đối phó với độ phức tạp của dự án. Câu hỏi là - những abstraction này sẽ chỉ dành riêng cho dự án này, hay chúng ta sẽ cố gắng rút ra những abstraction chung dựa trên đặc thù của frontend > Kiến trúc và ứng dụng nói chung vốn phức tạp, và câu hỏi duy nhất là làm thế nào để phân phối và mô tả tốt hơn độ phức tạp này ## Về phạm vi trách nhiệm[​](#về-phạm-vi-trách-nhiệm "Link trực tiếp đến heading") > Về các abstraction tùy chọn ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [Về nhu cầu cho các layer mới](https://t.me/feature_sliced/2801) * [Về khó khăn trong việc hiểu methodology và các layer](https://t.me/feature_sliced/2619) --- # Về kiến trúc ## Các vấn đề[​](#các-vấn-đề "Link trực tiếp đến heading") Thường thì cuộc trò chuyện về kiến trúc được nêng lên khi việc phát triển dừng lại do một số vấn đề nhất định trong dự án. ### Bus-factor & Onboarding[​](#bus-factor--onboarding "Link trực tiếp đến heading") Chỉ có một số lượng hạn chế người hiểu dự án và kiến trúc của nó **Ví dụ:** * *"Khó đưa người vào phát triển"* * *"Đối với mọi vấn đề, mọi người đều có ý kiến riêng về cách giải quyết" (hãy ghen tị với angular)* * *"Tôi không hiểu chuyện gì đang xảy ra trong khối monolith lớn này"* ### Hậu quả ngầm định và không kiểm soát được[​](#hậu-quả-ngầm-định-và-không-kiểm-soát-được "Link trực tiếp đến heading") Nhiều tác dụng phụ ngầm định trong quá trình phát triển/refactoring *("tất cả đều phụ thuộc vào nhau")* **Ví dụ:** * *"Feature import feature"* * *"Tôi cập nhật store của một trang, và chức năng ở trang khác bị rơi"* * *"Logic bị tráng đầy khắp ứng dụng, và không thể theo dõi được đâu là đầu, đâu là cuối"* ### Tái sử dụng logic không kiểm soát được[​](#tái-sử-dụng-logic-không-kiểm-soát-được "Link trực tiếp đến heading") Khó khăn trong việc tái sử dụng/sửa đổi logic hiện có Đồng thời, thường có [hai thái cực](https://github.com/feature-sliced/documentation/discussions/14): * Hoặc logic được viết hoàn toàn từ đầu cho mỗi module *(với khả năng lặp lại trong codebase hiện có)* * Hoặc có xu hướng chuyển tất-tất cả các module đã triển khai vào thư mục `shared`, từ đó tạo ra một kho chứa lớn các module *từ nó (trong đó hầu hết chỉ được sử dụng ở một nơi)* **Ví dụ:** * *"Tôi có **N** cách triển khai cùng một logic kinh doanh trong dự án của mình, mà tôi vẫn phải trả giá"* * *"Có 6 component khác nhau của button/pop-up/... trong dự án"* * *"Kho chứa các helper"* ## Yêu cầu[​](#yêu-cầu "Link trực tiếp đến heading") Do đó, có vẻ hợp lý khi trình bày các *yêu cầu mong muốn cho một kiến trúc lý tưởng:* ghi chú Bất cứ đâu nói "dễ dàng", điều đó có nghĩa là "tương đối dễ dàng cho một nhóm rộng các developer", vì rõ ràng là [sẽ không thể tạo ra một giải pháp lý tưởng cho tất cả mọi người](/documentation/vi/docs/about/mission.md#limitations) ### Tính rõ ràng[​](#tính-rõ-ràng "Link trực tiếp đến heading") * Nên **dễ dàng nắm vững và giải thích** dự án và kiến trúc của nó cho team * Cấu trúc nên phản ánh **giá trị kinh doanh thực tế của dự án** * Phải có **tác dụng phụ và kết nối** rõ ràng giữa các abstraction * Nên **dễ phát hiện logic trùng lặp** mà không can thiệp vào các triển khai độc đáo * Không nên có **sự phân tán logic** khắp dự án * Không nên có **quá nhiều abstraction và quy tắc khác biệt** cho một kiến trúc tốt ### Kiểm soát[​](#kiểm-soát "Link trực tiếp đến heading") * Một kiến trúc tốt nên **tăng tốc giải quyết các tác vụ, việc đưa vào các tính năng** * Nên có thể kiểm soát quá trình phát triển dự án * Nên dễ dàng **mở rộng, sửa đổi, xóa code** * Phải tuân thủ việc **phân tách và cô lập** chức năng * Mỗi component của hệ thống phải **dễ dàng thay thế và loại bỏ** * *[Không cần tối ưu hóa cho thay đổi](https://youtu.be/BWAeYuWFHhs?t=1631) - chúng ta không thể dự đoán tương lai* * *[Tốt hơn là tối ưu hóa cho việc xóa](https://youtu.be/BWAeYuWFHhs?t=1666) - dựa trên bối cảnh đã tồn tại* ### Khả năng thích ứng[​](#khả-năng-thích-ứng "Link trực tiếp đến heading") * Một kiến trúc tốt nên có thể áp dụng **cho hầu hết các dự án** * *Với các giải pháp hạ tầng hiện có* * *ở bất kỳ giai đoạn phát triển nào* * Không nên phụ thuộc vào framework và nền tảng * Nên có thể **dễ dàng mở rộng dự án và team**, với khả năng song song hóa phát triển * Nên dễ dàng **thích ứng với các yêu cầu và hoàn cảnh thay đổi** ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(React Berlin Talk) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs) * [(React SPB Meetup #1) Sergey Sova - Feature Slice](https://t.me/feature_slices) * [(Bài viết) Về việc modular hóa dự án](https://alexmngn.medium.com/why-react-developers-should-modularize-their-applications-d26d381854c1) * [(Bài viết) Về Separation of Concern và cấu trúc theo feature](https://ryanlanciaux.com/blog/2017/08/20/a-feature-based-approach-to-react-development/) --- # Các loại kiến thức trong dự án Có thể phân biệt các "loại kiến thức" sau trong bất kỳ dự án nào: * **Kiến thức cơ bản**
Kiến thức không thay đổi nhiều theo thời gian, chẳng hạn như thuật toán, khoa học máy tính, cơ chế ngôn ngữ lập trình và API của nó. * **Technology stack**
Kiến thức về tập hợp các giải pháp kỹ thuật được sử dụng trong dự án, bao gồm ngôn ngữ lập trình, framework và thư viện. * **Kiến thức dự án**
Kiến thức dành riêng cho dự án hiện tại và không có giá trị bên ngoài nó. Kiến thức này là cần thiết để các developer mới onboard có thể đóng góp hiệu quả. ghi chú **Feature-Sliced Design** được thiết kế để giảm sự phụ thuộc vào "kiến thức dự án", chịu trách nhiệm nhiều hơn và giúp onboard các thành viên team mới dễ dàng hơn. ## Xem thêm[​](#see-also "Link trực tiếp đến heading") * [(Video 🇷🇺) Ilya Klimov - Về các loại kiến thức](https://youtu.be/4xyb_tA-uw0?t=249) --- # Đặt tên Các developer khác nhau có kinh nghiệm và ngữ cảnh khác nhau, có thể dẫn đến hiểu lầm trong team khi cùng một entity được gọi khác nhau. Ví dụ: * Component để hiển thị có thể được gọi là "ui", "component", "ui-kit", "view", … * Code được tái sử dụng trong toàn bộ ứng dụng có thể được gọi là "core", "shared", "app", … * Code logic kinh doanh có thể được gọi là "store", "model", "state", … ## Đặt tên trong Feature-Sliced Design[​](#naming-in-fsd "Link trực tiếp đến heading") Methodology sử dụng các thuật ngữ cụ thể như: * "app", "process", "page", "feature", "entity", "shared" như tên layer, * "ui", "model", "lib", "api", "config" như tên segment. Rất quan trọng là phải tuân thủ những thuật ngữ này để tránh nhầm lẫn giữa các thành viên trong team và developer mới tham gia dự án. Việc sử dụng tên chuẩn cũng giúp khi xin trợ giúp từ cộng đồng. ## Xung đột đặt tên[​](#when-can-naming-interfere "Link trực tiếp đến heading") Xung đột đặt tên có thể xảy ra khi các thuật ngữ được sử dụng trong methodology FSD trùng lặp với các thuật ngữ được sử dụng trong kinh doanh: * `FSD#process` vs quy trình mô phỏng trong ứng dụng, * `FSD#page` vs trang log, * `FSD#model` vs model xe hơi. Ví dụ, một developer nhìn thấy từ "process" trong code sẽ tốn thêm thời gian để tìm hiểu quy trình nào đang được đề cập. Những **xung đột như vậy có thể làm gián đoạn quá trình phát triển**. Khi từ vựng của dự án chứa thuật ngữ đặc thù của FSD, điều quan trọng là phải cẩn thận khi thảo luận các thuật ngữ này với team và các bên liên quan không quan tâm đến kỹ thuật. Để giao tiếp hiệu quả với team, được khuyến nghị sử dụng từ viết tắt "FSD" làm tiền tố cho các thuật ngữ methodology. Ví dụ, khi nói về một process, bạn có thể nói, "Chúng ta có thể đặt process này trên layer feature của FSD." Ngược lại, khi giao tiếp với các bên liên quan không thuộc kỹ thuật, tốt hơn là hạn chế sử dụng thuật ngữ FSD và tránh đề cập đến cấu trúc bên trong của codebase. ## Xem thêm[​](#see-also "Link trực tiếp đến heading") * [(Thảo luận) Khả năng thích ứng của việc đặt tên](https://github.com/feature-sliced/documentation/discussions/16) * [(Thảo luận) Khảo sát đặt tên Entity](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-464894) * [(Thảo luận) "process" vs "flow" vs ...](https://github.com/feature-sliced/documentation/discussions/20) * [(Thảo luận) "model" vs "store" vs ...](https://github.com/feature-sliced/documentation/discussions/68) --- # Hướng nhu cầu Tóm tắt — *Không thể đưa ra mục tiêu mà tính năng mới sẽ giải quyết? Hoặc có thể vấn đề là bản thân task không được đưa ra? **Điềm mấu chốt cũng là methodology giúp rút ra định nghĩa có vấn đề của các task và mục tiêu*** — *dự án không sống trong tĩnh - yêu cầu và chức năng liên tục thay đổi. Theo thời gian, code biến thành nhệu, bởi vì lúc bắt đầu dự án chỉ được thiết kế cho ấn tượng ban đầu của mong muốn. **Và nhiệm vụ của một kiến trúc tốt cũng là được mài giũa cho các điều kiện phát triển thay đổi.*** ## Tại sao?[​](#tại-sao "Link trực tiếp đến heading") Để chọn một tên rõ ràng cho một entity và hiểu các thành phần của nó, **bạn cần hiểu rõ task nào sẽ được giải quyết bằng tất cả code này.** > *@sergeysova: Trong quá trình phát triển, chúng tôi cố gắng đặt cho mỗi entity hoặc function một tên rõ ràng phản ánh ý định và ý nghĩa của code đang được thực thi.* *Sau cùng, không hiểu task thì không thể viết được test đúng bao phủ những trường hợp quan trọng nhất, đặt các lỗi giúp đỡ người dùng ở đúng chỗ, thậm chí là thiếu không làm gián đoạn luồng của người dùng vì các lỗi không quan trọng có thể sửa được.* ## Chúng ta đang nói về task gì?[​](#chúng-ta-đang-nói-về-task-gì "Link trực tiếp đến heading") Frontend phát triển ứng dụng và giao diện cho người dùng cuối, nên chúng ta giải quyết các task của những người tiêu dùng này. Khi một người đến với chúng ta, **anh ta muốn giải quyết một số đau khổ của mình hoặc đóng một nhu cầu.** *Nhiệm vụ của các manager và analyst là đưa ra nhu cầu này, và triển khai developer tính đến các tính năng của phát triển web (mất kết nối, lỗi backend, typo, nhấm con trỏ hoặc ngón tay).* **Chính mục tiêu này, mà người dùng đến, là task của các developer.** > *Một vấn đề nhỏ được giải quyết là một feature trong methodology Feature-Sliced Design — bạn cần cắt toàn bộ phạm vi task của dự án thành các mục tiêu nhỏ.* ## Điều này ảnh hưởng đến phát triển như thế nào?[​](#điều-này-ảnh-hưởng-đến-phát-triển-như-thế-nào "Link trực tiếp đến heading") ### Phân tách task[​](#phân-tách-task "Link trực tiếp đến heading") Khi một developer bắt đầu triển khai một task, để đơn giản hóa việc hiểu và hỗ trợ code, anh ta trong đầu **cắt nó thành các giai đoạn**: * đầu tiên *chia thành các entity cấp cao nhất* và *triển khai chúng*, * sau đó những entity này *chia thành những cái nhỏ hơn* * và cứ tiếp tục như vậy *Trong quá trình chia thành các entity, developer bị buộc phải đặt tên cho chúng một cách rõ ràng phản ánh ý tưởng của mình và giúp hiểu task nào code giải quyết khi đọc listing* *Đồng thời, chúng ta không quên rằng chúng ta đang cố gắng giúp người dùng giảm bớt đau khổ hoặc thực hiện nhu cầu* ### Hiểu bản chất của task[​](#hiểu-bản-chất-của-task "Link trực tiếp đến heading") Nhưng để đặt tên rõ ràng cho một entity, **developer phải biết đủ về mục đích của nó** * anh ta sẽ sử dụng entity này như thế nào, * nó triển khai phần nào của task của người dùng, entity này còn có thể được áp dụng ở đâu khác, * nó có thể tham gia vào những task nào khác, * và vân vân Không khó để rút ra kết luận: **trong khi developer sẽ suy ngẫm về tên của các entity trong khuôn khổ của methodology, anh ta sẽ có thể tìm ra các task được đưa ra kém thậm chí trước khi viết code.** > Làm thế nào đặt tên cho một entity nếu bạn không hiểu rõ những task nào nó có thể giải quyết, làm thế nào bạn có thể chia một task thành các entity nếu bạn không hiểu rõ nó? ## Làm thế nào để đưa ra nó?[​](#làm-thế-nào-để-đưa-ra-nó "Link trực tiếp đến heading") **Để đưa ra một task được giải quyết bằng các feature, bạn cần hiểu bản thân task đó**, và đây đã là trách nhiệm của project manager và các analyst. *Methodology chỉ có thể nói cho developer những task nào mà product manager nên chú ý kỹ.* > *@sergeysova: Toàn bộ frontend chủ yếu là hiển thị thông tin, bất kỳ component nào ở lượt đầu tiên, hiển thị, và sau đó task "hiển thị cho người dùng một cái gì đó" không có giá trị thực tế.* > > *Ngay cả khi không tính đến đặc thù của frontend có thể hỏi, "tại sao tôi phải hiển thị cho bạn", và có thể tiếp tục hỏi cho đến khi không thoát được khỏi đau khổ hoặc nhu cầu của người tiêu dùng.* Người khi chúng ta có thể đến được những nhu cầu hoặc đau khổ cơ bản, chúng ta có thể quay lại và tìm hiểu **chính xác sản phẩm hoặc dịch vụ của bạn có thể giúp người dùng với mục tiêu của họ như thế nào** Bất kỳ task mới nào trong tracker của bạn đều hướng đến giải quyết các vấn đề kinh doanh, và doanh nghiệp cố gắng giải quyết các task của người dùng đồng thời kiếm tiền từ nó. Điều này có nghĩa là mỗi task đều có những mục tiêu nhất định, ngay cả khi chúng không được viết rõ trong văn bản mô tả. ***Developer phải hiểu rõ mục tiêu mà task này hay task khác đang theo đuổi**, nhưng không phải công ty nào cũng có thể đủ khả năng xây dựng quy trình hoàn hảo, mặc dù đây là một cuộc trò chuyện riêng, tuy nhiên, developer có thể "ping" các manager phù hợp để tìm hiểu điều này và thực hiện phần việc của mình một cách hiệu quả.* ## Và lợi ích là gì?[​](#và-lợi-ích-là-gì "Link trực tiếp đến heading") Bây giờ hãy nhìn toàn bộ quy trình từ đầu đến cuối. ### 1. Hiểu task của người dùng[​](#1-hiểu-task-của-người-dùng "Link trực tiếp đến heading") Khi developer hiểu nỗi đau của họ và cách doanh nghiệp giải quyết chúng, anh ta có thể đưa ra các giải pháp mà doanh nghiệp không có do đặc thù của phát triển web. > Nhưng tất nhiên, tất cả điều này chỉ có thể hoạt động nếu developer không thờ ơ với những gì anh ta đang làm và vì mục đích gì, nếu không thì *tại sao lại cần methodology và các cách tiếp cận?* ### 2. Cấu trúc hóa và sắp xếp[​](#2-cấu-trúc-hóa-và-sắp-xếp "Link trực tiếp đến heading") Với sự hiểu biết về task đến **một cấu trúc rõ ràng cả trong đầu và trong task cùng với code** ### 3. Hiểu feature và các thành phần của nó[​](#3-hiểu-feature-và-các-thành-phần-của-nó "Link trực tiếp đến heading") **Một feature là một chức năng hữu ích cho người dùng** * Khi nhiều features được triển khai trong một feature, đây là **vi phạm ranh giới** * Feature có thể không thể chia nhỏ và đang phát triển - **và điều này không tệ** * **Tệ** - khi feature không trả lời câu hỏi *"Giá trị kinh doanh cho người dùng là gì?"* * Không thể có feature "map-office" * Nhưng `booking-meeting-on-the-map`, `search-for-an-employee`, `change-of-workplace` - **có** > *@sergeysova: Điểm mấu chốt là feature chỉ chứa code triển khai chính chức năng*, không có chi tiết không cần thiết và giải pháp nội bộ (lý tưởng)\* > > *Mở code feature **và chỉ thấy những gì liên quan đến task** - không hơn* ### 4. Lợi ích[​](#4-lợi-ích "Link trực tiếp đến heading") Doanh nghiệp rất hiếm khi xoay chuyển hướng đi hoàn toàn sang hướng khác, có nghĩa là **phản ánh các task kinh doanh trong code ứng dụng frontend là lợi ích rất đáng kể.** *Sau đó bạn không phải giải thích cho mỗi thành viên mới trong team code này hay code kia làm gì, và nói chung tại sao nó được thêm vào - **mọi thứ sẽ được giải thích thông qua các task kinh doanh đã được phản ánh trong code.*** > Cái được gọi là ["Ngôn ngữ kinh doanh" trong Domain Driven Development](https://thedomaindrivendesign.io/developing-the-ubiquitous-language) *** ## Quay lại thực tế[​](#quay-lại-thực-tế "Link trực tiếp đến heading") Nếu quy trình kinh doanh được hiểu và đặt tên tốt ở giai đoạn thiết kế - *thì không có vấn đề gì đặc biệt khi chuyển sự hiểu biết và logic này vào code.* **Tuy nhiên, trong thực tế**, task và chức năng thường được phát triển "quá" lặp đi lặp lại và (hoặc) không có thời gian để suy nghĩ kỹ về thiết kế. **Kết quả là, feature có ý nghĩa hôm nay, và nếu bạn mở rộng feature này trong một tháng, bạn có thể phải viết lại nửa dự án.** > *\[[Từ cuộc thảo luận](https://t.me/sergeysova/318)]: Developer cố gắng suy nghĩ trước 2-3 bước, tính đến những mong muốn trong tương lai, nhưng ở đây anh ta dựa vào kinh nghiệm của riêng mình* > > *Kỹ sư có kinh nghiệm thường ngay lập tức nhìn trước 10 bước, và hiểu nơi nào để chia một feature và kết hợp với feature khác* > > *Nhưng đôi khi có những task mà chưa từng gặp trong kinh nghiệm, và không biết lấy đâu ra sự hiểu biết về cách phân tách hợp lý, với hậu quả không mong muốn ít nhất trong tương lai* ## Vai trò của methodology[​](#vai-trò-của-methodology "Link trực tiếp đến heading") **Methodology giúp giải quyết vấn đề của developers, để dễ dàng hơn giải quyết vấn đề của người dùng.** Không có giải pháp cho vấn đề của developers chỉ vì lợi ích của developers Nhưng để developer giải quyết task của mình, **bạn cần hiểu task của người dùng** - ngược lại sẽ không hoạt động ### Yêu cầu của methodology[​](#yêu-cầu-của-methodology "Link trực tiếp đến heading") Rõ ràng là bạn cần xác định ít nhất hai yêu cầu cho **Feature-Sliced Design**: 1. Methodology nên chỉ ra **cách tạo features, processes và entities** * Có nghĩa là nó nên giải thích rõ ràng *cách chia code giữa chúng*, có nghĩa là việc đặt tên các entities này cũng nên được quy định trong specification. 2. Methodology nên giúp kiến trúc **[dễ dàng thích ứng với yêu cầu thay đổi của dự án](/documentation/vi/docs/about/understanding/architecture.md#adaptability)** ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Bài viết) Kích thích cho việc xây dựng task rõ ràng (+ thảo luận)](https://t.me/sergeysova/318) > ***Bài viết hiện tại** là bản chuyển thể của cuộc thảo luận này, bạn có thể đọc phiên bản đầy đủ không cắt tại liên kết* * [(Thảo luận) Cách chia chức năng và nó là gì](https://t.me/atomicdesign/18972) * [(Bài viết) "Cách tổ chức ứng dụng của bạn tốt hơn"](https://alexmngn.medium.com/how-to-better-organize-your-react-applications-2fd3ea1920f1) --- # Signal của kiến trúc WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/194) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Nếu có giới hạn về phía kiến trúc, thì có những lý do rõ ràng cho điều này, và hậu quả nếu chúng bị bỏ qua > Methodology và kiến trúc đưa ra signal, và cách xử lý nó phụ thuộc vào những rủi ro bạn sẵn sàng đảm nhận và điều gì phù hợp nhất cho team của bạn) ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Thread) Về signal từ kiến trúc và dataflow](https://t.me/feature_sliced/2070) * [(Thread) Về tính chất cơ bản của kiến trúc](https://t.me/feature_sliced/2492) * [(Thread) Về việc làm nổi bật các điểm yếu](https://t.me/feature_sliced/3979) * [(Thread) Làm thế nào để hiểu rằng data model bị phình to](https://t.me/feature_sliced/4228) --- # Hướng dẫn Thương hiệu Bản sắc thị giác của FSD dựa trên các khái niệm cốt lõi: `Layered`, `Sliced self-contained parts`, `Parts & Compose`, `Segmented`. Nhưng chúng tôi cũng có xu hướng thiết kế bản sắc đơn giản, đẹp mắt, thể hiện triết lý FSD và dễ nhận biết. **Vui lòng sử dụng bản sắc của FSD "nguyên trạng", không thay đổi nhưng với các tài sản của chúng tôi để thuận tiện cho bạn.** Hướng dẫn thương hiệu này sẽ giúp bạn sử dụng bản sắc FSD một cách chính xác. Tương thích FSD trước đây đã có [một bản sắc cũ khác](https://drive.google.com/drive/folders/11Y-3qZ_C9jOFoW2UbSp11YasOhw4yBdl?usp=sharing). Thiết kế cũ không thể hiện được các khái niệm cốt lõi của phương pháp luận. Ngoài ra nó chỉ được tạo như một bản nháp thuần túy, và đáng lẽ phải được hiện thực hóa. Để sử dụng thương hiệu tương thích và lâu dài, chúng tôi đã cẩn thận thực hiện tái thương hiệu trong một năm (2021-2022). **Vì vậy bạn có thể yên tâm khi sử dụng bản sắc của FSD 🍰** *Nhưng hãy ưu tiên bản sắc hiện tại, không phải cũ!* ## Tiêu đề[​](#tiêu-đề "Link trực tiếp đến heading") * ✅ **Đúng:** `Feature-Sliced Design`, `FSD` * ❌ **Sai:** `Feature-Sliced`, `Feature Sliced`, `FeatureSliced`, `feature-sliced`, `feature sliced`, `FS` ## Emoji[​](#emoji "Link trực tiếp đến heading") Hình ảnh bánh 🍰 thể hiện khá tốt các khái niệm cốt lõi của FSD, nên nó đã được chọn làm emoji đặc trưng của chúng tôi > Ví dụ: *"🍰 Phương pháp luận thiết kế kiến trúc cho các dự án Frontend"* ## Logo & Bảng màu[​](#logo--bảng-màu "Link trực tiếp đến heading") FSD có một vài biến thể logo cho các ngữ cảnh khác nhau, nhưng được khuyến nghị ưu tiên **primary** | | | | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | Theme | Logo (Ctrl/Cmd + Click để tải xuống) | Sử dụng | | primary
(#29BEDC, #517AED) | [![logo-primary](/documentation/vi/img/brand/logo-primary.png)](/documentation/vi/img/brand/logo-primary.png) | Ưu tiên trong hầu hết trường hợp | | flat
(#3193FF) | [![logo-flat](/documentation/vi/img/brand/logo-flat.png)](/documentation/vi/img/brand/logo-flat.png) | Cho ngữ cảnh một màu | | monochrome
(#FFF) | [![logo-monocrhome](/documentation/vi/img/brand/logo-monochrome.png)](/documentation/vi/img/brand/logo-monochrome.png) | Cho ngữ cảnh thang màu xám | | square
(#3193FF) | [![logo-square](/documentation/vi/img/brand/logo-square.png)](/documentation/vi/img/brand/logo-square.png) | Cho biên hình vuông | ## Banner & Sơ đồ[​](#banner--sơ-đồ "Link trực tiếp đến heading") [![banner-primary](/documentation/vi/img/brand/banner-primary.jpg)](/documentation/vi/img/brand/banner-primary.jpg) [![banner-monochrome](/documentation/vi/img/brand/banner-monochrome.jpg)](/documentation/vi/img/brand/banner-monochrome.jpg) ## Social Preview[​](#social-preview "Link trực tiếp đến heading") Đang trong quá trình hoàn thiện... ## Template thuyết trình[​](#template-thuyết-trình "Link trực tiếp đến heading") Đang trong quá trình hoàn thiện... ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [Discussion (github)](https://github.com/feature-sliced/documentation/discussions/399) * [Lịch sử phát triển với tham khảo (figma)](https://www.figma.com/file/RPphccpoeasVB0lMpZwPVR/FSD-Brand?node-id=0%3A1) --- # Decomposition cheatsheet Sử dụng làm tài liệu tham khảo nhanh khi bạn quyết định cách decompose UI của mình. Phiên bản PDF cũng có sẵn bên dưới, để bạn có thể in ra và giữ một bản dưới gối. ## Chọn layer[​](#chọn-layer "Link trực tiếp đến heading") [Download PDF](/documentation/vi/assets/files/choosing-a-layer-en-12fdf3265c8fc4f6b58687352b81fce7.pdf) ![Definitions of all layers and self-check questions](/documentation/vi/assets/images/choosing-a-layer-en-5b67f20bb921ba17d78a56c0dc7654a9.jpg) ## Ví dụ[​](#ví-dụ "Link trực tiếp đến heading") ### Tweet[​](#tweet "Link trực tiếp đến heading") ![decomposed-tweet-bordered-bgLight](/documentation/vi/assets/images/decompose-twitter-7b9a50f879d763c49305b3bf0751ee35.png) ### GitHub[​](#github "Link trực tiếp đến heading") ![decomposed-github-bordered](/documentation/vi/assets/images/decompose-github-a0eeb839a4b5ef5c480a73726a4451b0.jpg) ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Thread) Logic tổng quát cho feature và entity](https://t.me/feature_sliced/4262) * [(Thread) Decomposition của logic phình to](https://t.me/feature_sliced/4210) * [(Thread) Về việc hiểu các vùng trách nhiệm trong decomposition](https://t.me/feature_sliced/4088) * [(Thread) Decomposition của Product List widget](https://t.me/feature_sliced/3828) * [(Article) Các cách tiếp cận khác nhau cho decomposition của logic](https://www.pluralsight.com/guides/how-to-organize-your-react-+-redux-codebase) * [(Thread) Về sự khác biệt giữa feature và entity](https://t.me/feature_sliced/3776) * [(Thread) Về sự khác biệt giữa things và entity (2)](https://t.me/feature_sliced/3248) * [(Thread) Về việc áp dụng tiêu chí cho decomposition](https://t.me/feature_sliced/3833) --- # FAQ thông tin Bạn có thể đặt câu hỏi trong [Telegram chat](https://t.me/feature_sliced), [Discord community](https://discord.gg/S8MzWTUsmp), và [GitHub Discussions](https://github.com/feature-sliced/documentation/discussions) của chúng tôi. ### Có toolkit hay linter nào không?[​](#có-toolkit-hay-linter-nào-không "Link trực tiếp đến heading") Có! Chúng tôi có linter tên là [Steiger](https://github.com/feature-sliced/steiger) để kiểm tra kiến trúc project của bạn và có [folder generator](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) thông qua CLI hoặc IDE. ### Đặt layout/template của trang ở đâu?[​](#đặt-layouttemplate-của-trang-ở-đâu "Link trực tiếp đến heading") Nếu bạn cần layout markup thuần túy, bạn có thể giữ chúng trong `shared/ui`. Nếu bạn cần sử dụng các layer cao hơn bên trong, có một vài lựa chọn: * Có lẽ bạn không cần layout chút nào? Nếu layout chỉ có vài dòng, có thể hợp lý hơn là duplicate code trong mỗi trang thay vì cố gắng trừu tượng hóa nó. * Nếu bạn thực sự cần layout, bạn có thể có chúng như các widget hoặc trang riêng biệt, và compose chúng trong cấu hình router ở App. Nested routing là một lựa chọn khác. ### Sự khác biệt giữa feature và entity là gì?[​](#sự-khác-biệt-giữa-feature-và-entity-là-gì "Link trực tiếp đến heading") *Entity* là khái niệm thực tế mà app của bạn đang làm việc với. *Feature* là tương tác cung cấp giá trị thực tế cho người dùng app của bạn, điều mà mọi người muốn làm với các entity của bạn. Để biết thêm thông tin cùng với ví dụ, xem trang Reference về [slice](/documentation/vi/docs/reference/layers.md#entities). ### Tôi có thể embed page/feature/entity vào nhau không?[​](#tôi-có-thể-embed-pagefeatureentity-vào-nhau-không "Link trực tiếp đến heading") Có, nhưng việc embedding này nên xảy ra ở các layer cao hơn. Ví dụ, bên trong widget, bạn có thể import cả feature rồi insert feature này vào feature khác như props/children. Bạn không thể import feature này từ feature khác, điều này bị cấm bởi [**import rule on layers**](/documentation/vi/docs/reference/layers.md#import-rule-on-layers). ### Còn Atomic Design thì sao?[​](#còn-atomic-design-thì-sao "Link trực tiếp đến heading") Phiên bản hiện tại của phương pháp luận không yêu cầu cũng không cấm việc sử dụng Atomic Design cùng với Feature-Sliced Design. Ví dụ, Atomic Design [có thể được áp dụng tốt](https://t.me/feature_sliced/1653) cho segment `ui` của các module. ### Có tài nguyên/bài viết/v.v. hữu ích nào về FSD không?[​](#có-tài-nguyênbài-viếtvv-hữu-ích-nào-về-fsd-không "Link trực tiếp đến heading") Có! ### Tại sao tôi cần Feature-Sliced Design?[​](#tại-sao-tôi-cần-feature-sliced-design "Link trực tiếp đến heading") Nó giúp bạn và team của bạn nhanh chóng tổng quan project theo các component mang lại giá trị chính. Kiến trúc được tiêu chuẩn hóa giúp tăng tốc onboarding và giải quyết các tranh luận về cấu trúc code. Xem trang [motivation](/documentation/vi/docs/about/motivation.md) để tìm hiểu thêm về lý do FSD được tạo ra. ### Developer mới có cần architecture/methodology không?[​](#developer-mới-có-cần-architecturemethodology-không "Link trực tiếp đến heading") Có thì tốt hơn là không *Thường thì khi bạn thiết kế và phát triển project một mình, mọi thứ diễn ra suôn sẻ. Nhưng nếu có tạm dừng trong quá trình phát triển, có thêm developer mới vào team - thì vấn đề sẽ xuất hiện* ### Làm thế nào để làm việc với authorization context?[​](#làm-thế-nào-để-làm-việc-với-authorization-context "Link trực tiếp đến heading") Trả lời [ở đây](/documentation/vi/docs/guides/examples/auth.md) --- # Tổng quan **Feature-Sliced Design** (FSD) là một phương pháp thiết kế kiến trúc để xây dựng các ứng dụng frontend. Nói đơn giản, đây là tập hợp các quy tắc và quy ước để tổ chức code. Mục đích chính của phương pháp này là làm cho dự án trở nên dễ hiểu và ổn định hơn khi đối mặt với những yêu cầu kinh doanh liên tục thay đổi. Ngoài tập hợp các quy ước, FSD còn là một bộ công cụ. Chúng tôi có [linter](https://github.com/feature-sliced/steiger) để kiểm tra kiến trúc dự án của bạn, [trình tạo thư mục](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) thông qua CLI hoặc IDE, cũng như thư viện phong phú các [ví dụ](/documentation/vi/examples.md). ## Có phù hợp với tôi không?[​](#is-it-right-for-me "Link trực tiếp đến heading") FSD có thể được triển khai trong các dự án và nhóm với bất kỳ quy mô nào. Nó phù hợp với dự án của bạn nếu: * Bạn đang làm **frontend** (UI trên web, mobile, desktop, v.v.) * Bạn đang xây dựng một **ứng dụng**, không phải thư viện Vâng, chỉ đơn giản thế thôi! Không có ràng buộc nào về ngôn ngữ lập trình, UI framework, hoặc state manager bạn sử dụng. Bạn cũng có thể áp dụng FSD từng bước một, sử dụng nó trong monorepo, và mở rộng quy mô lớn bằng cách chia ứng dụng thành các package và triển khai FSD riêng lẻ trong từng package. Nếu bạn đã có một kiến trúc và đang cân nhắc chuyển sang FSD, hãy đảm bảo rằng kiến trúc hiện tại đang **gây ra vấn đề** trong nhóm của bạn. Ví dụ, nếu dự án của bạn đã trở nên quá lớn và liên kết chặt chẽ với nhau khiến việc triển khai tính năng mới trở nên kém hiệu quả, hoặc nếu bạn dự kiến có nhiều thành viên mới tham gia nhóm. Nếu kiến trúc hiện tại hoạt động tốt, có lẽ không đáng để thay đổi. Nhưng nếu bạn quyết định migrate, hãy xem phần [Migration](/documentation/vi/docs/guides/migration/from-custom.md) để được hướng dẫn. ## Ví dụ cơ bản[​](#basic-example "Link trực tiếp đến heading") Đây là một dự án đơn giản triển khai FSD: * `📁 app` * `📁 pages` * `📁 shared` Những thư mục cấp cao này được gọi là *layer*. Hãy xem sâu hơn: * `📂 app` * `📁 routes` * `📁 analytics` * `📂 pages` * `📁 home` * `📂 article-reader` * `📁 ui` * `📁 api` * `📁 settings` * `📂 shared` * `📁 ui` * `📁 api` Các thư mục bên trong `📂 pages` được gọi là *slice*. Chúng chia layer theo domain (trong trường hợp này, theo trang). Các thư mục bên trong `📂 app`, `📂 shared`, và `📂 pages/article-reader` được gọi là *segment*, và chúng chia các slice (hoặc layer) theo mục đích kỹ thuật, tức là code đó dùng để làm gì. ## Các khái niệm[​](#concepts "Link trực tiếp đến heading") Layer, slice, và segment tạo thành một hệ thống phân cấp như sau: ![Hierarchy of FSD concepts, described below](/documentation/vi/assets/images/visual_schema-e826067f573946613dcdc76e3f585082.jpg) Hình minh họa: ba cột, được gắn nhãn từ trái sang phải lần lượt là "Layers", "Slices", và "Segments". Cột "Layers" chứa bảy phân chia được sắp xếp từ trên xuống dưới và được gắn nhãn "app", "processes", "pages", "widgets", "features", "entities", và "shared". Phân chia "processes" bị gạch ngang. Phân chia "entities" được kết nối với cột thứ hai "Slices" theo cách truyền đạt rằng cột thứ hai là nội dung của "entities". Cột "Slices" chứa ba phân chia được sắp xếp từ trên xuống dưới và được gắn nhãn "user", "post", và "comment". Phân chia "post" được kết nối với cột thứ ba "Segments" theo cùng cách như vậy để nó là nội dung của "post". Cột "Segments" chứa ba phân chia, được sắp xếp từ trên xuống dưới và được gắn nhãn "ui", "model", và "api". ### Layer[​](#layers "Link trực tiếp đến heading") Các layer được tiêu chuẩn hóa trên tất cả các dự án FSD. Bạn không cần phải sử dụng tất cả các layer, nhưng tên của chúng rất quan trọng. Hiện tại có bảy layer (từ trên xuống dưới): 1. **App** — mọi thứ khiến ứng dụng chạy được — routing, entrypoint, global style, provider. 2. **Processes** (deprecated) — các kịch bản phức tạp liên quan đến nhiều trang. 3. **Pages** — các trang đầy đủ hoặc các phần lớn của trang trong nested routing. 4. **Widget** — các khối chức năng hoặc UI lớn, tự chứa, thường cung cấp toàn bộ một use case. 5. **Feature** — các triển khai *tái sử dụng* của toàn bộ tính năng sản phẩm, tức là các hành động mang lại giá trị kinh doanh cho người dùng. 6. **Entity** — các thực thể kinh doanh mà dự án làm việc với, như `user` hoặc `product`. 7. **Shared** — chức năng tái sử dụng, đặc biệt khi nó tách rời khỏi đặc điểm cụ thể của dự án/kinh doanh, mặc dù không nhất thiết. cảnh báo Các layer **App** và **Shared**, không giống như các layer khác, không có slice và được chia trực tiếp thành các segment. Tuy nhiên, tất cả các layer khác — **Entity**, **Feature**, **Widget**, và **Page**, giữ nguyên cấu trúc trong đó bạn phải tạo slice trước, bên trong đó bạn tạo các segment. Điều thú vị với các layer là các module ở một layer chỉ có thể biết về và import từ các module từ các layer ở phía dưới một cách nghiêm ngặt. ### Slice[​](#slices "Link trực tiếp đến heading") Tiếp theo là các slice, chúng phân chia code theo domain business. Bạn có thể tự do chọn bất kỳ tên nào cho chúng và tạo nhiều như bạn muốn. Các slice làm cho codebase của bạn dễ điều hướng hơn bằng cách giữ các module có liên quan logic gần nhau. Các slice không thể sử dụng slice khác trên cùng layer, và điều đó giúp với tính liên kết cao và khớp nối thấp. ### Segment[​](#segments "Link trực tiếp đến heading") Các slice, cũng như các layer App và Shared, bao gồm các segment, và các segment nhóm code của bản theo mục đích của nó. Tên segment không bị ràng buộc bởi tiêu chuẩn, nhưng có một số tên quy ước cho các mục đích phổ biến nhất: * `ui` — mọi thứ liên quan đến hiển thị UI: UI component, date formatter, style, v.v. * `api` — tương tác backend: request function, data type, mapper, v.v. * `model` — model dữ liệu: schema, interface, store, và business logic. * `lib` — library code mà các module khác trên slice này cần. * `config` — file cấu hình và feature flag. Thường thì những segment này đủ cho hầu hết các layer, bạn chỉ tạo segment riêng của mình trong Shared hoặc App, nhưng đây không phải là quy tắc bắt buộc. ## Ưu điểm[​](#advantages "Link trực tiếp đến heading") * **Tính thống nhất**
Vì cấu trúc được tiêu chuẩn hóa, các dự án trở nên thống nhất hơn, điều này làm cho việc onboard thành viên mới dễ dàng hơn cho nhóm. * **Ổn định trước các thay đổi và refactoring**
Một module trên một layer không thể sử dụng các module khác trên cùng layer, hoặc các layer ở trên.
Điều này cho phép bạn thực hiện các sửa đổi độc lập mà không có hậu quả không lường trước đối với phần còn lại của ứng dụng. * **Kiểm soát việc tái sử dụng logic**
Tùy thuộc vào layer, bạn có thể làm cho code rất có thể tái sử dụng hoặc rất cục bộ.
Điều này giữ sự cân bằng giữa việc tuân theo nguyên tắc **DRY** và tính thực tế. * **Định hướng vào nhu cầu kinh doanh và người dùng**
Ứng dụng được chia theo các domain kinh doanh và việc sử dụng ngôn ngữ kinh doanh được khuyến khích trong việc đặt tên, để bạn có thể thực hiện công việc sản phẩm hữu ích mà không cần hiểu đầy đủ tất cả các phần không liên quan khác của dự án. ## Áp dụng từng bước[​](#incremental-adoption "Link trực tiếp đến heading") Nếu bạn có một codebase hiện có mà bạn muốn migrate sang FSD, chúng tôi đề xuất chiến lược sau. Chúng tôi thấy nó hữu ích trong kinh nghiệm migrate của chính mình. 1. Bắt đầu bằng cách từ từ định hình các layer App và Shared từng module một để tạo nền tảng. 2. Phân phối tất cả UI hiện có trên Widget và Page bằng cách sơ bộ, ngay cả khi chúng có dependency vi phạm các quy tắc của FSD. 3. Bắt đầu từ từ giải quyết các vi phạm import và cũng trích xuất Entity và có thể cả Feature. Nên tránh thêm các entity lớn mới trong khi refactor hoặc chỉ refactor một số phần nhất định của dự án. ## Bước tiếp theo[​](#next-steps "Link trực tiếp đến heading") * **Muốn nắm bắt tốt cách tư duy trong FSD?** Xem [Tutorial](/documentation/vi/docs/get-started/tutorial.md). * **Bạn thích học từ ví dụ?** Chúng tôi có rất nhiều trong phần [Examples](/documentation/vi/examples.md). * **Bạn có câu hỏi?** Ghé thăm [Telegram chat](https://t.me/feature_sliced) của chúng tôi và nhận trợ giúp từ cộng đồng. --- # Tutorial ## Phần 1. Trên giấy[​](#phần-1-trên-giấy "Link trực tiếp đến heading") Tutorial này sẽ xem xét một app thực tế, còn được biết đến với tên Conduit. Conduit là một bản clone cơ bản của [Medium](https://medium.com/) — nó cho phép bạn đọc và viết bài, cũng như bình luận trên các bài viết của người khác. ![Conduit home page](/documentation/vi/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) Đây là một ứng dụng khá nhỏ, vì vậy chúng ta sẽ giữ nó đơn giản và tránh phân tách quá mức. Rất có thể toàn bộ ứng dụng sẽ chỉ cần ba layer: **App**, **Pages**, và **Shared**. Nếu không, chúng ta sẽ giới thiệu thêm các layer khi cần. Sẵn sàng chưa? ### Bắt đầu bằng việc liệt kê các trang[​](#bắt-đầu-bằng-việc-liệt-kê-các-trang "Link trực tiếp đến heading") Nếu nhìn vào ảnh chụp màn hình ở trên, chúng ta có thể giả định ít nhất những trang sau: * Trang chủ (article feed) * Đăng nhập và đăng ký * Đọc article * Chỉnh sửa article * Xem profile người dùng * Chỉnh sửa profile người dùng (user settings) Mỗi trang này sẽ trở thành một *slice* riêng trên *layer* Pages. Hãy nhớ lại từ phần tổng quan rằng slice chỉ đơn giản là các folder bên trong layer, và layer chỉ đơn giản là các folder có tên định sẵn như `pages`. Như vậy, folder Pages của chúng ta sẽ trông như thế này: ``` 📂 pages/ 📁 feed/ 📁 sign-in/ 📁 article-read/ 📁 article-edit/ 📁 profile/ 📁 settings/ ``` Sự khác biệt chính của Feature-Sliced Design so với cấu trúc code không được quy định là các pages không thể tham chiếu lẫn nhau. Nghĩa là, một page không thể import code từ trang khác. Điều này là do **quy tắc import trên các layer**: *Một module (file) trong một slice chỉ có thể import các slice khác khi chúng nằm trên các layer ở bên dưới.* Trong trường hợp này, một trang là một slice, vì vậy các module (file) bên trong trang này chỉ có thể tham chiếu code từ các layer bên dưới, không phải từ cùng layer Pages. ### Nhìn kỹ hơn vào feed[​](#nhìn-kỹ-hơn-vào-feed "Link trực tiếp đến heading") ![Anonymous user\'s perspective](/documentation/vi/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) *Từ góc nhìn của người dùng ẩn danh* ![Authenticated user\'s perspective](/documentation/vi/assets/images/realworld-feed-authenticated-15427d9ff7baae009b47b501bee6c059.jpg) *Từ góc nhìn của người dùng đã xác thực* Có ba khu vực động trên trang feed: 1. Các link đăng nhập với thông báo nếu bạn đã đăng nhập 2. Danh sách các tag kích hoạt việc lọc trong feed 3. Một/hai feed của các article, mỗi article có nút like Các link đăng nhập là một phần của header chung cho tất cả các trang, chúng ta sẽ xem xét nó riêng. #### Danh sách các tag[​](#danh-sách-các-tag "Link trực tiếp đến heading") Để xây dựng danh sách các tag, chúng ta cần lấy các tag có sẵn, render mỗi tag dưới dạng chip, và lưu trữ các tag đã chọn trong client-side storage. Những thao tác này thuộc các danh mục "tương tác API", "giao diện người dùng", và "lưu trữ". Trong Feature-Sliced Design, code được phân tách theo mục đích sử dụng *segment*. Segment là các folder trong slice, và chúng có thể có tên tùy ý để mô tả mục đích, nhưng một số mục đích rất phổ biến nên có quy ước cho một số tên segment nhất định: * 📂 `api/` cho các tương tác backend * 📂 `ui/` cho code xử lý rendering và giao diện * 📂 `model/` cho storage và business logic * 📂 `config/` cho feature flag, biến môi trường và các hình thức cấu hình khác Chúng ta sẽ đặt code lấy tag vào `api`, component tag vào `ui`, và tương tác storage vào `model`. #### Các article[​](#các-article "Link trực tiếp đến heading") Sử dụng cùng nguyên tắc nhóm, chúng ta có thể phân tách feed của các article thành ba segment tương tự: * 📂 `api/`: lấy các article được phân trang với số lượng like; thích một article * 📂 `ui/`: * danh sách tab có thể render thêm tab nếu có tag được chọn * article riêng lẻ * phân trang chức năng * 📂 `model/`: client-side storage của các article hiện tại được tải và trang hiện tại (nếu cần) ### Tái sử dụng code chung[​](#tái-sử-dụng-code-chung "Link trực tiếp đến heading") Hầu hết các trang có ý định rất khác nhau, nhưng một số thứ nhất định vẫn giữ nguyên trong toàn bộ app — ví dụ, UI kit tuân thủ design language, hoặc quy ước trên backend rằng mọi thứ được thực hiện bằng REST API với cùng phương thức xác thực. Vì các slice được thiết kế để tách biệt, việc tái sử dụng code được hỗ trợ bởi một layer thấp hơn, **Shared**. Shared khác với các layer khác ở chỗ nó chứa các segment, không phải slice. Theo cách này, layer Shared có thể được coi là sự kết hợp giữa một layer và một slice. Thông thường, code trong Shared không được lên kế hoạch trước, mà được trích xuất trong quá trình phát triển, vì chỉ trong quá trình phát triển mới rõ phần nào của code thực sự được chia sẻ. Tuy nhiên, vẫn hữu ích khi ghi nhớ loại code nào thuộc về Shared: * 📂 `ui/` — UI kit, giao diện thuần túy, không có business logic. Ví dụ, button, modal dialog, form input. * 📂 `api/` — wrapper tiện lợi xung quanh các primitive tạo request (như `fetch()` trên Web) và, tùy chọn, các function để kích hoạt request cụ thể theo đặc tả backend. * 📂 `config/` — phân tích biến môi trường * 📂 `i18n/` — cấu hình hỗ trợ ngôn ngữ * 📂 `router/` — routing primitive và route constant Đó chỉ là một vài ví dụ về tên segment trong Shared, nhưng bạn có thể bỏ qua bất kỳ segment nào hoặc tạo segment của riêng bạn. Điều quan trọng duy nhất cần nhớ khi tạo segment mới là tên segment nên mô tả **mục đích (tại sao), không phải bản chất (cái gì)**. Các tên như "components", "hooks", "modals" *không nên* được sử dụng vì chúng mô tả những file này là gì, nhưng không giúp điều hướng code bên trong. Điều này yêu cầu mọi người trong team phải đào sâu vào từng file trong những folder như vậy và cũng giữ code không liên quan gần nhau, dẫn đến việc refactoring ảnh hưởng đến các khu vực rộng lớn của code và do đó làm cho việc review code và testing khó khăn hơn. ### Định nghĩa public API nghiêm ngặt[​](#định-nghĩa-public-api-nghiêm-ngặt "Link trực tiếp đến heading") Trong ngữ cảnh của Feature-Sliced Design, thuật ngữ *public API* đề cập đến một slice hoặc segment khai báo những gì có thể được import từ nó bởi các module khác trong dự án. Ví dụ, trong JavaScript đó có thể là file `index.js` re-export các object từ các file khác trong slice. Điều này cho phép tự do refactoring code bên trong slice miễn là hợp đồng với thế giới bên ngoài (tức là public API) vẫn giữ nguyên. Đối với layer Shared không có slice, thường thuận tiện hơn khi định nghĩa public API riêng cho mỗi segment thay vì định nghĩa một index duy nhất cho mọi thứ trong Shared. Điều này giữ các import từ Shared được tổ chức tự nhiên theo ý định. Đối với các layer khác có slice, ngược lại — thường thực tế hơn khi định nghĩa một index cho mỗi slice và để slice quyết định tập hợp segment riêng của nó mà thế giới bên ngoài không biết vì các layer khác thường có ít export hơn nhiều. Các slice/segment của chúng ta sẽ xuất hiện với nhau như sau: ``` 📂 pages/ 📂 feed/ 📄 index 📂 sign-in/ 📄 index 📂 article-read/ 📄 index 📁 … 📂 shared/ 📂 ui/ 📄 index 📂 api/ 📄 index 📁 … ``` Bất cứ thứ gì bên trong các folder như `pages/feed` hoặc `shared/ui` chỉ được biết đến bởi những folder đó, và các file khác không nên dựa vào cấu trúc nội bộ của những folder này. ### Khối UI lớn được tái sử dụng[​](#khối-ui-lớn-được-tái-sử-dụng "Link trực tiếp đến heading") Trước đó chúng ta đã ghi chú để xem lại header xuất hiện trên mỗi trang. Xây dựng lại từ đầu trên mỗi trang sẽ không thực tế, vì vậy việc muốn tái sử dụng nó là điều tự nhiên. Chúng ta đã có Shared để hỗ trợ tái sử dụng code, tuy nhiên, có một lưu ý khi đặt các khối UI lớn trong Shared — layer Shared không được biết về bất kỳ layer nào ở trên. Giữa Shared và Pages có ba layer khác: Entities, Features, và Widgets. Một số dự án có thể có thứ gì đó trong những layer đó mà họ cần trong một khối có thể tái sử dụng lớn, và điều đó có nghĩa là chúng ta không thể đặt khối có thể tái sử dụng đó trong Shared, nếu không nó sẽ import từ các layer trên, điều này bị cấm. Đó là lúc layer Widgets xuất hiện. Nó được đặt phía trên Shared, Entities, và Features, vì vậy nó có thể sử dụng tất cả chúng. Trong trường hợp của chúng ta, header rất đơn giản — đó là logo tĩnh và điều hướng cấp cao nhất. Điều hướng cần thực hiện request đến API để xác định người dùng hiện tại có đăng nhập hay không, nhưng điều đó có thể được xử lý bằng một import đơn giản từ segment `api`. Do đó, chúng ta sẽ giữ header của mình trong Shared. ### Nhìn kỹ vào trang có form[​](#nhìn-kỹ-vào-trang-có-form "Link trực tiếp đến heading") Hãy cũng kiểm tra một trang được thiết kế để chỉnh sửa, không phải đọc. Ví dụ, trình soạn thảo article: ![Conduit post editor](/documentation/vi/assets/images/realworld-editor-authenticated-10de4d01479270886859e08592045b1e.jpg) Nó trông đơn giản, nhưng chứa một số khía cạnh của phát triển ứng dụng mà chúng ta chưa khám phá — validation form, trạng thái lỗi, và data persistence. Nếu chúng ta xây dựng trang này, chúng ta sẽ lấy một số input và button từ Shared và ghép thành một form trong segment `ui` của trang này. Sau đó, trong segment `api`, chúng ta sẽ định nghĩa một mutation request để tạo article trên backend. Để validate request trước khi gửi, chúng ta cần một validation schema, và vị trí tốt cho nó là segment `model`, vì nó là data model. Ở đó chúng ta sẽ tạo ra các thông báo lỗi và hiển thị chúng bằng một component khác trong segment `ui`. Để cải thiện trải nghiệm người dùng, chúng ta cũng có thể persist các input để ngăn mất dữ liệu vô tình. Đây cũng là công việc của segment `model`. ### Tóm tắt[​](#tóm-tắt "Link trực tiếp đến heading") Chúng ta đã kiểm tra một số trang và phác thảo cấu trúc sơ bộ cho ứng dụng của mình: 1. Layer Shared 1. `ui` sẽ chứa UI kit có thể tái sử dụng của chúng ta 2. `api` sẽ chứa các tương tác primitive với backend 3. Phần còn lại sẽ được sắp xếp theo yêu cầu 2. Layer Pages — mỗi trang là một slice riêng biệt 1. `ui` sẽ chứa chính trang đó và tất cả các phần của nó 2. `api` sẽ chứa data fetching chuyên biệt hơn, sử dụng `shared/api` 3. `model` có thể chứa client-side storage của dữ liệu mà chúng ta sẽ hiển thị Hãy bắt đầu xây dựng! ## Phần 2. Trong code[​](#phần-2-trong-code "Link trực tiếp đến heading") Bây giờ chúng ta đã có kế hoạch, hãy đưa nó vào thực hành. Chúng ta sẽ sử dụng React và [Remix](https://remix.run). Có một template sẵn sàng cho dự án này, clone nó từ GitHub để có được khởi đầu: . Cài đặt dependencies với `npm install` và khởi động development server với `npm run dev`. Mở và bạn sẽ thấy một app trống. ### Bố trí các trang[​](#bố-trí-các-trang "Link trực tiếp đến heading") Hãy bắt đầu bằng việc tạo các component trống cho tất cả các trang của chúng ta. Chạy lệnh sau trong dự án của bạn: ``` npx fsd pages feed sign-in article-read article-edit profile settings --segments ui ``` Điều này sẽ tạo các folder như `pages/feed/ui/` và một file index, `pages/feed/index.ts`, cho mỗi trang. ### Kết nối trang feed[​](#kết-nối-trang-feed "Link trực tiếp đến heading") Hãy kết nối route gốc của ứng dụng với trang feed. Tạo một component, `FeedPage.tsx` trong `pages/feed/ui` và đặt nội dung sau vào đó: pages/feed/ui/FeedPage.tsx ``` export function FeedPage() { return (

conduit

A place to share your knowledge.

); } ``` Sau đó re-export component này trong public API của trang feed, file `pages/feed/index.ts`: pages/feed/index.ts ``` export { FeedPage } from "./ui/FeedPage"; ``` Bây giờ kết nối nó với root route. Trong Remix, routing dựa trên file, và các file route được đặt trong folder `app/routes`, điều này phù hợp với Feature-Sliced Design. Sử dụng component `FeedPage` trong `app/routes/_index.tsx`: app/routes/\_index.tsx ``` import type { MetaFunction } from "@remix-run/node"; import { FeedPage } from "pages/feed"; export const meta: MetaFunction = () => { return [{ title: "Conduit" }]; }; export default FeedPage; ``` Sau đó, nếu bạn chạy dev server và mở ứng dụng, bạn sẽ thấy banner của Conduit! ![The banner of Conduit](/documentation/vi/assets/images/conduit-banner-a20e38edcd109ee21a8b1426d93a66b3.jpg) ### API client[​](#api-client "Link trực tiếp đến heading") Để giao tiếp với RealWorld backend, hãy tạo một API client tiện lợi trong Shared. Tạo hai segment, `api` cho client và `config` cho các biến như backend base URL: ``` npx fsd shared --segments api config ``` Sau đó tạo `shared/config/backend.ts`: shared/config/backend.ts ``` export { mockBackendUrl as backendBaseUrl } from "mocks/handlers"; ``` shared/config/index.ts ``` export { backendBaseUrl } from "./backend"; ``` Vì dự án RealWorld tiện lợi cung cấp [đặc tả OpenAPI](https://github.com/gothinkster/realworld/blob/main/api/openapi.yml), chúng ta có thể tận dụng các type được tự động tạo cho client của mình. Chúng ta sẽ sử dụng [package `openapi-fetch`](https://openapi-ts.pages.dev/openapi-fetch/) đi kèm với type generator bổ sung. Chạy lệnh sau để tạo API typing cập nhật: ``` npm run generate-api-types ``` Điều này sẽ tạo file `shared/api/v1.d.ts`. Chúng ta sẽ sử dụng file này để tạo typed API client trong `shared/api/client.ts`: shared/api/client.ts ``` import createClient from "openapi-fetch"; import { backendBaseUrl } from "shared/config"; import type { paths } from "./v1"; export const { GET, POST, PUT, DELETE } = createClient({ baseUrl: backendBaseUrl }); ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; ``` ### Dữ liệu thực trong feed[​](#dữ-liệu-thực-trong-feed "Link trực tiếp đến heading") Bây giờ chúng ta có thể tiến hành thêm các article vào feed, được lấy từ backend. Hãy bắt đầu bằng cách triển khai component preview article. Tạo `pages/feed/ui/ArticlePreview.tsx` với nội dung sau: pages/feed/ui/ArticlePreview\.tsx ``` export function ArticlePreview({ article }) { /* TODO */ } ``` Vì chúng ta viết bằng TypeScript, sẽ tốt nếu có một article object được type. Nếu chúng ta khám phá `v1.d.ts` được tạo, chúng ta có thể thấy rằng article object có sẵn thông qua `components["schemas"]["Article"]`. Vì vậy hãy tạo file với các data model của chúng ta trong Shared và export các model: shared/api/models.ts ``` import type { components } from "./v1"; export type Article = components["schemas"]["Article"]; ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; ``` Bây giờ chúng ta có thể quay lại component preview article và điền markup với dữ liệu. Cập nhật component với nội dung sau: pages/feed/ui/ArticlePreview\.tsx ``` import { Link } from "@remix-run/react"; import type { Article } from "shared/api"; interface ArticlePreviewProps { article: Article; } export function ArticlePreview({ article }: ArticlePreviewProps) { return (
{article.author.username} {new Date(article.createdAt).toLocaleDateString(undefined, { dateStyle: "long", })}

{article.title}

{article.description}

Read more...
    {article.tagList.map((tag) => (
  • {tag}
  • ))}
); } ``` Button like hiện tại chưa làm gì cả, chúng ta sẽ sửa điều đó khi đến trang đọc article và triển khai tính năng thích. Bây giờ chúng ta có thể lấy các article và render ra một loạt các card này. Lấy dữ liệu trong Remix được thực hiện bằng *loader* — các function phía server lấy chính xác những gì trang cần. Loader tương tác với API thay mặt cho trang, vì vậy chúng ta sẽ đặt chúng trong segment `api` của trang: pages/feed/api/loader.ts ``` import { json } from "@remix-run/node"; import { GET } from "shared/api"; export const loader = async () => { const { data: articles, error, response } = await GET("/articles"); if (error !== undefined) { throw json(error, { status: response.status }); } return json({ articles }); }; ``` Để kết nối nó với trang, chúng ta cần export nó với tên `loader` từ route file: pages/feed/index.ts ``` export { FeedPage } from "./ui/FeedPage"; export { loader } from "./api/loader"; ``` app/routes/\_index.tsx ``` import type { MetaFunction } from "@remix-run/node"; import { FeedPage } from "pages/feed"; export { loader } from "pages/feed"; export const meta: MetaFunction = () => { return [{ title: "Conduit" }]; }; export default FeedPage; ``` Và bước cuối cùng là render các card này trong feed. Cập nhật `FeedPage` của bạn với code sau: pages/feed/ui/FeedPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const { articles } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
); } ``` ### Lọc theo tag[​](#lọc-theo-tag "Link trực tiếp đến heading") Về các tag, công việc của chúng ta là lấy chúng từ backend và lưu trữ tag hiện tại được chọn. Chúng ta đã biết cách lấy — đó là một request khác từ loader. Chúng ta sẽ sử dụng function tiện lợi `promiseHash` từ package `remix-utils`, đã được cài đặt. Cập nhật file loader, `pages/feed/api/loader.ts`, với code sau: pages/feed/api/loader.ts ``` import { json } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async () => { return json( await promiseHash({ articles: throwAnyErrors(GET("/articles")), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` Bạn có thể nhận thấy rằng chúng ta đã trích xuất xử lý lỗi thành function generic `throwAnyErrors`. Nó trông khá hữu ích, vì vậy chúng ta có thể muốn tái sử dụng nó sau, nhưng hiện tại hãy chỉ để mắt đến nó. Bây giờ, đến danh sách các tag. Nó cần phải tương tác — nhấp vào tag sẽ làm cho tag đó được chọn. Theo quy ước Remix, chúng ta sẽ sử dụng URL search parameter làm storage cho tag đã chọn. Để trình duyệt lo về storage trong khi chúng ta tập trung vào những thứ quan trọng hơn. Cập nhật `pages/feed/ui/FeedPage.tsx` với code sau: pages/feed/ui/FeedPage.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const { articles, tags } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` Sau đó chúng ta cần sử dụng search parameter `tag` trong loader của chúng ta. Thay đổi function `loader` trong `pages/feed/api/loader.ts` thành như sau: pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag } } }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` Thế là xong, không cần segment `model`. Remix khá gọn gàng. ### Phân trang[​](#phân-trang "Link trực tiếp đến heading") Tương tự như vậy, chúng ta có thể triển khai phân trang. Hãy thoải mái thử tự làm hoặc chỉ copy code bên dưới. Dù sao cũng không ai phán xét bạn. pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } /** Amount of articles on one page. */ export const LIMIT = 20; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag, limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` pages/feed/ui/FeedPage.tsx ``` import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import { LIMIT, type loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const [searchParams] = useSearchParams(); const { articles, tags } = useLoaderData(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
    {Array(pageAmount) .fill(null) .map((_, index) => index + 1 === currentPage ? (
  • {index + 1}
  • ) : (
  • ), )}

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` Vậy là cũng xong rồi. Còn có danh sách tab có thể được triển khai tương tự, nhưng hãy đợi cho đến khi chúng ta triển khai authentication. Nói về điều đó! ### Authentication[​](#authentication "Link trực tiếp đến heading") Authentication liên quan đến hai trang — một để đăng nhập và một để đăng ký. Chúng hầu như giống nhau, vì vậy hợp lý khi giữ chúng trong cùng một slice, `sign-in`, để chúng có thể tái sử dụng code nếu cần. Tạo `RegisterPage.tsx` trong segment `ui` của `pages/sign-in` với nội dung sau: pages/sign-in/ui/RegisterPage.tsx ``` import { Form, Link, useActionData } from "@remix-run/react"; import type { register } from "../api/register"; export function RegisterPage() { const registerData = useActionData(); return (

Sign up

Have an account?

{registerData?.error && (
    {registerData.error.errors.body.map((error) => (
  • {error}
  • ))}
)}
); } ``` Chúng ta có một import bị lỗi cần sửa bây giờ. Nó liên quan đến một segment mới, vì vậy hãy tạo nó: ``` npx fsd pages sign-in -s api ``` Tuy nhiên, trước khi chúng ta có thể triển khai phần backend của đăng ký, chúng ta cần một số code infrastructure để Remix xử lý session. Điều đó thuộc về Shared, phòng khi trang nào khác cần nó. Đặt code sau vào `shared/api/auth.server.ts`. Đây là code rất cụ thể cho Remix, vì vậy đừng lo lắng quá nhiều về nó, chỉ cần copy-paste: shared/api/auth.server.ts ``` import { createCookieSessionStorage, redirect } from "@remix-run/node"; import invariant from "tiny-invariant"; import type { User } from "./models"; invariant( process.env.SESSION_SECRET, "SESSION_SECRET must be set for authentication to work", ); const sessionStorage = createCookieSessionStorage<{ user: User; }>({ cookie: { name: "__session", httpOnly: true, path: "/", sameSite: "lax", secrets: [process.env.SESSION_SECRET], secure: process.env.NODE_ENV === "production", }, }); export async function createUserSession({ request, user, redirectTo, }: { request: Request; user: User; redirectTo: string; }) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); session.set("user", user); return redirect(redirectTo, { headers: { "Set-Cookie": await sessionStorage.commitSession(session, { maxAge: 60 * 60 * 24 * 7, // 7 days }), }, }); } export async function getUserFromSession(request: Request) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); return session.get("user") ?? null; } export async function requireUser(request: Request) { const user = await getUserFromSession(request); if (user === null) { throw redirect("/login"); } return user; } ``` Và cũng export model `User` từ file `models.ts` ngay cạnh nó: shared/api/models.ts ``` import type { components } from "./v1"; export type Article = components["schemas"]["Article"]; export type User = components["schemas"]["User"]; ``` Trước khi code này có thể hoạt động, biến môi trường `SESSION_SECRET` cần được đặt. Tạo file tên `.env` trong thư mục gốc của dự án, viết `SESSION_SECRET=` và sau đó bấm một số phím trên bàn phím để tạo chuỗi ngẫu nhiên dài. Bạn sẽ có thứ gì đó như thế này: .env ``` SESSION_SECRET=dontyoudarecopypastethis ``` Cuối cùng, thêm một số export vào public API để sử dụng code này: shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; ``` Bây giờ chúng ta có thể viết code sẽ giao tiếp với RealWorld backend để thực sự thực hiện đăng ký. Chúng ta sẽ giữ điều đó trong `pages/sign-in/api`. Tạo file có tên `register.ts` và đặt code sau vào bên trong: pages/sign-in/api/register.ts ``` import { json, type ActionFunctionArgs } from "@remix-run/node"; import { POST, createUserSession } from "shared/api"; export const register = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const username = formData.get("username")?.toString() ?? ""; const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users", { body: { user: { email, password, username } }, }); if (error) { return json({ error }, { status: 400 }); } else { return createUserSession({ request: request, user: data.user, redirectTo: "/", }); } }; ``` pages/sign-in/index.ts ``` export { RegisterPage } from './ui/RegisterPage'; export { register } from './api/register'; ``` Gần xong rồi! Chỉ cần kết nối trang và action với route `/register`. Tạo `register.tsx` trong `app/routes`: app/routes/register.tsx ``` import { RegisterPage, register } from "pages/sign-in"; export { register as action }; export default RegisterPage; ``` Bây giờ nếu bạn đi đến , bạn sẽ có thể tạo người dùng! Phần còn lại của ứng dụng sẽ chưa phản ứng với điều này, chúng ta sẽ giải quyết điều đó ngay. Tương tự như vậy, chúng ta có thể triển khai trang đăng nhập. Hãy thử hoặc chỉ lấy code và tiếp tục: pages/sign-in/api/sign-in.ts ``` import { json, type ActionFunctionArgs } from "@remix-run/node"; import { POST, createUserSession } from "shared/api"; export const signIn = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users/login", { body: { user: { email, password } }, }); if (error) { return json({ error }, { status: 400 }); } else { return createUserSession({ request: request, user: data.user, redirectTo: "/", }); } }; ``` pages/sign-in/ui/SignInPage.tsx ``` import { Form, Link, useActionData } from "@remix-run/react"; import type { signIn } from "../api/sign-in"; export function SignInPage() { const signInData = useActionData(); return (

Sign in

Need an account?

{signInData?.error && (
    {signInData.error.errors.body.map((error) => (
  • {error}
  • ))}
)}
); } ``` pages/sign-in/index.ts ``` export { RegisterPage } from './ui/RegisterPage'; export { register } from './api/register'; export { SignInPage } from './ui/SignInPage'; export { signIn } from './api/sign-in'; ``` app/routes/login.tsx ``` import { SignInPage, signIn } from "pages/sign-in"; export { signIn as action }; export default SignInPage; ``` Bây giờ hãy cung cấp cho người dùng cách thực sự đến các trang này. ### Header[​](#header "Link trực tiếp đến heading") Như chúng ta đã thảo luận trong phần 1, header app thường được đặt trong Widgets hoặc trong Shared. Chúng ta sẽ đặt nó trong Shared vì nó rất đơn giản và tất cả business logic có thể được giữ bên ngoài nó. Hãy tạo chỗ cho nó: ``` npx fsd shared ui ``` Bây giờ tạo `shared/ui/Header.tsx` với nội dung sau: shared/ui/Header.tsx ``` import { useContext } from "react"; import { Link, useLocation } from "@remix-run/react"; import { CurrentUser } from "../api/currentUser"; export function Header() { const currentUser = useContext(CurrentUser); const { pathname } = useLocation(); return ( ); } ``` Export component này từ `shared/ui`: shared/ui/index.ts ``` export { Header } from "./Header"; ``` Trong header, chúng ta dựa vào context được giữ trong `shared/api`. Cũng tạo điều đó: shared/api/currentUser.ts ``` import { createContext } from "react"; import type { User } from "./models"; export const CurrentUser = createContext(null); ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; export { CurrentUser } from "./currentUser"; ``` Bây giờ hãy thêm header vào trang. Chúng ta muốn nó có trên mọi trang, vì vậy hợp lý khi chỉ thêm nó vào root route và wrap outlet (nơi trang sẽ được render) với provider context `CurrentUser`. Theo cách này, toàn bộ app của chúng ta và cả header đều có quyền truy cập vào object người dùng hiện tại. Chúng ta cũng sẽ thêm loader để thực sự lấy object người dùng hiện tại từ cookie. Đặt nội dung sau vào `app/root.tsx`: app/root.tsx ``` import { cssBundleHref } from "@remix-run/css-bundle"; import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData, } from "@remix-run/react"; import { Header } from "shared/ui"; import { getUserFromSession, CurrentUser } from "shared/api"; export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; export const loader = ({ request }: LoaderFunctionArgs) => getUserFromSession(request); export default function App() { const user = useLoaderData(); return (
); } ``` Tại thời điểm này, bạn sẽ có kết quả sau trên trang chủ: ![The feed page of Conduit, including the header, the feed, and the tags. The tabs are still missing.](/documentation/vi/assets/images/realworld-feed-without-tabs-5da4c9072101ac20e82e2234bd3badbe.jpg) Trang feed của Conduit, bao gồm header, feed, và các tag. Các tab vẫn còn thiếu. ### Tab[​](#tab "Link trực tiếp đến heading") Bây giờ chúng ta có thể phát hiện trạng thái authentication, hãy cũng nhanh chóng triển khai các tab và like bài viết để hoàn thành trang feed. Chúng ta cần một form khác, nhưng file trang này đang trở nên khá lớn, vì vậy hãy chuyển những form này vào các file liền kề. Chúng ta sẽ tạo `Tabs.tsx`, `PopularTags.tsx`, và `Pagination.tsx` với nội dung sau: pages/feed/ui/Tabs.tsx ``` import { useContext } from "react"; import { Form, useSearchParams } from "@remix-run/react"; import { CurrentUser } from "shared/api"; export function Tabs() { const [searchParams] = useSearchParams(); const currentUser = useContext(CurrentUser); return (
    {currentUser !== null && (
  • )}
  • {searchParams.has("tag") && (
  • {searchParams.get("tag")}
  • )}
); } ``` pages/feed/ui/PopularTags.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import type { loader } from "../api/loader"; export function PopularTags() { const { tags } = useLoaderData(); return (

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` pages/feed/ui/Pagination.tsx ``` import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import { LIMIT, type loader } from "../api/loader"; export function Pagination() { const [searchParams] = useSearchParams(); const { articles } = useLoaderData(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (
    {Array(pageAmount) .fill(null) .map((_, index) => index + 1 === currentPage ? (
  • {index + 1}
  • ) : (
  • ), )}
); } ``` Và bây giờ chúng ta có thể đơn giản hóa đáng kể chính trang feed: pages/feed/ui/FeedPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; import { Tabs } from "./Tabs"; import { PopularTags } from "./PopularTags"; import { Pagination } from "./Pagination"; export function FeedPage() { const { articles } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
); } ``` Chúng ta cũng cần tính đến tab mới trong function loader: pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET, requireUser } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { /* unchanged */ } /** Amount of articles on one page. */ export const LIMIT = 20; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); if (url.searchParams.get("source") === "my-feed") { const userSession = await requireUser(request); return json( await promiseHash({ articles: throwAnyErrors( GET("/articles/feed", { params: { query: { limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, headers: { Authorization: `Token ${userSession.token}` }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); } return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag, limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` Trước khi rời trang feed, hãy thêm một số code xử lý like cho bài viết. Thay đổi `ArticlePreview.tsx` của bạn thành như sau: pages/feed/ui/ArticlePreview\.tsx ``` import { Form, Link } from "@remix-run/react"; import type { Article } from "shared/api"; interface ArticlePreviewProps { article: Article; } export function ArticlePreview({ article }: ArticlePreviewProps) { return (
{article.author.username} {new Date(article.createdAt).toLocaleDateString(undefined, { dateStyle: "long", })}

{article.title}

{article.description}

Read more...
    {article.tagList.map((tag) => (
  • {tag}
  • ))}
); } ``` Code này sẽ gửi POST request đến `/article/:slug` với `_action=favorite` để đánh dấu article là yêu thích. Nó chưa hoạt động, nhưng khi chúng ta bắt đầu làm việc trên trình đọc article, chúng ta cũng sẽ triển khai điều này. Và với điều đó, chúng ta đã chính thức hoàn thành feed! Yay! ### Trình đọc article[​](#trình-đọc-article "Link trực tiếp đến heading") Trước tiên, chúng ta cần dữ liệu. Hãy tạo một loader: ``` npx fsd pages article-read -s api ``` pages/article-read/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import invariant from "tiny-invariant"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET, getUserFromSession } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ request, params }: LoaderFunctionArgs) => { invariant(params.slug, "Expected a slug parameter"); const currentUser = await getUserFromSession(request); const authorization = currentUser ? { Authorization: `Token ${currentUser.token}` } : undefined; return json( await promiseHash({ article: throwAnyErrors( GET("/articles/{slug}", { params: { path: { slug: params.slug }, }, headers: authorization, }), ), comments: throwAnyErrors( GET("/articles/{slug}/comments", { params: { path: { slug: params.slug }, }, headers: authorization, }), ), }), ); }; ``` pages/article-read/index.ts ``` export { loader } from "./api/loader"; ``` Bây giờ chúng ta có thể kết nối nó với route `/article/:slug` bằng cách tạo file route có tên `article.$slug.tsx`: app/routes/article.$slug.tsx ``` export { loader } from "pages/article-read"; ``` Chính trang bao gồm ba khối chính — header article với các action (lặp lại hai lần), nội dung article, và phần comment. Đây là markup cho trang, nó không đặc biệt thú vị: pages/article-read/ui/ArticleReadPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticleMeta } from "./ArticleMeta"; import { Comments } from "./Comments"; export function ArticleReadPage() { const { article } = useLoaderData(); return (

{article.article.title}

{article.article.body}

    {article.article.tagList.map((tag) => (
  • {tag}
  • ))}

); } ``` Điều thú vị hơn là `ArticleMeta` và `Comments`. Chúng chứa các thao tác ghi như thích article, để lại comment, v.v. Để chúng hoạt động, trước tiên chúng ta cần triển khai phần backend. Tạo `action.ts` trong segment `api` của trang: pages/article-read/api/action.ts ``` import { redirect, type ActionFunctionArgs } from "@remix-run/node"; import { namedAction } from "remix-utils/named-action"; import { redirectBack } from "remix-utils/redirect-back"; import invariant from "tiny-invariant"; import { DELETE, POST, requireUser } from "shared/api"; export const action = async ({ request, params }: ActionFunctionArgs) => { const currentUser = await requireUser(request); const authorization = { Authorization: `Token ${currentUser.token}` }; const formData = await request.formData(); return namedAction(formData, { async delete() { invariant(params.slug, "Expected a slug parameter"); await DELETE("/articles/{slug}", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirect("/"); }, async favorite() { invariant(params.slug, "Expected a slug parameter"); await POST("/articles/{slug}/favorite", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async unfavorite() { invariant(params.slug, "Expected a slug parameter"); await DELETE("/articles/{slug}/favorite", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async createComment() { invariant(params.slug, "Expected a slug parameter"); const comment = formData.get("comment"); invariant(typeof comment === "string", "Expected a comment parameter"); await POST("/articles/{slug}/comments", { params: { path: { slug: params.slug } }, headers: { ...authorization, "Content-Type": "application/json" }, body: { comment: { body: comment } }, }); return redirectBack(request, { fallback: "/" }); }, async deleteComment() { invariant(params.slug, "Expected a slug parameter"); const commentId = formData.get("id"); invariant(typeof commentId === "string", "Expected an id parameter"); const commentIdNumeric = parseInt(commentId, 10); invariant( !Number.isNaN(commentIdNumeric), "Expected a numeric id parameter", ); await DELETE("/articles/{slug}/comments/{id}", { params: { path: { slug: params.slug, id: commentIdNumeric } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async followAuthor() { const authorUsername = formData.get("username"); invariant( typeof authorUsername === "string", "Expected a username parameter", ); await POST("/profiles/{username}/follow", { params: { path: { username: authorUsername } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async unfollowAuthor() { const authorUsername = formData.get("username"); invariant( typeof authorUsername === "string", "Expected a username parameter", ); await DELETE("/profiles/{username}/follow", { params: { path: { username: authorUsername } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, }); }; ``` Export điều đó từ slice và sau đó từ route. Trong khi làm điều đó, hãy cũng kết nối chính trang: pages/article-read/index.ts ``` export { ArticleReadPage } from "./ui/ArticleReadPage"; export { loader } from "./api/loader"; export { action } from "./api/action"; ``` app/routes/article.$slug.tsx ``` import { ArticleReadPage } from "pages/article-read"; export { loader, action } from "pages/article-read"; export default ArticleReadPage; ``` Bây giờ, mặc dù chúng ta chưa triển khai button like trên trang đọc, button like trong feed sẽ bắt đầu hoạt động! Đó là vì nó đã gửi request "like" đến route này. Hãy thử điều đó. `ArticleMeta` và `Comments`, một lần nữa, là một loạt form. Chúng ta đã làm điều này trước đây, hãy lấy code của chúng và tiếp tục: pages/article-read/ui/ArticleMeta.tsx ``` import { Form, Link, useLoaderData } from "@remix-run/react"; import { useContext } from "react"; import { CurrentUser } from "shared/api"; import type { loader } from "../api/loader"; export function ArticleMeta() { const currentUser = useContext(CurrentUser); const { article } = useLoaderData(); return (
{article.article.author.username} {article.article.createdAt}
{article.article.author.username == currentUser?.username ? ( <> Edit Article    ) : ( <>    )}
); } ``` pages/article-read/ui/Comments.tsx ``` import { useContext } from "react"; import { Form, Link, useLoaderData } from "@remix-run/react"; import { CurrentUser } from "shared/api"; import type { loader } from "../api/loader"; export function Comments() { const { comments } = useLoaderData(); const currentUser = useContext(CurrentUser); return (
{currentUser !== null ? (
) : (

Sign in   or   Sign up   to add comments on this article.

)} {comments.comments.map((comment) => (

{comment.body}

  {comment.author.username} {comment.createdAt} {comment.author.username === currentUser?.username && (
)}
))}
); } ``` Và với điều đó, trình đọc article của chúng ta cũng hoàn thành! Các button theo dõi tác giả, thích bài viết, và để lại comment giờ đây sẽ hoạt động như mong đợi. ![Article reader with functioning buttons to like and follow](/documentation/vi/assets/images/realworld-article-reader-6a420e4f2afe139d2bdd54d62974f0b9.jpg) Trình đọc article với các button hoạt động để thích và theo dõi ### Trình chỉnh sửa article[​](#trình-chỉnh-sửa-article "Link trực tiếp đến heading") Đây là trang cuối cùng mà chúng ta sẽ đề cập trong tutorial này, và phần thú vị nhất ở đây là cách chúng ta sẽ validate dữ liệu form. Chính trang, `article-edit/ui/ArticleEditPage.tsx`, sẽ khá đơn giản, độ phức tạp bổ sung được cất giấu trong hai component khác: pages/article-edit/ui/ArticleEditPage.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { TagsInput } from "./TagsInput"; import { FormErrors } from "./FormErrors"; export function ArticleEditPage() { const article = useLoaderData(); return (
); } ``` Trang này lấy article hiện tại (trừ khi chúng ta viết từ đầu) và điền vào các trường form tương ứng. Chúng ta đã thấy điều này trước đây. Phần thú vị là `FormErrors`, vì nó sẽ nhận kết quả validation và hiển thị cho người dùng. Hãy xem: pages/article-edit/ui/FormErrors.tsx ``` import { useActionData } from "@remix-run/react"; import type { action } from "../api/action"; export function FormErrors() { const actionData = useActionData(); return actionData?.errors != null ? (
    {actionData.errors.map((error) => (
  • {error}
  • ))}
) : null; } ``` Ở đây chúng ta giả định rằng action của chúng ta sẽ trả về trường `errors`, một mảng các thông báo lỗi có thể đọc được. Chúng ta sẽ đến action ngay. Component khác là input tag. Nó chỉ là trường input đơn giản với preview bổ sung của các tag đã chọn. Không có gì nhiều để xem ở đây: pages/article-edit/ui/TagsInput.tsx ``` import { useEffect, useRef, useState } from "react"; export function TagsInput({ name, defaultValue, }: { name: string; defaultValue?: Array; }) { const [tagListState, setTagListState] = useState(defaultValue ?? []); function removeTag(tag: string): void { const newTagList = tagListState.filter((t) => t !== tag); setTagListState(newTagList); } const tagsInput = useRef(null); useEffect(() => { tagsInput.current && (tagsInput.current.value = tagListState.join(",")); }, [tagListState]); return ( <> setTagListState(e.target.value.split(",").filter(Boolean)) } />
{tagListState.map((tag) => ( [" ", "Enter"].includes(e.key) && removeTag(tag) } onClick={() => removeTag(tag)} >{" "} {tag} ))}
); } ``` Bây giờ, cho phần API. Loader nên nhìn vào URL, và nếu nó chứa article slug, có nghĩa là chúng ta đang chỉnh sửa article hiện có, và dữ liệu của nó nên được tải. Nếu không, trả về không có gì. Hãy tạo loader đó: pages/article-edit/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { GET, requireUser } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ params, request }: LoaderFunctionArgs) => { const currentUser = await requireUser(request); if (!params.slug) { return { article: null }; } return throwAnyErrors( GET("/articles/{slug}", { params: { path: { slug: params.slug } }, headers: { Authorization: `Token ${currentUser.token}` }, }), ); }; ``` Action sẽ lấy các giá trị trường mới, chạy chúng qua data schema của chúng ta, và nếu mọi thứ đều đúng, commit những thay đổi đó đến backend, hoặc bằng cách cập nhật article hiện có hoặc tạo một article mới: pages/article-edit/api/action.ts ``` import { json, redirect, type ActionFunctionArgs } from "@remix-run/node"; import { POST, PUT, requireUser } from "shared/api"; import { parseAsArticle } from "../model/parseAsArticle"; export const action = async ({ request, params }: ActionFunctionArgs) => { try { const { body, description, title, tags } = parseAsArticle( await request.formData(), ); const tagList = tags?.split(",") ?? []; const currentUser = await requireUser(request); const payload = { body: { article: { title, description, body, tagList, }, }, headers: { Authorization: `Token ${currentUser.token}` }, }; const { data, error } = await (params.slug ? PUT("/articles/{slug}", { params: { path: { slug: params.slug } }, ...payload, }) : POST("/articles", payload)); if (error) { return json({ errors: error }, { status: 422 }); } return redirect(`/article/${data.article.slug ?? ""}`); } catch (errors) { return json({ errors }, { status: 400 }); } }; ``` Schema có vai trò kép như một function phân tích cho `FormData`, cho phép chúng ta thuận tiện lấy các trường sạch hoặc chỉ throw lỗi để xử lý ở cuối. Đây là cách function phân tích đó có thể trông như thế nào: pages/article-edit/model/parseAsArticle.ts ``` export function parseAsArticle(data: FormData) { const errors = []; const title = data.get("title"); if (typeof title !== "string" || title === "") { errors.push("Give this article a title"); } const description = data.get("description"); if (typeof description !== "string" || description === "") { errors.push("Describe what this article is about"); } const body = data.get("body"); if (typeof body !== "string" || body === "") { errors.push("Write the article itself"); } const tags = data.get("tags"); if (typeof tags !== "string") { errors.push("The tags must be a string"); } if (errors.length > 0) { throw errors; } return { title, description, body, tags: data.get("tags") ?? "" } as { title: string; description: string; body: string; tags: string; }; } ``` Có thể nói, nó hơi dài và lặp lại, nhưng đó là cái giá chúng ta phải trả cho các lỗi có thể đọc được. Điều này cũng có thể là Zod schema, ví dụ, nhưng sau đó chúng ta sẽ phải render thông báo lỗi trên frontend, và form này không đáng để phức tạp hóa. Một bước cuối cùng — kết nối trang, loader, và action với các route. Vì chúng ta hỗ trợ gọn gàng cả tạo và chỉnh sửa, chúng ta có thể export cùng một thứ từ cả `editor._index.tsx` và `editor.$slug.tsx`: pages/article-edit/index.ts ``` export { ArticleEditPage } from "./ui/ArticleEditPage"; export { loader } from "./api/loader"; export { action } from "./api/action"; ``` app/routes/editor.\_index.tsx, app/routes/editor.$slug.tsx (same content) ``` import { ArticleEditPage } from "pages/article-edit"; export { loader, action } from "pages/article-edit"; export default ArticleEditPage; ``` Chúng ta hoàn thành rồi! Đăng nhập và thử tạo article mới. Hoặc "quên" viết article và xem validation hoạt động. ![The Conduit article editor, with the title field saying \"New article\" and the rest of the fields empty. Above the form there are two errors: \"Describe what this article is about\" and \"Write the article itself\".](/documentation/vi/assets/images/realworld-article-editor-bc3ee45c96ae905fdbb54d6463d12723.jpg) Trình chỉnh sửa article Conduit, với trường tiêu đề nói "New article" và phần còn lại của các trường trống. Phía trên form có hai lỗi: **"Describe what this article is about"** và **"Write the article itself"**. Các trang profile và settings rất giống với trình đọc và chỉnh sửa article, chúng được để lại như bài tập cho người đọc, đó là bạn :) --- # Xử lý API Requests ## Shared API Requests[​](#shared-api-requests "Link trực tiếp đến heading") Bắt đầu bằng cách đặt logic API request chung trong thư mục `shared/api`. Điều này giúp dễ dàng tái sử dụng các request trong toàn bộ ứng dụng và hỗ trợ prototyping nhanh hơn. Đối với nhiều dự án, đây là tất cả những gì bạn cần cho các API call. Cấu trúc file điển hình sẽ là: * 📂 shared * 📂 api * 📄 client.ts * 📄 index.ts * 📂 endpoints * 📄 login.ts File `client.ts` tập trung thiết lập HTTP request của bạn. Nó bao bọc phương thức bạn chọn (như `fetch()` hoặc một instance `axios`) và xử lý các cấu hình chung, chẳng hạn như: * Backend base URL. * Default headers (ví dụ, cho authentication). * Data serialization. Dưới đây là các ví dụ cho `axios` và `fetch`: * Axios * Fetch shared/api/client.ts ``` // Example using axios import axios from 'axios'; export const client = axios.create({ baseURL: 'https://your-api-domain.com/api/', timeout: 5000, headers: { 'X-Custom-Header': 'my-custom-value' } }); ``` shared/api/client.ts ``` export const client = { async post(endpoint: string, body: any, options?: RequestInit) { const response = await fetch(`https://your-api-domain.com/api${endpoint}`, { method: 'POST', body: JSON.stringify(body), ...options, headers: { 'Content-Type': 'application/json', 'X-Custom-Header': 'my-custom-value', ...options?.headers, }, }); return response.json(); } // ... other methods like put, delete, etc. }; ``` Tổ chức các function API request riêng lẻ của bạn trong `shared/api/endpoints`, nhóm chúng theo API endpoint. ghi chú Để giữ các ví dụ tập trung, chúng tôi bỏ qua form interaction và validation. Để biết chi tiết về các thư viện như Zod hoặc Valibot, hãy tham khảo bài viết [Type Validation và Schemas](/documentation/vi/docs/guides/examples/types.md#type-validation-schemas-and-zod). shared/api/endpoints/login.ts ``` import { client } from '../client'; export interface LoginCredentials { email: string; password: string; } export function login(credentials: LoginCredentials) { return client.post('/login', credentials); } ``` Sử dụng file `index.ts` trong `shared/api` để export các function request của bạn. shared/api/index.ts ``` export { client } from './client'; // Nếu bạn muốn export client export { login } from './endpoints/login'; export type { LoginCredentials } from './endpoints/login'; ``` ## Slice-Specific API Requests[​](#slice-specific-api-requests "Link trực tiếp đến heading") Nếu một API request chỉ được sử dụng bởi một slice cụ thể (như một page hoặc feature đơn lẻ) và sẽ không được tái sử dụng, hãy đặt nó trong segment api của slice đó. Điều này giúp logic slice-specific được chứa đóng một cách gọn gàng. * 📂 pages * 📂 login * 📄 index.ts * 📂 api * 📄 login.ts * 📂 ui * 📄 LoginPage.tsx pages/login/api/login.ts ``` import { client } from 'shared/api'; interface LoginCredentials { email: string; password: string; } export function login(credentials: LoginCredentials) { return client.post('/login', credentials); } ``` Bạn không cần export function `login()` trong public API của page, vì không có khả năng nơi nào khác trong app sẽ cần request này. ghi chú Tránh đặt API calls và response types trong layer `entities` quá sớm. Backend responses có thể khác với những gì frontend entities của bạn cần. Logic API trong `shared/api` hoặc segment `api` của slice cho phép bạn transform data một cách phù hợp, giữ cho entities tập trung vào các mối quan tâm của frontend. ## Sử dụng Client Generators[​](#client-generators "Link trực tiếp đến heading") Nếu backend của bạn có OpenAPI specification, các công cụ như [orval](https://orval.dev/) hoặc [openapi-typescript](https://openapi-ts.dev/) có thể generate API types và request functions cho bạn. Đặt code được generate trong, ví dụ, `shared/api/openapi`. Đảm bảo bao gồm `README.md` để document những types đó là gì và cách generate chúng. ## Tích hợp với Server State Libraries[​](#server-state-libraries "Link trực tiếp đến heading") Khi sử dụng server state libraries như [TanStack Query (React Query)](https://tanstack.com/query/latest) hoặc [Pinia Colada](https://pinia-colada.esm.dev/), bạn có thể cần chia sẻ types hoặc cache keys giữa các slices. Sử dụng layer `shared` cho những thứ như: * API data types * Cache keys * Common query/mutation options Để biết thêm chi tiết về cách làm việc với server state libraries, hãy tham khảo [bài viết React Query](/documentation/vi/docs/guides/tech/with-react-query.md) --- # Authentication Nói chung, authentication bao gồm các bước sau: 1. Lấy credentials từ người dùng 2. Gửi chúng đến backend 3. Lưu trữ token để thực hiện các authenticated requests ## Cách lấy credentials từ người dùng[​](#cách-lấy-credentials-từ-người-dùng "Link trực tiếp đến heading") Chúng tôi giả định rằng app của bạn chịu trách nhiệm lấy credentials. Nếu bạn có authentication qua OAuth, bạn có thể đơn giản tạo một login page với link đến login page của OAuth provider và bỏ qua đến [bước 3](#how-to-store-the-token-for-authenticated-requests). ### Page chuyên dụng cho login[​](#page-chuyên-dụng-cho-login "Link trực tiếp đến heading") Thông thường, các websites có các pages chuyên dụng cho login, nơi bạn nhập username và password. Các pages này khá đơn giản, vì vậy chúng không yêu cầu decomposition. Login và registration forms khá giống nhau về ngoại hình, vì vậy chúng thậm chí có thể được nhóm vào một page. Tạo một slice cho login/registration page của bạn trên layer Pages: * 📂 pages * 📂 login * 📂 ui * 📄 LoginPage.tsx (or your framework's component file format) * 📄 RegisterPage.tsx * 📄 index.ts * other pages… Ở đây chúng tôi tạo hai components và export cả hai trong index file của slice. Các components này sẽ chứa forms chịu trách nhiệm trình bày cho người dùng các controls dễ hiểu để lấy credentials của họ. ### Dialog cho login[​](#dialog-cho-login "Link trực tiếp đến heading") Nếu app của bạn có dialog cho login có thể được sử dụng trên bất kỳ page nào, hãy cân nhắc tạo dialog đó thành một widget. Bằng cách đó, bạn vẫn có thể tránh quá nhiều decomposition, nhưng có tự do tái sử dụng dialog này trên bất kỳ page nào. * 📂 widgets * 📂 login-dialog * 📂 ui * 📄 LoginDialog.tsx * 📄 index.ts * other widgets… Phần còn lại của hướng dẫn này được viết cho cách tiếp cận dedicated page, nhưng các nguyên tắc tương tự áp dụng cho dialog widget. ### Client-side validation[​](#client-side-validation "Link trực tiếp đến heading") Thiệng thoảng, đặc biệt là cho registration, việc thực hiện client-side validation để cho người dùng biết nhanh chóng rằng họ đã mắc lỗi là hợp lý. Validation có thể diễn ra trong segment `model` của login page. Sử dụng một schema validation library, ví dụ, [Zod](https://zod.dev) cho JS/TS, và expose schema đó cho segment `ui`: pages/login/model/registration-schema.ts ``` import { z } from "zod"; export const registrationData = z.object({ email: z.string().email(), password: z.string().min(6), confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { message: "Passwords do not match", path: ["confirmPassword"], }); ``` Sau đó, trong segment `ui`, bạn có thể sử dụng schema này để validate user input: pages/login/ui/RegisterPage.tsx ``` import { registrationData } from "../model/registration-schema"; function validate(formData: FormData) { const data = Object.fromEntries(formData.entries()); try { registrationData.parse(data); } catch (error) { // TODO: Show error message to the user } } export function RegisterPage() { return (
validate(new FormData(e.target))}>
) } ``` ## Cách gửi credentials đến backend[​](#cách-gửi-credentials-đến-backend "Link trực tiếp đến heading") Tạo một function thực hiện request đến login endpoint của backend. Function này có thể được gọi trực tiếp trong component code sử dụng mutation library (ví dụ TanStack Query), hoặc có thể được gọi như side effect trong state manager. Như được giải thích trong [hướng dẫn cho API requests](/documentation/vi/docs/guides/examples/api-requests.md), bạn có thể đặt request của mình trong `shared/api` hoặc trong segment `api` của login page. ### Two-factor authentication[​](#two-factor-authentication "Link trực tiếp đến heading") Nếu app của bạn hỗ trợ two-factor authentication (2FA), bạn có thể phải redirect đến page khác nơi người dùng có thể nhập one-time password. Thông thường `POST /login` request của bạn sẽ trả về user object với flag chỉ ra rằng người dùng đã bật 2FA. Nếu flag đó được thiết lập, redirect người dùng đến 2FA page. Vì page này liên quan rất chặt chẽ đến logging in, bạn cũng có thể giữ nó trong cùng một slice, `login` trên layer Pages. Bạn cũng cần một request function khác, tương tự như `login()` mà chúng tôi đã tạo ở trên. Đặt chúng cùng nhau, hoặc trong Shared, hoặc trong segment `api` của page `login`. ## Cách lưu trữ token cho authenticated requests[​](#how-to-store-the-token-for-authenticated-requests "Link trực tiếp đến heading") Bất kể authentication scheme nào bạn có, cho dù là login & password đơn giản, OAuth, hoặc two-factor authentication, cuối cùng bạn sẽ nhận được một token. Token này nên được lưu trữ để các requests tiếp theo có thể identify chính chúng. Lưu trữ token lý tưởng cho web app là **cookie** — nó không yêu cầu token storage hoặc handling thủ công. Vì vậy, cookie storage hầu như không cần cân nhắc gì từ phía frontend architecture. Nếu frontend framework của bạn có server side (ví dụ, [Remix](https://remix.run)), thì bạn nên lưu trữ server-side cookie infrastructure trong `shared/api`. Có một ví dụ trong [phần Authentication của tutorial](/documentation/vi/docs/get-started/tutorial.md#authentication) về cách thực hiện điều đó với Remix. Tuy nhiên, đôi khi cookie storage không phải là lựa chọn. Trong trường hợp này, bạn sẽ phải lưu trữ token thủ công. Ngoài việc lưu trữ token, bạn cũng có thể cần thiết lập logic để refresh token khi nó expires. Với FSD, có nhiều nơi bạn có thể lưu trữ token, cũng như nhiều cách để làm cho nó available cho phần còn lại của app. ### Trong Shared[​](#trong-shared "Link trực tiếp đến heading") Cách tiếp cận này hoạt động tốt với API client được define trong `shared/api` vì token có sẵn một cách tự do cho các request functions khác yêu cầu authentication để thành công. Bạn có thể làm cho API client giữ state, hoặc với reactive store hoặc đơn giản là module-level variable, và cập nhật state đó trong các functions `login()`/`logout()` của bạn. Automatic token refresh có thể được implement như middleware trong API client — thứ gì đó có thể thực thi mỗi khi bạn thực hiện bất kỳ request nào. Nó có thể hoạt động như thế này: * Authenticate và lưu trữ access token cũng như refresh token * Thực hiện bất kỳ request nào yêu cầu authentication * Nếu request thất bại với status code chỉ ra token expiration, và có token trong store, thực hiện refresh request, lưu trữ các tokens mới, và retry request gốc Một trong những drawbacks của cách tiếp cận này là logic managing và refreshing token không có một nơi chuyên dụng. Điều này có thể ổn đối với một số apps hoặc teams, nhưng nếu logic token management phức tạp hơn, có thể tốt hơn là tách biệt trách nhiệm của việc thực hiện requests và managing tokens. Bạn có thể làm điều đó bằng cách giữ requests và API client trong `shared/api`, nhưng token store và management logic trong `shared/auth`. Một drawback khác của cách tiếp cận này là nếu backend của bạn trả về object thông tin của current user cùng với token, bạn phải lưu trữ điều đó ở đâu đó hoặc bỏ qua thông tin đó và request lại từ endpoint như `/me` hoặc `/users/current`. ### Trong Entities[​](#trong-entities "Link trực tiếp đến heading") Thông thường các dự án FSD có một entity cho user và/hoặc một entity cho current user. Nó thậm chí có thể là cùng một entity cho cả hai. ghi chú **Current user** đôi khi cũng được gọi là "viewer" hoặc "me". Điều này là để phân biệt single authenticated user, với permissions và private information, từ danh sách tất cả users với publicly accessible information. Để lưu trữ token trong User entity, tạo reactive store trong segment `model`. Store đó có thể chứa cả token và user object. Vì API client thường được define trong `shared/api` hoặc spread qua các entities, thách thức chính của cách tiếp cận này là làm cho token available cho các requests khác cần nó mà không vi phạm [import rule trên các layers](/documentation/vi/docs/reference/layers.md#import-rule-on-layers): > Một module (file) trong slice chỉ có thể import các slices khác khi chúng được đặt trên các layers ở phía dưới. Có nhiều giải pháp cho thách thức này: 1. **Pass token thủ công mỗi lần bạn thực hiện request**
Đây là giải pháp đơn giản nhất, nhưng nó nhanh chóng trở nên cồng kềnh, và nếu bạn không có type safety, dễ quên. Nó cũng không tương thích với middlewares pattern cho API client trong Shared. 2. **Expose token cho toàn bộ app với context hoặc global store như `localStorage`**
Key để retrieve token sẽ được giữ trong `shared/api` để API client có thể truy cập nó. Reactive store của token sẽ được export từ User entity, và context provider (nếu cần) sẽ được thiết lập trên layer App. Điều này cho nhiều tự do hơn để thiết kế API client, tuy nhiên, nó tạo ra implicit dependency trên các layers cao hơn để cung cấp context. Khi theo cách tiếp cận này, hãy cân nhắc cung cấp các error messages hữu ích nếu context hoặc `localStorage` không được thiết lập chính xác. 3. **Inject token vào API client mỗi khi nó thay đổi**
Nếu store của bạn là reactive, bạn có thể tạo subscription sẽ cập nhật token store của API client mỗi khi store trong entity thay đổi. Điều này tương tự như giải pháp trước ở chỗ chúng đều tạo implicit dependency trên các layers cao hơn, nhưng cái này imperative hơn ("push"), trong khi cái trước declarative hơn ("pull"). Khi bạn vượt qua thách thức expose token được lưu trữ trong model của entity, bạn có thể encode nhiều business logic liên quan đến token management. Ví dụ, segment `model` có thể chứa logic để invalidate token sau một khoảng thời gian nhất định, hoặc refresh token khi nó expires. Để thực sự thực hiện requests đến backend, sử dụng segment `api` của User entity hoặc `shared/api`. ### Trong Pages/Widgets (không được khuyến nghị)[​](#trong-pageswidgets-không-được-khuyến-nghị "Link trực tiếp đến heading") Không được khuyến khích lưu trữ app-wide state như access token trong pages hoặc widgets. Tránh đặt token store của bạn trong segment `model` của login page, thay vào đó hãy chọn từ hai giải pháp đầu tiên, Shared hoặc Entities. ## Logout và token invalidation[​](#logout-và-token-invalidation "Link trực tiếp đến heading") Thông thường, các apps không có một page hoàn chỉnh cho logging out, nhưng logout functionality vẫn rất quan trọng. Nó bao gồm authenticated request đến backend và cập nhật token store. Nếu bạn lưu trữ tất cả requests trong `shared/api`, hãy giữ logout request function ở đó, gần login function. Nếu không, hãy cân nhắc giữ logout request function gần button kích hoạt nó. Ví dụ, nếu bạn có header widget xuất hiện trên mỗi page và chứa logout link, hãy đặt request đó trong segment `api` của widget đó. Cập nhật token store sẽ phải được trigger từ vị trí của logout button, như header widget. Bạn có thể kết hợp request và store update trong segment `model` của widget đó. ### Automatic logout[​](#automatic-logout "Link trực tiếp đến heading") Đừng quên xây dựng các failsafes cho khi request log out thất bại, hoặc request refresh login token thất bại. Trong cả hai trường hợp này, bạn nên clear token store. Nếu bạn giữ token trong Entities, code này có thể được đặt trong segment `model` vì nó là pure business logic. Nếu bạn giữ token trong Shared, đặt logic này trong `shared/api` có thể làm segment phình to và pha loãng mục đích của nó. Nếu bạn nhận thấy rằng API segment của mình chứa nhiều thứ không liên quan, hãy cân nhắc tách logic token management thành segment khác, ví dụ, `shared/auth`. --- # Autocomplete WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/170) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Về decomposition theo layers ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Discussion) Về việc áp dụng phương pháp luận cho việc lựa chọn với loaded dictionaries](https://github.com/feature-sliced/documentation/discussions/65#discussioncomment-480807) --- # Browser API WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/197) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Về việc làm việc với Browser API: localStorage, audio Api, bluetooth API, v.v. > > Bạn có thể hỏi về ý tưởng chi tiết hơn [@alex\_novi](https://t.me/alex_novich) --- # CMS WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/172) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Features có thể khác nhau[​](#features-có-thể-khác-nhau "Link trực tiếp đến heading") Trong một số dự án, tất cả functionality được tập trung trong data từ server > ## Cách làm việc đúng đắn hơn với CMS markup[​](#cách-làm-việc-đúng-đắn-hơn-với-cms-markup "Link trực tiếp đến heading") > > --- # Feedback WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/187) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Errors, Alerts, Notifications, ... --- # i18n WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/171) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Đặt ở đâu? Làm thế nào để làm việc với điều này?[​](#đặt-ở-đâu-làm-thế-nào-để-làm-việc-với-điều-này "Link trực tiếp đến heading") * * * --- # Metric WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/181) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About ways to initialize metrics in the application --- # Monorepositories WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/221) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About applicability for mono repositories, about bff, about microapps ## See also[​](#see-also "Link trực tiếp đến heading") * [(Discussion) About mono repositories and plug-ins-packages](https://github.com/feature-sliced/documentation/discussions/50) * [(Thread) About the application for a mono repository](https://t.me/feature_sliced/2412) --- # Page layouts Hướng dẫn này xem xét abstraction của một *page layout* — khi nhiều pages chia sẻ cùng một cấu trúc tổng thể, và chỉ khác nhau ở main content. thông tin Câu hỏi của bạn không được đề cập trong hướng dẫn này? Đăng câu hỏi của bạn bằng cách để lại feedback trên bài viết này (nút xanh ở bên phải) và chúng tôi sẽ cân nhắc mở rộng hướng dẫn này! ## Simple layout[​](#simple-layout "Link trực tiếp đến heading") Layout đơn giản nhất có thể được nhìn thấy trên trang bạn đang xem đây. Nó có header với site navigation, hai sidebars, và footer với external links. Không có business logic phức tạp, và các phần dynamic duy nhất là sidebars và switchers ở phía bên phải của header. Layout như vậy có thể được đặt hoàn toàn trong `shared/ui` hoặc trong `app/layouts`, với props điền vào content cho các sidebars: shared/ui/layout/Layout.tsx ``` import { Link, Outlet } from "react-router-dom"; import { useThemeSwitcher } from "./useThemeSwitcher"; export function Layout({ siblingPages, headings }) { const [theme, toggleTheme] = useThemeSwitcher(); return (
{/* This is where the main content goes */}
  • GitHub
  • Twitter
); } ``` shared/ui/layout/useThemeSwitcher.ts ``` export function useThemeSwitcher() { const [theme, setTheme] = useState("light"); function toggleTheme() { setTheme(theme === "light" ? "dark" : "light"); } useEffect(() => { document.body.classList.remove("light", "dark"); document.body.classList.add(theme); }, [theme]); return [theme, toggleTheme] as const; } ``` Code của các sidebars được để lại như bài tập cho độc giả 😉. ## Sử dụng widgets trong layout[​](#sử-dụng-widgets-trong-layout "Link trực tiếp đến heading") Thiệng thoảng bạn muốn bao gồm business logic nhất định trong layout, đặc biệt nếu bạn đang sử dụng deeply nested routes với router như [React Router](https://reactrouter.com/). Khi đó bạn không thể lưu trữ layout trong Shared hoặc trong Widgets do [import rule trên các layers](/documentation/vi/docs/reference/layers.md#import-rule-on-layers): > Một module trong slice chỉ có thể import các slices khác khi chúng được đặt trên các layers ở phía dưới. Trước khi chúng ta thảo luận các giải pháp, chúng ta cần thảo luận liệu đó có phải là vấn đề ngay từ đầu hay không. Bạn có *thực sự cần* layout đó không, và nếu có, liệu nó *thực sự cần* là Widget không? Nếu block business logic đang bàn được tái sử dụng trên 2-3 pages, và layout chỉ đơn giản là wrapper nhỏ cho widget đó, hãy cân nhắc một trong hai lựa chọn này: 1. **Viết layout inline trên layer App, nơi bạn cấu hình routing**
Điều này rất tốt cho các routers hỗ trợ nesting, vì bạn có thể nhóm các routes nhất định và chỉ áp dụng layout cho chúng. 2. **Chỉ cần copy-paste nó**
Xu hướng abstract code thường bị đánh giá quá cao. Điều này đặc biệt đúng cho các layouts, hiếm khi thay đổi. Tại một thời điểm nào đó, nếu một trong những pages này cần thay đổi, bạn có thể đơn giản thực hiện thay đổi mà không cần thiết ảnh hưởng đến các pages khác. Nếu bạn lo lắng rằng ai đó có thể quên cập nhật các pages khác, bạn luôn có thể để lại comment mô tả mối quan hệ giữa các pages. Nếu không có điều nào ở trên áp dụng được, có hai giải pháp để bao gồm widget trong layout: 1. **Sử dụng render props hoặc slots**
Hầu hết các frameworks cho phép bạn pass một phần UI từ bên ngoài. Trong React, được gọi là [render props](https://www.patterns.dev/react/render-props-pattern/), trong Vue được gọi là [slots](https://vuejs.org/guide/components/slots). 2. **Chuyển layout đến layer App**
Bạn cũng có thể lưu trữ layout của mình trên layer App, ví dụ, trong `app/layouts`, và compose bất kỳ widgets nào bạn muốn. ## Đọc thêm[​](#đọc-thêm "Link trực tiếp đến heading") * Có một ví dụ về cách xây dựng layout với authentication sử dụng React và Remix (tương đương với React Router) trong [tutorial](/documentation/vi/docs/get-started/tutorial.md). --- # Desktop/Touch platforms WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/198) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About the application of the methodology for desktop/touch --- # SSR WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/173) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About the implementation of SSR using the methodology --- # Theme WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/207) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Where should I put my work with the theme and palette?[​](#where-should-i-put-my-work-with-the-theme-and-palette "Link trực tiếp đến heading") > ## Discussion about the location of the theme, i18n logic[​](#discussion-about-the-location-of-the-theme-i18n-logic "Link trực tiếp đến heading") > --- # Types Hướng dẫn này liên quan đến data types từ các typed languages như TypeScript và mô tả chúng phù hợp ở đâu trong FSD. thông tin Câu hỏi của bạn không được đề cập trong hướng dẫn này? Đăng câu hỏi của bạn bằng cách để lại feedback trên bài viết này (nút xanh ở bên phải) và chúng tôi sẽ cân nhắc mở rộng hướng dẫn này! ## Utility types[​](#utility-types "Link trực tiếp đến heading") Utility types là các types không có nhiều ý nghĩa riêng và thường được sử dụng với các types khác. Ví dụ: ``` type ArrayValues = T[number]; ``` Nguồn: Để làm cho utility types có sẵn trong toàn bộ dự án của bạn, hoặc là install một library như [`type-fest`](https://github.com/sindresorhus/type-fest), hoặc tạo library riêng của bạn trong `shared/lib`. Đảm bảo chỉ rõ ràng những types mới nào *nên* được thêm vào library này, và những types nào *không thuộc về* đó. Ví dụ, gọi nó là `shared/lib/utility-types` và thêm README bên trong mô tả utility type là gì trong team của bạn. Đừng đánh giá quá cao khả năng tái sử dụng của utility type. Chỉ vì nó có thể được tái sử dụng, không có nghĩa là nó sẽ được tái sử dụng, và vì vậy, không phải mọi utility type đều cần ở trong Shared. Một số utility types thì ổn ngay bên cạnh nơi chúng được cần: * 📂 pages * 📂 home * 📂 api * 📄 ArrayValues.ts (utility type) * 📄 getMemoryUsageMetrics.ts (code sử dụng utility type) cảnh báo Hãy cưỡng lại cám dỗ tạo folder `shared/types`, hoặc thêm segment `types` vào slices của bạn. Category "types" tương tự như category "components" hoặc "hooks" ở chỗ nó mô tả nội dung là gì, chứ không phải chúng dành cho gì. Segments nên mô tả mục đích của code, không phải bản chất. ## Business entities và cross-references của chúng[​](#business-entities-và-cross-references-của-chúng "Link trực tiếp đến heading") Trong số những types quan trọng nhất trong app là types của business entities, tức là những thứ trong thế giới thực mà app của bạn làm việc với. Ví dụ, trong music streaming app, bạn có thể có business entities *Song*, *Album*, v.v. Business entities thường đến từ backend, vì vậy bước đầu tiên là type backend responses. Thật tiện lợi khi có function để thực hiện request đến mỗi endpoint, và type response của function này. Để có thêm type safety, bạn có thể muốn chạy response qua schema validation library như [Zod](https://zod.dev). Ví dụ, nếu bạn giữ tất cả requests của mình trong Shared, bạn có thể làm như thế này: shared/api/songs.ts ``` import type { Artist } from "./artists"; interface Song { id: number; title: string; artists: Array; } export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise>); } ``` Bạn có thể nhận thấy rằng type `Song` tham chiếu đến một entity khác, `Artist`. Đây là lợi ích của việc lưu trữ requests của bạn trong Shared — các types thế giới thực thường đan xen. Nếu chúng ta giữ function này trong `entities/song/api`, chúng ta sẽ không thể đơn giản import `Artist` từ `entities/artist`, vì FSD hạn chế cross-imports giữa các slices với [import rule trên các layers](/documentation/vi/docs/reference/layers.md#import-rule-on-layers): > Một module trong slice chỉ có thể import các slices khác khi chúng được đặt trên các layers ở phía dưới. Có hai cách để giải quyết vấn đề này: 1. **Tham số hóa các types của bạn**
Bạn có thể làm cho types của mình chấp nhận type arguments làm slots cho các kết nối với entities khác, và thậm chí áp dụng constraints trên những slots đó. Ví dụ: entities/song/model/song.ts ``` interface Song { id: number; title: string; artists: Array; } ``` Điều này hoạt động tốt hơn cho một số types so với những types khác. Một type đơn giản như `Cart = { items: Array }` có thể dễ dàng được làm để hoạt động với bất kỳ loại product nào. Các types kết nối nhiều hơn, như `Country` và `City`, có thể không dễ tách rời. 2. **Cross-import (nhưng làm đúng cách)**
Để thực hiện cross-imports giữa các entities trong FSD, bạn có thể sử dụng public API đặc biệt dành riêng cho mỗi slice sẽ cross-importing. Ví dụ, nếu chúng ta có entities `song`, `artist`, và `playlist`, và hai cái sau cần tham chiếu `song`, chúng ta có thể tạo hai public APIs đặc biệt cho cả hai trong entity `song` với ký hiệu `@x`: * 📂 entities * 📂 song * 📂 @x * 📄 artist.ts (public API cho entity `artist` để import) * 📄 playlist.ts (public API cho entity `playlist` để import) * 📄 index.ts (public API thông thường) Nội dung của file `📄 entities/song/@x/artist.ts` tương tự như `📄 entities/song/index.ts`: entities/song/@x/artist.ts ``` export type { Song } from "../model/song.ts"; ``` Sau đó `📄 entities/artist/model/artist.ts` có thể import `Song` như thế này: entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` Bằng cách tạo kết nối rõ ràng giữa các entities, chúng ta kiểm soát được inter-dependencies và duy trì mức độ phân tách domain tốt. ## Data transfer objects và mappers[​](#data-transfer-objects-and-mappers "Link trực tiếp đến heading") Data transfer objects, hay DTOs, là thuật ngữ mô tả hình dạng của dữ liệu đến từ backend. Đôi khi, DTO có thể sử dụng ngay, nhưng đôi khi nó không thuận tiện cho frontend. Đó là lúc mappers xuất hiện — chúng biến đổi DTO thành hình dạng thuận tiện hơn. ### Đặt DTOs ở đâu[​](#đặt-dtos-ở-đâu "Link trực tiếp đến heading") Nếu bạn có backend types trong package riêng (ví dụ, nếu bạn chia sẻ code giữa frontend và backend), thì chỉ cần import DTOs từ đó và xong! Nếu bạn không chia sẻ code giữa backend và frontend, thì bạn cần giữ DTOs ở đâu đó trong frontend codebase, và chúng ta sẽ khám phá trường hợp này dưới đây. Nếu bạn có request functions trong `shared/api`, đó là nơi DTOs nên ở, ngay cạnh function sử dụng chúng: shared/api/songs.ts ``` import type { ArtistDTO } from "./artists"; interface SongDTO { id: number; title: string; artist_ids: Array; } export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise>); } ``` Như đã đề cập trong phần trước, lưu trữ requests và DTOs của bạn trong Shared mang lại lợi ích có thể tham chiếu DTOs khác. ### Đặt mappers ở đâu[​](#đặt-mappers-ở-đâu "Link trực tiếp đến heading") Mappers là các functions chấp nhận DTO để biến đổi, và vì vậy, chúng nên được đặt gần định nghĩa của DTO. Trong thực tế điều này có nghĩa là nếu requests và DTOs của bạn được định nghĩa trong `shared/api`, thì mappers cũng nên ở đó: shared/api/songs.ts ``` import type { ArtistDTO } from "./artists"; interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array; } interface Song { id: string; title: string; /** Tiêu đề đầy đủ của bài hát, bao gồm số đĩa. */ fullTitle: string; artistIds: Array; } function adaptSongDTO(dto: SongDTO): Song { return { id: String(dto.id), title: dto.title, fullTitle: `${dto.disc_no} / ${dto.title}`, artistIds: dto.artist_ids.map(String), }; } export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); } ``` Nếu requests và stores của bạn được định nghĩa trong entity slices, thì tất cả code này sẽ đi vào đó, nhớ lưu ý giới hạn của cross-imports giữa các slices: entities/song/api/dto.ts ``` import type { ArtistDTO } from "entities/artist/@x/song"; export interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array; } ``` entities/song/api/mapper.ts ``` import type { SongDTO } from "./dto"; export interface Song { id: string; title: string; /** Tiêu đề đầy đủ của bài hát, bao gồm số đĩa. */ fullTitle: string; artistIds: Array; } export function adaptSongDTO(dto: SongDTO): Song { return { id: String(dto.id), title: dto.title, fullTitle: `${dto.disc_no} / ${dto.title}`, artistIds: dto.artist_ids.map(String), }; } ``` entities/song/api/listSongs.ts ``` import { adaptSongDTO } from "./mapper"; export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); } ``` entities/song/model/songs.ts ``` import { createSlice, createEntityAdapter } from "@reduxjs/toolkit"; import { listSongs } from "../api/listSongs"; export const fetchSongs = createAsyncThunk('songs/fetchSongs', listSongs); const songAdapter = createEntityAdapter(); const songsSlice = createSlice({ name: "songs", initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSongs.fulfilled, (state, action) => { songAdapter.upsertMany(state, action.payload); }) }, }); ``` ### Cách xử lý nested DTOs[​](#cách-xử-lý-nested-dtos "Link trực tiếp đến heading") Phần có vấn đề nhất là khi response từ backend chứa nhiều entities. Ví dụ, nếu bài hát bao gồm không chỉ IDs của tác giả, mà cả toàn bộ author objects. Trong trường hợp này, không thể cho các entities không biết về nhau (trừ khi chúng ta muốn loại bỏ dữ liệu hoặc có cuộc trò chuyện nghiêm túc với backend team). Thay vì nghĩ ra giải pháp cho kết nối gián tiếp giữa các slices (như common middleware sẽ dispatch actions đến slices khác), ưu tiên cross-imports rõ ràng với ký hiệu `@x`. Đây là cách chúng ta có thể triển khai với Redux Toolkit: entities/song/model/songs.ts ``` import { createSlice, createEntityAdapter, createAsyncThunk, createSelector, } from '@reduxjs/toolkit' import { normalize, schema } from 'normalizr' import { getSong } from "../api/getSong"; // Định nghĩa normalizr entity schemas export const artistEntity = new schema.Entity('artists') export const songEntity = new schema.Entity('songs', { artists: [artistEntity], }) const songAdapter = createEntityAdapter() export const fetchSong = createAsyncThunk( 'songs/fetchSong', async (id: string) => { const data = await getSong(id) // Normalize dữ liệu để reducers có thể load payload dự đoán được, như: // `action.payload = { songs: {}, artists: {} }` const normalized = normalize(data, songEntity) return normalized.entities } ) export const slice = createSlice({ name: 'songs', initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSong.fulfilled, (state, action) => { songAdapter.upsertMany(state, action.payload.songs) }) }, }) const reducer = slice.reducer export default reducer ``` entities/song/@x/artist.ts ``` export { fetchSong } from "../model/songs"; ``` entities/artist/model/artists.ts ``` import { createSlice, createEntityAdapter } from '@reduxjs/toolkit' import { fetchSong } from 'entities/song/@x/artist' const artistAdapter = createEntityAdapter() export const slice = createSlice({ name: 'users', initialState: artistAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSong.fulfilled, (state, action) => { // Và xử lý cùng fetch result bằng cách chèn artists ở đây artistAdapter.upsertMany(state, action.payload.artists) }) }, }) const reducer = slice.reducer export default reducer ``` Điều này hạn chế một chút lợi ích của slice isolation, nhưng nó thể hiện chính xác kết nối giữa hai entities này mà chúng ta không kiểm soát được. Nếu các entities này cần được refactor, chúng phải được refactor cùng nhau. ## Global types và Redux[​](#global-types-và-redux "Link trực tiếp đến heading") Global types là types sẽ được sử dụng trong toàn bộ ứng dụng. Có hai loại global types, dựa trên những gì chúng cần biết: 1. Generic types không có bất kỳ đặc điểm ứng dụng nào 2. Types cần biết về toàn bộ ứng dụng Trường hợp đầu tiên đơn giản để giải quyết — đặt types của bạn trong Shared, trong segment phù hợp. Ví dụ, nếu bạn có interface cho global variable cho analytics, bạn có thể đặt nó trong `shared/analytics`. cảnh báo Tránh tạo folder `shared/types`. Nó nhóm những thứ không liên quan chỉ dựa trên thuộc tính "là một type", và thuộc tính đó thường không hữu ích khi tìm kiếm code trong dự án. Trường hợp thứ hai thường gặp trong các dự án với Redux không có RTK. Store type cuối cùng của bạn chỉ có sẵn khi bạn thêm tất cả reducers lại với nhau, nhưng store type này cần có sẵn cho selectors mà bạn sử dụng trong app. Ví dụ, đây là định nghĩa store điển hình của bạn: app/store/index.ts ``` import { combineReducers, rootReducer } from "redux"; import { songReducer } from "entities/song"; import { artistReducer } from "entities/artist"; const rootReducer = combineReducers(songReducer, artistReducer); const store = createStore(rootReducer); type RootState = ReturnType; type AppDispatch = typeof store.dispatch; ``` Sẽ tốt nếu có typed Redux hooks `useAppDispatch` và `useAppSelector` trong `shared/store`, nhưng chúng không thể import `RootState` và `AppDispatch` từ App layer do [import rule trên layers](/documentation/vi/docs/reference/layers.md#import-rule-on-layers): > Một module trong slice chỉ có thể import các slices khác khi chúng được đặt trên layers nghiêm ngặt bên dưới. Giải pháp được khuyến nghị trong trường hợp này là tạo implicit dependency giữa layers Shared và App. Hai types này, `RootState` và `AppDispatch` không chắc sẽ thay đổi, và chúng sẽ quen thuộc với Redux developers, vì vậy chúng ta không phải lo lắng về chúng nhiều. Trong TypeScript, bạn có thể làm điều đó bằng cách khai báo types là global như thế này: app/store/index.ts ``` /* cùng nội dung như trong code block trước… */ declare type RootState = ReturnType; declare type AppDispatch = typeof store.dispatch; ``` shared/store/index.ts ``` import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux"; export const useAppDispatch = useDispatch.withTypes() export const useAppSelector: TypedUseSelectorHook = useSelector; ``` ## Enums[​](#enums "Link trực tiếp đến heading") Quy tắc chung với enums là chúng nên được định nghĩa **càng gần với vị trí sử dụng càng tốt**. Khi enum đại diện cho các giá trị cụ thể cho một feature duy nhất, nó nên được định nghĩa trong cùng feature đó. Việc chọn segment cũng nên được quyết định bởi vị trí sử dụng. Nếu enum của bạn chứa, ví dụ, vị trí của toast trên màn hình, nó nên được đặt trong segment `ui`. Nếu nó đại diện cho loading state của backend operation, nó nên được đặt trong segment `api`. Một số enums thực sự chung cho toàn bộ dự án, như general backend response statuses hoặc design system tokens. Trong trường hợp này, bạn có thể đặt chúng trong Shared, và chọn segment dựa trên enum đại diện cho gì (`api` cho response statuses, `ui` cho design tokens, v.v.). ## Type validation schemas và Zod[​](#type-validation-schemas-và-zod "Link trực tiếp đến heading") Nếu bạn muốn validate rằng dữ liệu của bạn phù hợp với hình dạng hoặc ràng buộc nhất định, bạn có thể định nghĩa validation schema. Trong TypeScript, library phổ biến cho công việc này là [Zod](https://zod.dev). Validation schemas cũng nên được colocated với code sử dụng chúng, càng nhiều càng tốt. Validation schemas tương tự như mappers (như đã thảo luận trong phần [Data transfer objects và mappers](#data-transfer-objects-and-mappers)) ở chỗ chúng nhận data transfer object và parse nó, tạo ra lỗi nếu parsing thất bại. Một trong những trường hợp phổ biến nhất của validation là cho dữ liệu đến từ backend. Thông thường, bạn muốn request thất bại khi dữ liệu không khớp với schema, vì vậy hợp lý khi đặt schema ở cùng nơi với request function, thường là segment `api`. Nếu dữ liệu của bạn đến qua user input, như form, validation nên xảy ra khi dữ liệu đang được nhập. Bạn có thể đặt schema trong segment `ui`, cạnh form component, hoặc trong segment `model`, nếu segment `ui` quá đông. ## Typings của component props và context[​](#typings-của-component-props-và-context "Link trực tiếp đến heading") Nói chung, tốt nhất là giữ props hoặc context interface trong cùng file với component hoặc context sử dụng chúng. Nếu bạn có framework với single-file components, như Vue hoặc Svelte, và bạn không thể định nghĩa props interface trong cùng file, hoặc bạn muốn chia sẻ interface đó giữa nhiều components, tạo file riêng trong cùng folder, thường là segment `ui`. Đây là ví dụ với JSX (React hoặc Solid): pages/home/ui/RecentActions.tsx ``` interface RecentActionsProps { actions: Array<{ id: string; text: string }>; } export function RecentActions({ actions }: RecentActionsProps) { /* … */ } ``` Và đây là ví dụ với interface được lưu trong file riêng cho Vue: pages/home/ui/RecentActionsProps.ts ``` export interface RecentActionsProps { actions: Array<{ id: string; text: string }>; } ``` pages/home/ui/RecentActions.vue ``` ``` ## Ambient declaration files (`*.d.ts`)[​](#ambient-declaration-files-dts "Link trực tiếp đến heading") Một số packages, ví dụ, [Vite](https://vitejs.dev) hoặc [ts-reset](https://www.totaltypescript.com/ts-reset), yêu cầu ambient declaration files để hoạt động trong app của bạn. Thường thì chúng không lớn hoặc phức tạp, vì vậy chúng thường không yêu cầu bất kỳ architecting nào, có thể chỉ cần đặt chúng trong folder `src/`. Để giữ `src` có tổ chức hơn, bạn có thể giữ chúng trên App layer, trong `app/ambient/`. Các packages khác đơn giản là không có typings, và bạn có thể muốn khai báo chúng là untyped hoặc thậm chí viết typings riêng cho chúng. Nơi tốt cho những typings đó sẽ là `shared/lib`, trong folder như `shared/lib/untyped-packages`. Tạo file `%LIBRARY_NAME%.d.ts` ở đó và khai báo types bạn cần: shared/lib/untyped-packages/use-react-screenshot.d.ts ``` // Library này không có typings, và chúng tôi không muốn phiền viết riêng. declare module "use-react-screenshot"; ``` ## Tự động sinh types[​](#tự-động-sinh-types "Link trực tiếp đến heading") Thường xuyên sinh types từ nguồn bên ngoài, ví dụ, sinh backend types từ OpenAPI schema. Trong trường hợp này, tạo nơi chuyên dụng trong codebase của bạn cho những types này, như `shared/api/openapi`. Lý tưởng nhất, bạn cũng nên bao gồm README trong folder đó mô tả những files này là gì, cách tái tạo chúng, v.v. --- # White Labels WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/215) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Figma, brand uikit, templates, adaptability to brands ## See also[​](#see-also "Link trực tiếp đến heading") * [(Thread) About the application for white-labels (branded) projects](https://t.me/feature_sliced/1543) * [(Presentation) About white-labels apps and design](http://yadi.sk/i/5IdhzsWrpO3v4Q) --- # Cross-imports WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/220) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Cross-imports xuất hiện khi layer hoặc abstraction bắt đầu đảm nhận quá nhiều trách nhiệm hơn nó nên. Đó là lý do tại sao phương pháp luận xác định các layers mới cho phép bạn tách rời những cross-imports này ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Thread) Về tính không thể tránh khỏi của cross-ports](https://t.me/feature_sliced/4515) * [(Thread) Về việc giải quyết cross-ports trong entities](https://t.me/feature_sliced/3678) * [(Thread) Về cross-imports và responsibility](https://t.me/feature_sliced/3287) * [(Thread) Về imports giữa các segments](https://t.me/feature_sliced/4021) * [(Thread) Về cross-imports bên trong shared](https://t.me/feature_sliced/3618) --- # Desegmented WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/148) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Tình huống[​](#tình-huống "Link trực tiếp đến heading") Rất thường xuyên xảy ra tình huống trên các project khi các module liên quan đến một domain cụ thể từ lĩnh vực chủ đề bị phân tách không cần thiết và nằm rải rác khắp project ``` ├── components/ | ├── DeliveryCard | ├── DeliveryChoice | ├── RegionSelect | ├── UserAvatar ├── actions/ | ├── delivery.js | ├── region.js | ├── user.js ├── epics/ | ├── delivery.js | ├── region.js | ├── user.js ├── constants/ | ├── delivery.js | ├── region.js | ├── user.js ├── helpers/ | ├── delivery.js | ├── region.js | ├── user.js ├── entities/ | ├── delivery/ | | ├── getters.js | | ├── selectors.js | ├── region/ | ├── user/ ``` ## Vấn đề[​](#vấn-đề "Link trực tiếp đến heading") Vấn đề thể hiện ít nhất là vi phạm nguyên tắc **High Cohesion** và kéo dài quá mức **trục thay đổi** ## Nếu bỏ qua[​](#nếu-bỏ-qua "Link trực tiếp đến heading") * Nếu cần chạm vào logic, ví dụ delivery - chúng ta sẽ phải nhớ rằng nó nằm ở nhiều nơi và phải chạm vào nhiều chỗ trong code - điều này kéo dài không cần thiết **Trục thay đổi** của chúng ta * Nếu cần nghiên cứu logic của user, chúng ta sẽ phải đi khắp project để tìm hiểu chi tiết **actions, epics, constants, entities, components** - thay vì để nó nằm ở một chỗ * Các liên kết ngầm và sự mất kiểm soát của domain area đang phát triển * Với cách tiếp cận này, mắt rất dễ bị mờ đi và bạn có thể không nhận ra khi chúng ta "tạo constants vì constants", tạo ra một đống rác trong thư mục tương ứng của project ## Giải pháp[​](#giải-pháp "Link trực tiếp đến heading") Đặt tất cả các module liên quan đến một domain/use case cụ thể - ngay cạnh nhau Để khi nghiên cứu một module cụ thể, tất cả các thành phần của nó nằm cạnh nhau, không bị rải rác khắp project > Điều này cũng tăng khả năng khám phá và sự rõ ràng của code base và mối quan hệ giữa các module ``` - ├── components/ - | ├── DeliveryCard - | ├── DeliveryChoice - | ├── RegionSelect - | ├── UserAvatar - ├── actions/ - | ├── delivery.js - | ├── region.js - | ├── user.js - ├── epics/{...} - ├── constants/{...} - ├── helpers/{...} ├── entities/ | ├── delivery/ + | | ├── ui/ # ~ components/ + | | | ├── card.js + | | | ├── choice.js + | | ├── model/ + | | | ├── actions.js + | | | ├── constants.js + | | | ├── epics.js + | | | ├── getters.js + | | | ├── selectors.js + | | ├── lib/ # ~ helpers | ├── region/ | ├── user/ ``` ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Article) Về Low Coupling và High Cohesion một cách rõ ràng](https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/) * [(Article) Low Coupling và High Cohesion. Law of Demeter](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9) --- # Routing WIP Bài viết đang trong quá trình hoàn thiện Để đẩy nhanh việc phát hành bài viết, bạn có thể: * 📢 Chia sẻ phản hồi của bạn [tại bài viết (comment/emoji-reaction)](https://github.com/feature-sliced/documentation/issues/169) * 💬 Thu thập tài liệu liên quan [về chủ đề từ chat](https://t.me/feature_sliced) * ⚒️ Đóng góp [bằng bất kỳ cách nào khác](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Tình huống[​](#tình-huống "Link trực tiếp đến heading") URL đến các trang được hardcode trong các layer bên dưới pages entities/post/card ``` ... ``` ## Vấn đề[​](#vấn-đề "Link trực tiếp đến heading") URL không được tập trung trong layer pages, nơi chúng thuộc về theo phạm vi trách nhiệm ## Nếu bỏ qua[​](#nếu-bỏ-qua "Link trực tiếp đến heading") Khi thay đổi URL, bạn sẽ phải nhớ rằng các URL này (và logic của URL/redirect) có thể nằm ở tất cả các layer trừ pages Và điều đó cũng có nghĩa là giờ đây ngay cả một product card đơn giản cũng đảm nhận một phần trách nhiệm từ pages, làm mờ logic của project ## Giải pháp[​](#giải-pháp "Link trực tiếp đến heading") Xác định cách làm việc với URL/redirect từ cấp độ page trở lên Chuyển xuống các layer bên dưới thông qua composition/props/factories ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [(Thread) Điều gì xảy ra nếu tôi "may" routing trong entities/features/widgets](https://t.me/feature_sliced/4389) * [(Thread) Tại sao nó làm mờ logic của routes chỉ trong pages](https://t.me/feature_sliced/3756) --- # Migration từ custom architecture Hướng dẫn này mô tả cách tiếp cận có thể hữu ích khi migration từ custom self-made architecture sang Feature-Sliced Design. Đây là cấu trúc folder của một custom architecture điển hình. Chúng tôi sẽ sử dụng nó làm ví dụ trong hướng dẫn này.
Nhấp vào mũi tên xanh để mở folder. 📁 src * 📁 actions * 📁 product * 📁 order * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 routes * 📁 products.jsx * 📄 products.\[id].jsx * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles * 📄 App.jsx * 📄 index.js ## Trước khi bạn bắt đầu[​](#before-you-start "Link trực tiếp đến heading") Câu hỏi quan trọng nhất để hỏi team của bạn khi cân nhắc chuyển sang Feature-Sliced Design là — *bạn có thực sự cần nó không?* Chúng tôi yêu Feature-Sliced Design, nhưng thậm chí chúng tôi cũng nhận ra rằng một số dự án hoàn toàn ổn mà không cần nó. Đây là một số lý do để cân nhắc thực hiện sự chuyển đổi: 1. Thành viên mới trong team phàn nàn rằng khó đạt đến mức độ hiệu quả 2. Thực hiện modifications ở một phần của code **thường** gây ra phần khác không liên quan bị hỏng 3. Thêm functionality mới khó khăn do số lượng lớn các thứ bạn cần suy nghĩ **Tránh chuyển sang FSD trái với ý muốn của các đồng đội**, ngay cả khi bạn là lead.
Trước tiên, hãy thuyết phục các đồng đội rằng lợi ích vượt trội so với chi phí migration và chi phí học một architecture mới thay vì architecture đã được thiết lập. Cũng cần nhớ rằng bất kỳ loại thay đổi kiến trúc nào cũng không ngay lập tức có thể quan sát được đối với quản lý. Hãy đảm bảo họ đồng ý với việc chuyển đổi trước khi bắt đầu và giải thích cho họ tại sao điều này có thể có lợi cho dự án. mẹo Nếu bạn cần giúp để thuyết phục project manager rằng FSD có lợi, hãy cân nhắc một số điểm này: 1. Migration sang FSD có thể diễn ra dần dần, vì vậy nó sẽ không dừng việc phát triển các tính năng mới 2. Một architecture tốt có thể giảm đáng kể thời gian mà một developer mới cần để trở nên hiệu quả 3. FSD là một architecture được tài liệu hóa, vì vậy team không phải liên tục dành thời gian để duy trì documentation riêng của họ *** Nếu bạn đã quyết định bắt đầu migration, thì điều đầu tiên bạn muốn làm là thiết lập một alias cho `📁 src`. Điều này sẽ hữu ích sau này khi tham chiếu đến các folder tầng cao. Chúng tôi sẽ coi `@` là alias cho `./src` trong phần còn lại của hướng dẫn này. ## Bước 1. Chia code theo pages[​](#divide-code-by-pages "Link trực tiếp đến heading") Hầu hết custom architectures đã có sự phân chia theo pages, dù logic nhỏ hay lớn. Nếu bạn đã có `📁 pages`, bạn có thể bỏ qua bước này. Nếu bạn chỉ có `📁 routes`, hãy tạo `📁 pages` và cố gắng di chuyển càng nhiều component code từ `📁 routes` càng tốt. Lý tưởng là bạn sẽ có một route nhỏ và một page lớn hơn. Khi đang di chuyển code, hãy tạo một folder cho mỗi page và thêm một index file: ghi chú Hiện tại, không sao nếu các page của bạn tham chiếu lẫn nhau. Bạn có thể giải quyết điều đó sau, nhưng hiện tại, hãy tập trung vào việc thiết lập một sự phân chia rõ ràng theo pages. Route file: src/routes/products.\[id].js ``` export { ProductPage as default } from "@/pages/product" ``` Page index file: src/pages/product/index.js ``` export { ProductPage } from "./ProductPage.jsx" ``` Page component file: src/pages/product/ProductPage.jsx ``` export function ProductPage(props) { return
; } ``` ## Bước 2. Tách mọi thứ khác ra khỏi pages[​](#separate-everything-else-from-pages "Link trực tiếp đến heading") Tầo folder `📁 src/shared` và di chuyển mọi thứ không import từ `📁 pages` hoặc `📁 routes` vào đó. Tạo folder `📁 src/app` và di chuyển mọi thứ có import pages hoặc routes vào đó, bao gồm cả chính các routes. Hãy nhớ rằng Shared layer không có slices, vì vậy không sao nếu các segment import lẫn nhau. Cuối cùng bạn sẽ có được cấu trúc tệp như thế này: 📁 src * 📁 app * 📁 routes * 📄 products.jsx * 📄 products.\[id].jsx * 📄 App.jsx * 📄 index.js * 📁 pages * 📁 product * 📁 ui * 📄 ProductPage.jsx * 📄 index.js * 📁 catalog * 📁 shared * 📁 actions * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles ## Bước 3. Giải quyết cross-imports giữa các pages[​](#tackle-cross-imports-between-pages "Link trực tiếp đến heading") Tìm tất cả các trường hợp mà một page đang import từ page khác và làm một trong hai điều sau: 1. Copy-paste code được import vào page phụ thuộc để loại bỏ dependency 2. Di chuyển code vào một segment thích hợp trong Shared: * nếu nó là một phần của UI kit, di chuyển vào `📁 shared/ui`; * nếu nó là một configuration constant, di chuyển vào `📁 shared/config`; * nếu nó là một backend interaction, di chuyển vào `📁 shared/api`. ghi chú **Copy-pasting không sai về mặt kiến trúc**, thực tế, đôi khi duplicate có thể chính xác hơn là abstract thành một reusable module mới. Lý do là đôi khi các phần shared của pages bắt đầu tách rời, và bạn không muốn các dependency cản trở bạn trong những trường hợp này. Tuy nhiên, vẫn còn ý nghĩa trong nguyên tắc DRY ("don't repeat yourself"), vì vậy hãy đảm bảo bạn không copy-paste business logic. Nếu không bạn sẽ cần phải nhớ sửa bug ở nhiều nơi cùng lúc. ## Bước 4. Giải nén Shared layer[​](#unpack-shared-layer "Link trực tiếp đến heading") Bạn có thể có rất nhiều thứ trong Shared layer ở bước này, và nói chung bạn muốn tránh điều đó. Lý do là Shared layer có thể là dependency cho bất kỳ layer nào khác trong codebase của bạn, vì vậy việc thay đổi code đó tự động dễ gây ra các hậu quả ngoài ý muốn hơn. Tìm tất cả các object chỉ được sử dụng trên một page và di chuyển nó vào slice của page đó. Và đúng rồi, *điều đó cũng áp dụng cho actions, reducers, và selectors*. Không có lợi ích gì khi nhóm tất cả actions lại với nhau, nhưng có lợi ích khi đặt các relevant actions gần với nơi sử dụng chúng. Cuối cùng bạn sẽ có được cấu trúc tệp như thế này: 📁 src * 📁 app (unchanged) * 📁 pages * 📁 product * 📁 actions * 📁 reducers * 📁 selectors * 📁 ui * 📄 Component.jsx * 📄 Container.jsx * 📄 ProductPage.jsx * 📄 index.js * 📁 catalog * 📁 shared (only objects that are reused) * 📁 actions * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles ## Bước 5. Tổ chức code theo mục đích kỹ thuật[​](#organize-by-technical-purpose "Link trực tiếp đến heading") Trong FSD, việc phân chia theo mục đích kỹ thuật được thực hiện với *segments*. Có một số loại phổ biến: * `ui` — mọi thứ liên quan đến hiển thị UI: UI components, date formatters, styles, v.v. * `api` — các tương tác backend: request functions, data types, mappers, v.v. * `model` — data model: schemas, interfaces, stores, và business logic. * `lib` — library code mà các module khác trên slice này cần. * `config` — các file cấu hình và feature flags. Bạn cũng có thể tạo các segment riêng của mình nếu cần. Hãy đảm bảo không tạo các segment nhóm code theo những gì nó là, như `components`, `actions`, `types`, `utils`. Thay vào đó, hãy nhóm code theo những gì nó dành cho. Tổ chức lại các pages của bạn để tách biệt code theo segments. Bạn nên đã có một `ui` segment, bây giờ là lúc tạo các segment khác, như `model` cho actions, reducers, và selectors của bạn, hoặc `api` cho thunks và mutations của bạn. Cũng tổ chức lại Shared layer để loại bỏ các folder này: * `📁 components`, `📁 containers` — phần lớn nó nên trở thành `📁 shared/ui`; * `📁 helpers`, `📁 utils` — nếu còn một số helpers được tái sử dụng, hãy nhóm chúng lại theo function, như dates hoặc type conversions, và di chuyển các nhóm này vào `📁 shared/lib`; * `📁 constants` — lại, nhóm theo function và di chuyển vào `📁 shared/config`. ## Các bước tùy chọn[​](#optional-steps "Link trực tiếp đến heading") ### Bước 6. Tạo entities/features từ các Redux slice được sử dụng trên nhiều pages[​](#form-entities-features-from-redux "Link trực tiếp đến heading") Thường thì các Redux slice được tái sử dụng này sẽ mô tả điều gì đó liên quan đến nghiệp vụ, ví dụ như products hoặc users, vì vậy chúng có thể được di chuyển vào Entities layer, một entity một folder. Nếu Redux slice liên quan đến một action mà người dùng của bạn muốn thực hiện trong app, như comments, thì bạn có thể di chuyển nó vào Features layer. Entities và features có ý định độc lập với nhau. Nếu business domain của bạn chứa các kết nối bẩm sinh giữa các entity, hãy tham khảo [hướng dẫn về business entities](/documentation/vi/docs/guides/examples/types.md#business-entities-and-their-cross-references) để có lời khuyên về cách tổ chức các kết nối này. Các API functions liên quan đến các slice này có thể giữ lại trong `📁 shared/api`. ### Bước 7. Refactor các modules của bạn[​](#refactor-your-modules "Link trực tiếp đến heading") Folder `📁 modules` thường được sử dụng cho business logic, vì vậy nó đã khá tương tự về bản chất với Features layer từ FSD. Một số module cũng có thể mô tả những khối lớn của UI, như app header. Trong trường hợp đó, bạn nên migration chúng vào Widgets layer. ### Bước 8. Tạo một UI foundation sạch trong `shared/ui`[​](#form-clean-ui-foundation "Link trực tiếp đến heading") `📁 shared/ui` lý tưởng nên chứa một tập hợp các UI elements không có business logic nào được encode trong chúng. Chúng cũng nên có tính tái sử dụng cao. Refactor các UI components đã từng nằm trong `📁 components` và `📁 containers` để tách riêng business logic. Di chuyển business logic đó lên các layer cao hơn. Nếu nó không được sử dụng ở quá nhiều nơi, bạn thậm chí có thể cân nhắc copy-paste. ## Xem thêm[​](#see-also "Link trực tiếp đến heading") * [(Bài nói tiếng Nga) Ilya Klimov — Крысиные бега бесконечного рефакторинга: как не дать техническому долгу убить мотивацию и продукт](https://youtu.be/aOiJ3k2UvO4) --- # Migration từ v1 sang v2 ## Tại sao v2?[​](#tại-sao-v2 "Link trực tiếp đến heading") Khái niệm gốc của **feature-slices** [đã được công bố](https://t.me/feature_slices) vào năm 2018. Kể từ đó, nhiều sự biến đổi của phương pháp luận đã diễn ra, nhưng đồng thời **[các nguyên tắc cơ bản đã được bảo tồn](https://feature-sliced.github.io/featureslices.dev/v1.0.html)**: * Sử dụng cấu trúc dự án frontend *được tiêu chuẩn hóa* * Chia nhỏ ứng dụng ngay từ đầu - theo *logic nghiệp vụ* * Sử dụng các *feature độc lập* để ngăn chặn side effects ngầm định và phụ thuộc vòng tròn * Sử dụng *Public API* với việc cấm "leo vào bên trong" của module Đồng thời, trong phiên bản trước của phương pháp luận, vẫn còn những **điểm yếu** mà: * Đôi khi dẫn đến boilerplate code * Đôi khi dẫn đến sự phức tạp quá mức của code base và các quy tắc không rõ ràng giữa các abstraction * Đôi khi dẫn đến các giải pháp kiến trúc ngầm định, ngăn cản việc kéo dự án lên và onboarding người mới Phiên bản mới của phương pháp luận ([v2](https://github.com/feature-sliced/documentation)) được thiết kế **để loại bỏ những thiếu sót này, đồng thời bảo tồn các ưu điểm hiện có** của cách tiếp cận này. Kể từ năm 2018, [cũng đã phát triển](https://github.com/kof/feature-driven-architecture/issues) một phương pháp luận tương tự khác - [**feature-driven**](https://github.com/feature-sliced/documentation/tree/rc/feature-driven), được công bố lần đầu bởi [Oleg Isonen](https://github.com/kof). Sau khi hợp nhất hai cách tiếp cận, chúng tôi đã **cải thiện và tinh chỉnh các thực tiễn hiện có** - hướng tới sự linh hoạt, rõ ràng và hiệu quả hơn trong ứng dụng. > Kết quả là, điều này thậm chí đã ảnh hưởng đến tên của phương pháp luận - *"feature-slice**d**"* ## Tại sao việc migration dự án sang v2 có ý nghĩa?[​](#tại-sao-việc-migration-dự-án-sang-v2-có-ý-nghĩa "Link trực tiếp đến heading") > `WIP:` Phiên bản hiện tại của phương pháp luận đang được phát triển và một số chi tiết *có thể thay đổi* #### 🔍 Kiến trúc minh bạch và đơn giản hơn[​](#-kiến-trúc-minh-bạch-và-đơn-giản-hơn "Link trực tiếp đến heading") Phương pháp luận (v2) cung cấp **các abstraction trực quan hơn và phổ biến hơn cũng như các cách phân tách logic giữa các developer.** Tất cả điều này có tác động cực kỳ tích cực đến việc thu hút người mới, cũng như nghiên cứu trạng thái hiện tại của dự án, và phân phối logic nghiệp vụ của ứng dụng. #### 📦 Tính modular linh hoạt và trung thực hơn[​](#-tính-modular-linh-hoạt-và-trung-thực-hơn "Link trực tiếp đến heading") Phương pháp luận (v2) cho phép **phân phối logic theo cách linh hoạt hơn:** * Với khả năng refactor các phần riêng biệt từ đầu * Với khả năng dựa vào cùng các abstraction, nhưng không có sự đan xen phụ thuộc không cần thiết * Với các yêu cầu đơn giản hơn cho vị trí của module mới *(layer => slice => segment)* #### 🚀 Nhiều đặc tả, kế hoạch, cộng đồng hơn[​](#-nhiều-đặc-tả-kế-hoạch-cộng-đồng-hơn "Link trực tiếp đến heading") Hiện tại, `core-team` đang tích cực làm việc trên phiên bản mới nhất (v2) của phương pháp luận Vì vậy đối với nó: * sẽ có nhiều case / vấn đề được mô tả hơn * sẽ có nhiều hướng dẫn về ứng dụng hơn * sẽ có nhiều ví dụ thực tế hơn * nói chung, sẽ có nhiều tài liệu hơn để onboarding người mới và nghiên cứu các khái niệm của phương pháp luận * toolkit sẽ được phát triển trong tương lai để tuân thủ các khái niệm và quy ước về kiến trúc > Tất nhiên, cũng sẽ có hỗ trợ người dùng cho phiên bản đầu tiên - nhưng phiên bản mới nhất vẫn là ưu tiên của chúng tôi > > Trong tương lai, với các bản cập nhật major tiếp theo, bạn vẫn sẽ có quyền truy cập vào phiên bản hiện tại (v2) của phương pháp luận, **không có rủi ro cho team và dự án của bạn** ## Changelog[​](#changelog "Link trực tiếp đến heading") ### `BREAKING` Layers[​](#breaking-layers "Link trực tiếp đến heading") Bây giờ phương pháp luận giả định việc phân bổ rõ ràng các layer ở tầng cao nhất * `/app` > `/processes` > **`/pages`** > **`/features`** > `/entities` > `/shared` * *Tức là, không phải mọi thứ bây giờ đều được coi là features/pages* * Cách tiếp cận này cho phép bạn [đặt quy tắc rõ ràng cho các layer](https://t.me/atomicdesign/18708): * **Càng cao layer** của module được đặt, càng nhiều **context** nó có *(nói cách khác - mỗi module của layer - chỉ có thể import các module của các layer bên dưới, nhưng không phải cao hơn)* * **Càng thấp layer** của module được đặt, càng nhiều **nguy hiểm và trách nhiệm** khi thực hiện thay đổi *(vì thường là các layer bên dưới được sử dụng nhiều hơn)* ### `BREAKING` Shared[​](#breaking-shared "Link trực tiếp đến heading") Các infrastructure abstraction `/ui`, `/lib`, `/api`, trước đây nằm trong src root của dự án, bây giờ được tách biệt bởi thư mục riêng biệt `/src/shared` * `shared/ui` - Vẫn là uikit tổng quát giống như cũ của ứng dụng (tùy chọn) * *Đồng thời, không ai cấm sử dụng `Atomic Design` ở đây như trước* * `shared/lib` - Tập hợp các thư viện phụ trợ để triển khai logic * *Vẫn - không có dump của helpers* * `shared/api` - Điểm vào chung để truy cập API * *Cũng có thể đăng ký cục bộ trong mỗi feature / page - nhưng không được khuyến khích* * Như trước - không nên có ràng buộc rõ ràng với business logic trong `shared` * *Nếu cần thiết, bạn cần đưa mối quan hệ này lên tầng `entities` hoặc thậm chí cao hơn* ### `NEW` Entities, Processes[​](#new-entities-processes "Link trực tiếp đến heading") Trong v2 **, các abstraction mới khác** đã được thêm vào để loại bỏ các vấn đề về độ phức tạp logic và coupling cao. * `/entities` - layer **business entities** chứa các slice có liên quan trực tiếp đến các business model hoặc synthetic entities chỉ cần thiết trên frontend * *Ví dụ: `user`, `i18n`, `order`, `blog`* * `/processes` - layer **business processes**, xuyên suốt app * **Layer này là tùy chọn**, thường được khuyến khích sử dụng khi *logic phát triển và bắt đầu mờ nhạt trong nhiều page* * *Ví dụ: `payment`, `auth`, `quick-tour`* ### `BREAKING` Abstractions & Naming[​](#breaking-abstractions--naming "Link trực tiếp đến heading") Bây giờ các abstraction cụ thể và [khuyến nghị rõ ràng cho việc đặt tên chúng](/documentation/vi/docs/about/understanding/naming.md) đã được định nghĩa #### Layers[​](#layers "Link trực tiếp đến heading") * `/app` — **layer khởi tạo ứng dụng** * *Các phiên bản trước: `app`, `core`,`init`, `src/index` (và điều này cũng xảy ra)* * `/processes` — [**layer business process**](https://github.com/feature-sliced/documentation/discussions/20) * *Các phiên bản trước: `processes`, `flows`, `workflows`* * `/pages` — **layer page ứng dụng** * *Các phiên bản trước: `pages`, `screens`, `views`, `layouts`, `components`, `containers`* * `/features` — [**layer các phần functionality**](https://github.com/feature-sliced/documentation/discussions/23) * *Các phiên bản trước: `features`, `components`, `containers`* * `/entities` — [**layer business entity**](https://github.com/feature-sliced/documentation/discussions/18#discussioncomment-422649) * *Các phiên bản trước: `entities`, `models`, `shared`* * `/shared` — [**layer của infrastructure code tái sử dụng**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453020) 🔥 * *Các phiên bản trước: `shared`, `common`, `lib`* #### Segments[​](#segments "Link trực tiếp đến heading") * `/ui` — [**UI segment**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453132) 🔥 * *Các phiên bản trước: `ui`, `components`, `view`* * `/model` — [**BL-segment**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-472645) 🔥 * *Các phiên bản trước: `model`, `store`, `state`, `services`, `controller`* * `/lib` — segment **của auxiliary code** * *Các phiên bản trước: `lib`, `libs`, `utils`, `helpers`* * `/api` — [**API segment**](https://github.com/feature-sliced/documentation/discussions/66) * *Các phiên bản trước: `api`, `service`, `requests`, `queries`* * `/config` — **segment cấu hình ứng dụng** * *Các phiên bản trước: `config`, `env`, `get-env`* ### `REFINED` Low coupling[​](#refined-low-coupling "Link trực tiếp đến heading") Bây giờ việc [tuân thủ nguyên tắc low coupling](/documentation/vi/docs/reference/slices-segments.md#zero-coupling-high-cohesion) giữa các module dễ dàng hơn nhiều, nhờ vào các layer mới. *Đồng thời, vẫn được khuyến khích tránh càng nhiều càng tốt các trường hợp khi cực kỳ khó để "uncouple" các module* ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [Ghi chú từ báo cáo "React SPB Meetup #1"](https://t.me/feature_slices) * [React Berlin Talk - Oleg Isonen "Feature Driven Architecture"](https://www.youtube.com/watch?v=BWAeYuWFHhs) * [So sánh với v1 (community-chat)](https://t.me/feature_sliced/493) * [Ý tưởng mới v2 với giải thích (atomicdesign-chat)](https://t.me/atomicdesign/18708) * [Thảo luận về các abstraction và naming cho phiên bản mới của phương pháp luận (v2)](https://github.com/feature-sliced/documentation/discussions/31) --- # Migration từ v2.0 sang v2.1 Thay đổi chính trong v2.1 là mental model mới để phân tách interface — pages first. Trong v2.0, FSD khuyến nghị xác định entities và features trong interface của bạn, cân nhắc thậm chí những bit nhỏ nhất của entity representation và tính tương tác để phân tách. Sau đó bạn sẽ xây dựng widgets và pages từ entities và features. Trong model phân tách này, phần lớn logic nằm trong entities và features, và pages chỉ là các compositional layer không có nhiều ý nghĩa riêng biệt. Trong v2.1, chúng tôi khuyến nghị bắt đầu với pages, và có thể thậm chí dừng lại ở đó. Hầu hết mọi người đã biết cách tách app thành các page riêng biệt, và pages cũng là điểm khởi đầu phổ biến khi cố gắng tìm một component trong codebase. Trong model phân tách mới này, bạn giữ phần lớn UI và logic trong mỗi page riêng biệt, duy trì một foundation có thể tái sử dụng trong Shared. Nếu phát sinh nhu cầu tái sử dụng business logic trên nhiều page, bạn có thể di chuyển nó xuống layer bên dưới. Một bổ sung khác cho Feature-Sliced Design là việc tiêu chuẩn hóa cross-imports giữa các entity với ký hiệu `@x`. ## Cách migration[​](#how-to-migrate "Link trực tiếp đến heading") Không có breaking changes trong v2.1, điều này có nghĩa là một dự án được viết với FSD v2.0 cũng là một dự án hợp lệ trong FSD v2.1. Tuy nhiên, chúng tôi tin rằng mental model mới có lợi hơn cho các team và đặc biệt là onboarding các developer mới, vì vậy chúng tôi khuyến nghị thực hiện các điều chỉnh nhỏ đối với việc phân tách của bạn. ### Merge slices[​](#merge-slices "Link trực tiếp đến heading") Một cách đơn giản để bắt đầu là chạy linter của chúng tôi, [Steiger](https://github.com/feature-sliced/steiger), trên dự án. Steiger được xây dựng với mental model mới, và các rule hữu ích nhất sẽ là: * [`insignificant-slice`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice) — nếu một entity hoặc feature chỉ được sử dụng trong một page, rule này sẽ đề xuất merge entity hoặc feature đó hoàn toàn vào page. * [`excessive-slicing`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/excessive-slicing) — nếu một layer có quá nhiều slices, thường là dấu hiệu cho thấy việc phân tách quá chi tiết. Rule này sẽ đề xuất merge hoặc nhóm một số slices để hỗ trợ navigation dự án. ``` npx steiger src ``` Điều này sẽ giúp bạn xác định những slice chỉ được sử dụng một lần, để bạn có thể cân nhắc lại xem chúng có thực sự cần thiết không. Trong những cân nhắc như vậy, hãy nhớ rằng một layer tạo thành một loại global namespace cho tất cả các slice bên trong nó. Giống như bản sẽ không làm ô nhiễm global namespace với các biến chỉ được sử dụng một lần, bạn nên coi một vị trí trong namespace của layer là có giá trị, được sử dụng một cách tiết kiệm. ### Tiêu chuẩn hóa cross-imports[​](#tiêu-chuẩn-hóa-cross-imports "Link trực tiếp đến heading") Nếu trước đây bạn đã có cross-imports giữa trong dự án của mình (chúng tôi không phán xét!), bây giờ bạn có thể tận dụng một ký hiệu mới cho cross-importing trong Feature-Sliced Design — ký hiệu `@x`. Nó trông như thế này: entities/B/some/file.ts ``` import type { EntityA } from "entities/A/@x/B"; ``` Để biết thêm chi tiết, hãy xem phần [Public API for cross-imports](/documentation/vi/docs/reference/public-api.md#public-api-for-cross-imports) trong reference. --- # Sử dụng với Electron Các ứng dụng Electron có kiến trúc đặc biệt gồm nhiều process với các trách nhiệm khác nhau. Việc áp dụng FSD trong bối cảnh như vậy yêu cầu phải thích nghi cấu trúc với các đặc điểm của Electron. ``` └── src ├── app # Common app layer │ ├── main # Main process │ │ └── index.ts # Main process entry point │ ├── preload # Preload script and Context Bridge │ │ └── index.ts # Preload entry point │ └── renderer # Renderer process │ └── index.html # Renderer process entry point ├── main │ ├── features │ │ └── user │ │ └── ipc │ │ ├── get-user.ts │ │ └── send-user.ts │ ├── entities │ └── shared ├── renderer │ ├── pages │ │ ├── settings │ │ │ ├── ipc │ │ │ │ ├── get-user.ts │ │ │ │ └── save-user.ts │ │ │ ├── ui │ │ │ │ └── user.tsx │ │ │ └── index.ts │ │ └── home │ │ ├── ui │ │ │ └── home.tsx │ │ └── index.ts │ ├── widgets │ ├── features │ ├── entities │ └── shared └── shared # Common code between main and renderer └── ipc # IPC description (event names, contracts) ``` ## Quy tắc Public API[​](#quy-tắc-public-api "Link trực tiếp đến heading") Mỗi process phải có public API riêng của nó. Ví dụ, bạn không thể import các module từ `main` vào `renderer`. Chỉ có thư mục `src/shared` là public cho cả hai process. Nó cũng cần thiết để mô tả các hợp đồng cho tương tác giữa các process. ## Các thay đổi bổ sung cho cấu trúc chuẩn[​](#các-thay-đổi-bổ-sung-cho-cấu-trúc-chuẩn "Link trực tiếp đến heading") Được đề xuất sử dụng segment `ipc` mới, nơi diễn ra tương tác giữa các process. Các layer `pages` và `widgets`, dựa trên tên gọi của chúng, không nên có mặt trong `src/main`. Bạn có thể sử dụng `features`, `entities` và `shared`. Layer `app` trong `src` chứa các điểm đầu vào cho `main` và `renderer`, cũng như IPC. Không mong muốn các segment trong layer `app` có điểm giao nhau ## Ví dụ về tương tác[​](#ví-dụ-về-tương-tác "Link trực tiếp đến heading") src/shared/ipc/channels.ts ``` export const CHANNELS = { GET_USER_DATA: 'GET_USER_DATA', SAVE_USER: 'SAVE_USER', } as const; export type TChannelKeys = keyof typeof CHANNELS; ``` src/shared/ipc/events.ts ``` import { CHANNELS } from './channels'; export interface IEvents { [CHANNELS.GET_USER_DATA]: { args: void, response?: { name: string; email: string; }; }; [CHANNELS.SAVE_USER]: { args: { name: string; }; response: void; }; } ``` src/shared/ipc/preload.ts ``` import { CHANNELS } from './channels'; import type { IEvents } from './events'; type TOptionalArgs = T extends void ? [] : [args: T]; export type TElectronAPI = { [K in keyof typeof CHANNELS]: (...args: TOptionalArgs) => IEvents[typeof CHANNELS[K]]['response']; }; ``` src/app/preload/index.ts ``` import { contextBridge, ipcRenderer } from 'electron'; import { CHANNELS, type TElectronAPI } from 'shared/ipc'; const API: TElectronAPI = { [CHANNELS.GET_USER_DATA]: () => ipcRenderer.sendSync(CHANNELS.GET_USER_DATA), [CHANNELS.SAVE_USER]: args => ipcRenderer.invoke(CHANNELS.SAVE_USER, args), } as const; contextBridge.exposeInMainWorld('electron', API); ``` src/main/features/user/ipc/send-user.ts ``` import { ipcMain } from 'electron'; import { CHANNELS } from 'shared/ipc'; export const sendUser = () => { ipcMain.on(CHANNELS.GET_USER_DATA, ev => { ev.returnValue = { name: 'John Doe', email: 'john.doe@example.com', }; }); }; ``` src/renderer/pages/user-settings/ipc/get-user.ts ``` import { CHANNELS } from 'shared/ipc'; export const getUser = () => { const user = window.electron[CHANNELS.GET_USER_DATA](); return user ?? { name: 'John Donte', email: 'john.donte@example.com' }; }; ``` ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [Process Model Documentation](https://www.electronjs.org/docs/latest/tutorial/process-model) * [Context Isolation Documentation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) * [Inter-Process Communication Documentation](https://www.electronjs.org/docs/latest/tutorial/ipc) * [Example](https://github.com/feature-sliced/examples/tree/master/examples/electron) --- # Sử dụng với Next.js FSD tương thích với Next.js trong cả phiên bản App Router và Pages Router nếu bạn giải quyết được xung đột chính — thư mục `app` và `pages`. ## App Router[​](#app-router "Link trực tiếp đến heading") ### Xung đột giữa FSD và Next.js trong layer `app`[​](#conflict-between-fsd-and-nextjs-in-the-app-layer "Link trực tiếp đến heading") Next.js đề xuất sử dụng thư mục `app` để định nghĩa các route của ứng dụng. Nó mong đợi các file trong thư mục `app` tương ứng với các pathname. Cơ chế routing này **không phù hợp** với khái niệm FSD, vì không thể duy trì cấu trúc slice phẳng. Giải pháp là di chuyển thư mục `app` của Next.js vào thư mục gốc của dự án và import các pages FSD từ `src`, nơi chứa các layer FSD, vào thư mục `app` của Next.js. Bạn cũng cần thêm thư mục `pages` vào thư mục gốc của dự án, nếu không Next.js sẽ cố gắng sử dụng `src/pages` như Pages Router ngay cả khi bạn sử dụng App Router, điều này sẽ làm hỏng quá trình build. Cũng nên đặt file `README.md` bên trong thư mục `pages` gốc này để mô tả tại sao nó cần thiết, mặc dù nó trống. ``` ├── app # App folder (Next.js) │ ├── api │ │ └── get-example │ │ └── route.ts │ └── example │ └── page.tsx ├── pages # Empty pages folder (Next.js) │ └── README.md └── src ├── app │ └── api-routes # API routes ├── pages │ └── example │ ├── index.ts │ └── ui │ └── example.tsx ├── widgets ├── features ├── entities └── shared ``` Ví dụ về việc re-export một page từ `src/pages` trong `app` của Next.js: app/example/page.tsx ``` export { ExamplePage as default, metadata } from '@/pages/example'; ``` ### Middleware[​](#middleware "Link trực tiếp đến heading") Nếu bạn sử dụng middleware trong dự án, nó phải được đặt ở thư mục gốc của dự án cùng với thư mục `app` và `pages` của Next.js. ### Instrumentation[​](#instrumentation "Link trực tiếp đến heading") File `instrumentation.js` cho phép bạn giám sát hiệu suất và hành vi của ứng dụng. Nếu bạn sử dụng nó, nó phải được đặt ở thư mục gốc của dự án, tương tự như `middleware.js`. ## Pages Router[​](#pages-router "Link trực tiếp đến heading") ### Xung đột giữa FSD và Next.js trong layer `pages`[​](#conflict-between-fsd-and-nextjs-in-the-pages-layer "Link trực tiếp đến heading") Các route nên được đặt trong thư mục `pages` ở thư mục gốc của dự án, tương tự như thư mục `app` cho App Router. Cấu trúc bên trong `src` nơi các thư mục layer được đặt vẫn không thay đổi. ``` ├── pages # Pages folder (Next.js) │ ├── _app.tsx │ ├── api │ │ └── example.ts # API route re-export │ └── example │ └── index.tsx └── src ├── app │ ├── custom-app │ │ └── custom-app.tsx # Custom App component │ └── api-routes │ └── get-example-data.ts # API route ├── pages │ └── example │ ├── index.ts │ └── ui │ └── example.tsx ├── widgets ├── features ├── entities └── shared ``` Ví dụ về việc re-export một page từ `src/pages` trong `pages` của Next.js: pages/example/index.tsx ``` export { Example as default } from '@/pages/example'; ``` ### Custom `_app` component[​](#custom-_app-component "Link trực tiếp đến heading") Bạn có thể đặt Custom App component của mình trong `src/app/_app` hoặc `src/app/custom-app`: src/app/custom-app/custom-app.tsx ``` import type { AppProps } from 'next/app'; export const MyApp = ({ Component, pageProps }: AppProps) => { return ( <>

My Custom App component

); }; ``` pages/\_app.tsx ``` export { App as default } from '@/app/custom-app'; ``` ## Route Handlers (API routes)[​](#route-handlers-api-routes "Link trực tiếp đến heading") Sử dụng segment `api-routes` trong layer `app` để làm việc với Route Handlers. Hãy chú ý khi viết code backend trong cấu trúc FSD — FSD chủ yếu dành cho frontend, nghĩa là đó là điều mà mọi người sẽ mong đợi tìm thấy. Nếu bạn cần nhiều endpoint, hãy cân nhắc tách chúng thành một package khác trong một monorepo. * App Router * Pages Router src/app/api-routes/get-example-data.ts ``` import { getExamplesList } from '@/shared/db'; export const getExampleData = () => { try { const examplesList = getExamplesList(); return Response.json({ examplesList }); } catch { return Response.json(null, { status: 500, statusText: 'Ouch, something went wrong', }); } }; ``` app/api/example/route.ts ``` export { getExampleData as GET } from '@/app/api-routes'; ``` src/app/api-routes/get-example-data.ts ``` import type { NextApiRequest, NextApiResponse } from 'next'; const config = { api: { bodyParser: { sizeLimit: '1mb', }, }, maxDuration: 5, }; const handler = (req: NextApiRequest, res: NextApiResponse) => { res.status(200).json({ message: 'Hello from FSD' }); }; export const getExampleData = { config, handler } as const; ``` src/app/api-routes/index.ts ``` export { getExampleData } from './get-example-data'; ``` app/api/example.ts ``` import { getExampleData } from '@/app/api-routes'; export const config = getExampleData.config; export default getExampleData.handler; ``` ## Các đề xuất bổ sung[​](#additional-recommendations "Link trực tiếp đến heading") * Sử dụng segment `db` trong layer `shared` để mô tả các database query và việc sử dụng chúng ở các layer cao hơn. * Logic caching và revalidating queries tốt nhất nên được giữ cùng chỗ với các query. ## Xem thêm[​](#see-also "Link trực tiếp đến heading") * [Next.js Project Structure](https://nextjs.org/docs/app/getting-started/project-structure) * [Next.js Page Layouts](https://nextjs.org/docs/app/getting-started/layouts-and-pages) --- # Sử dụng với NuxtJS Có thể triển khai FSD trong dự án NuxtJS, nhưng xảy ra xung đột do sự khác biệt giữa yêu cầu cấu trúc dự án của NuxtJS và các nguyên tắc FSD: * Ban đầu, NuxtJS cung cấp cấu trúc file dự án không có thư mục `src`, tức là ở thư mục gốc của dự án. * File routing nằm trong thư mục `pages`, trong khi ở FSD thư mục này được dành riêng cho cấu trúc slice phẳng. ## Thêm alias cho thư mục `src`[​](#thêm-alias-cho-thư-mục-src "Link trực tiếp đến heading") Thêm đối tượng `alias` vào config của bạn: nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // Not FSD related, enabled at project startup alias: { "@": '../src' }, }) ``` ## Chọn cách cấu hình router[​](#chọn-cách-cấu-hình-router "Link trực tiếp đến heading") Trong NuxtJS, có hai cách để tùy chỉnh routing - sử dụng config và sử dụng cấu trúc file. Trong trường hợp file-based routing, bạn sẽ tạo các file index.vue trong các thư mục bên trong thư mục app/routes, và trong trường hợp configure, bạn sẽ cấu hình các router trong file `router.options.ts`. ### Routing sử dụng config[​](#routing-sử-dụng-config "Link trực tiếp đến heading") Trong layer `app`, tạo file `router.options.ts` và export một config object từ nó: app/router.options.ts ``` import type { RouterConfig } from '@nuxt/schema'; export default { routes: (_routes) => [], }; ``` Để thêm page `Home` vào dự án của bạn, bạn cần thực hiện các bước sau: * Thêm một page slice bên trong layer `pages` * Thêm route phù hợp vào config `app/router.config.ts` Để tạo một page slice, hãy sử dụng [CLI](https://github.com/feature-sliced/cli): ``` fsd pages home ``` Tạo file `home-page.vue` bên trong segment ui, truy cập nó bằng Public API src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` Vậy, cấu trúc file sẽ trông như thế này: ``` |── src │ ├── app │ │ ├── router.config.ts │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.vue │ │ │ ├── index.ts ``` Cuối cùng, hãy thêm một route vào config: app/router.config.ts ``` import type { RouterConfig } from '@nuxt/schema' export default { routes: (_routes) => [ { name: 'home', path: '/', component: () => import('@/pages/home.vue').then(r => r.default || r) } ], } ``` ### File Routing[​](#file-routing "Link trực tiếp đến heading") Trước tiên, tạo thư mục `src` trong thư mục gốc của dự án của bạn, và tạo các layer app và pages bên trong thư mục này cùng với thư mục routes bên trong layer app. Vậy, cấu trúc file của bạn nên trông như thế này: ``` ├── src │ ├── app │ │ ├── routes │ ├── pages # Pages folder, related to FSD ``` Để NuxtJS sử dụng thư mục routes bên trong layer `app` cho file routing, bạn cần sửa đổi `nuxt.config.ts` như sau: nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // Not FSD related, enabled at project startup alias: { "@": '../src' }, dir: { pages: './src/app/routes' } }) ``` Bây giờ, bạn có thể tạo các route cho pages trong `app` và kết nối các page từ `pages` với chúng. Ví dụ, để thêm page `Home` vào dự án của bạn, bạn cần thực hiện các bước sau: * Thêm một page slice bên trong layer `pages` * Thêm route tương ứng bên trong layer `app` * Kết nối page từ slice với route Để tạo một page slice, hãy sử dụng [CLI](https://github.com/feature-sliced/cli): ``` fsd pages home ``` Tạo file `home-page.vue` bên trong segment ui, truy cập nó bằng Public API src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` Tạo một route cho page này bên trong layer `app`: ``` ├── src │ ├── app │ │ ├── routes │ │ │ ├── index.vue │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.vue │ │ │ ├── index.ts ``` Thêm page component của bạn vào bên trong file `index.vue`: src/app/routes/index.vue ``` ``` ## Làm gì với `layouts`?[​](#làm-gì-với-layouts "Link trực tiếp đến heading") Bạn có thể đặt layouts bên trong layer `app`, để làm điều này bạn cần sửa đổi config như sau: nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // Not related to FSD, enabled at project startup alias: { "@": '../src' }, dir: { pages: './src/app/routes', layouts: './src/app/layouts' } }) ``` ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [Documentation on changing directory config in NuxtJS](https://nuxt.com/docs/api/nuxt-config#dir) * [Documentation on changing router config in NuxtJS](https://nuxt.com/docs/guide/recipes/custom-routing#router-config) * [Documentation on changing aliases in NuxtJS](https://nuxt.com/docs/api/nuxt-config#alias) --- # Sử dụng với React Query ## Vấn đề "nên đặt các key ở đâu"[​](#vấn-đề-nên-đặt-các-key-ở-đâu "Link trực tiếp đến heading") ### Giải pháp — phân chia theo entities[​](#giải-pháp--phân-chia-theo-entities "Link trực tiếp đến heading") Nếu dự án đã có sự phân chia thành các entity, và mỗi request tương ứng với một entity duy nhất, cách phân chia thuần khiết nhất sẽ là theo entity. Trong trường hợp này, chúng tôi đề xuất sử dụng cấu trúc sau: ``` └── src/ # ├── app/ # | ... # ├── pages/ # | ... # ├── entities/ # | ├── {entity}/ # | ... └── api/ # | ├── `{entity}.query` # Query-factory nơi chứa các key và function | ├── `get-{entity}` # Function lấy entity | ├── `create-{entity}` # Function tạo entity | ├── `update-{entity}` # Function cập nhật entity | ├── `delete-{entity}` # Function xóa entity | ... # | # ├── features/ # | ... # ├── widgets/ # | ... # └── shared/ # ... # ``` Nếu có kết nối giữa các entity (ví dụ, entity Country có field-list của các entity City), thì bạn có thể sử dụng [public API for cross-imports](/documentation/vi/docs/reference/public-api.md#public-api-for-cross-imports) hoặc cân nhắc giải pháp thay thế bên dưới. ### Giải pháp thay thế — giữ trong shared[​](#giải-pháp-thay-thế--giữ-trong-shared "Link trực tiếp đến heading") Trong các trường hợp mà việc tách biệt entity không phù hợp, có thể cân nhắc cấu trúc sau: ``` └── src/ # ... # └── shared/ # ├── api/ # ... ├── `queries` # Query-factories | ├── `document.ts` # | ├── `background-jobs.ts` # | ... # └── index.ts # ``` Sau đó trong `@/shared/api/index.ts`: @/shared/api/index.ts ``` export { documentQueries } from "./queries/document"; ``` ## Vấn đề "Đặt mutations ở đâu?"[​](#vấn-đề-đặt-mutations-ở-đâu "Link trực tiếp đến heading") Không nên trộn lẫn mutations với queries. Có hai lựa chọn: ### 1. Định nghĩa một custom hook trong segment `api` gần nơi sử dụng[​](#1-định-nghĩa-một-custom-hook-trong-segment-api-gần-nơi-sử-dụng "Link trực tiếp đến heading") @/features/update-post/api/use-update-title.ts ``` export const useUpdateTitle = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, newTitle }) => apiClient .patch(`/posts/${id}`, { title: newTitle }) .then((data) => console.log(data)), onSuccess: (newPost) => { queryClient.setQueryData(postsQueries.ids(id), newPost); }, }); }; ``` ### 2. Định nghĩa một mutation function ở nơi khác (Shared hoặc Entities) và sử dụng `useMutation` trực tiếp trong component[​](#2-định-nghĩa-một-mutation-function-ở-nơi-khác-shared-hoặc-entities-và-sử-dụng-usemutation-trực-tiếp-trong-component "Link trực tiếp đến heading") ``` const { mutateAsync, isPending } = useMutation({ mutationFn: postApi.createPost, }); ``` @/pages/post-create/ui/post-create-page.tsx ``` export const CreatePost = () => { const { classes } = useStyles(); const [title, setTitle] = useState(""); const { mutate, isPending } = useMutation({ mutationFn: postApi.createPost, }); const handleChange = (e: ChangeEvent) => setTitle(e.target.value); const handleSubmit = (e: FormEvent) => { e.preventDefault(); mutate({ title, userId: DEFAULT_USER_ID }); }; return (
Create ); }; ``` ## Tổ chức các request[​](#tổ-chức-các-request "Link trực tiếp đến heading") ### Query factory[​](#query-factory "Link trực tiếp đến heading") Một query factory là một object mà các giá trị key là các function trả về một danh sách các query key. Đây là cách sử dụng nó: ``` const keyFactory = { all: () => ["entity"], lists: () => [...postQueries.all(), "list"], }; ``` thông tin `queryOptions` là một utility tích hợp sẵn trong react-query\@v5 (tùy chọn) ``` queryOptions({ queryKey, ...options, }); ``` Để có type safety tốt hơn, tương thích với các phiên bản tương lai của react-query, và dễ dàng truy cập các function và query key, bạn có thể sử dụng function queryOptions tích hợp sẵn từ "@tanstack/react-query" [(Chi tiết thêm tại đây)](https://tkdodo.eu/blog/the-query-options-api#queryoptions). ### 1. Tạo một Query Factory[​](#1-tạo-một-query-factory "Link trực tiếp đến heading") @/entities/post/api/post.queries.ts ``` import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; import { getDetailPost } from "./get-detail-post"; import { PostDetailQuery } from "./query/post.query"; export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }), details: () => [...postQueries.all(), "detail"], detail: (query?: PostDetailQuery) => queryOptions({ queryKey: [...postQueries.details(), query?.id], queryFn: () => getDetailPost({ id: query?.id }), staleTime: 5000, }), }; ``` ### 2. Sử dụng Query Factory trong code ứng dụng[​](#2-sử-dụng-query-factory-trong-code-ứng-dụng "Link trực tiếp đến heading") ``` import { useParams } from "react-router-dom"; import { postApi } from "@/entities/post"; import { useQuery } from "@tanstack/react-query"; type Params = { postId: string; }; export const PostPage = () => { const { postId } = useParams(); const id = parseInt(postId || ""); const { data: post, error, isLoading, isError, } = useQuery(postApi.postQueries.detail({ id })); if (isLoading) { return
Loading...
; } if (isError || !post) { return <>{error?.message}; } return (

Post id: {post.id}

{post.title}

{post.body}

Owner: {post.userId}
); }; ``` ### Lợi ích của việc sử dụng Query Factory[​](#lợi-ích-của-việc-sử-dụng-query-factory "Link trực tiếp đến heading") * **Cấu trúc hóa request:** Factory cho phép bạn tổ chức tất cả API request tại một nơi, giúp code dễ đọc và bảo trì hơn. * **Truy cập thuận tiện vào query và key:** Factory cung cấp các method thuận tiện để truy cập các loại query khác nhau và key của chúng. * **Khả năng refetch query:** Factory cho phép refetch dễ dàng mà không cần thay đổi query key ở các phần khác nhau của ứng dụng. ## Phân trang[​](#phân-trang "Link trực tiếp đến heading") Trong phần này, chúng ta sẽ xem xét ví dụ về function `getPosts`, thực hiện API request để lấy các post entity sử dụng phân trang. ### 1. Tạo function `getPosts`[​](#1-tạo-function-getposts "Link trực tiếp đến heading") Function getPosts nằm trong file `get-posts.ts`, được đặt trong segment `api` @/pages/post-feed/api/get-posts.ts ``` import { apiClient } from "@/shared/api/base"; import { PostWithPaginationDto } from "./dto/post-with-pagination.dto"; import { PostQuery } from "./query/post.query"; import { mapPost } from "./mapper/map-post"; import { PostWithPagination } from "../model/post-with-pagination"; const calculatePostPage = (totalCount: number, limit: number) => Math.floor(totalCount / limit); export const getPosts = async ( page: number, limit: number, ): Promise => { const skip = page * limit; const query: PostQuery = { skip, limit }; const result = await apiClient.get("/posts", query); return { posts: result.posts.map((post) => mapPost(post)), limit: result.limit, skip: result.skip, total: result.total, totalPages: calculatePostPage(result.total, limit), }; }; ``` ### 2. Query factory cho phân trang[​](#2-query-factory-cho-phân-trang "Link trực tiếp đến heading") Query factory `postQueries` định nghĩa các query option khác nhau để làm việc với post, bao gồm request danh sách post với page và limit cụ thể. ``` import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }), }; ``` ### 3. Sử dụng trong code ứng dụng[​](#3-sử-dụng-trong-code-ứng-dụng "Link trực tiếp đến heading") @/pages/home/ui/index.tsx ``` export const HomePage = () => { const itemsOnScreen = DEFAULT_ITEMS_ON_SCREEN; const [page, setPage] = usePageParam(DEFAULT_PAGE); const { data, isFetching, isLoading } = useQuery( postApi.postQueries.list(page, itemsOnScreen), ); return ( <> setPage(page)} page={page} count={data?.totalPages} variant="outlined" color="primary" /> ); }; ``` ghi chú Ví dụ đã được đơn giản hóa, phiên bản đầy đủ có sẵn trên [GitHub](https://github.com/ruslan4432013/fsd-react-query-example) ## `QueryProvider` để quản lý queries[​](#queryprovider-để-quản-lý-queries "Link trực tiếp đến heading") Trong hướng dẫn này, chúng ta sẽ xem cách tổ chức một `QueryProvider`. ### 1. Tạo một `QueryProvider`[​](#1-tạo-một-queryprovider "Link trực tiếp đến heading") File `query-provider.tsx` nằm tại đường dẫn `@/app/providers/query-provider.tsx`. @/app/providers/query-provider.tsx ``` import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactNode } from "react"; type Props = { children: ReactNode; client: QueryClient; }; export const QueryProvider = ({ client, children }: Props) => { return ( {children} ); }; ``` ### 2. Tạo một `QueryClient`[​](#2-tạo-một-queryclient "Link trực tiếp đến heading") `QueryClient` là một instance được sử dụng để quản lý các API request. File `query-client.ts` nằm tại `@/shared/api/query-client.ts`. `QueryClient` được tạo với các cài đặt nhất định cho việc cache query. @/shared/api/query-client.ts ``` import { QueryClient } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000, }, }, }); ``` ## Tự động sinh code[​](#tự-động-sinh-code "Link trực tiếp đến heading") Có các công cụ có thể tự động sinh API code cho bạn, nhưng chúng kém linh hoạt hơn so với cách tiếp cận thủ công được mô tả ở trên. Nếu file Swagger của bạn có cấu trúc tốt, và bạn đang sử dụng một trong những công cụ này, việc sinh tất cả code trong thư mục `@/shared/api` có thể hợp lý. ## Lời khuyên bổ sung cho việc tổ chức RQ[​](#lời-khuyên-bổ-sung-cho-việc-tổ-chức-rq "Link trực tiếp đến heading") ### API Client[​](#api-client "Link trực tiếp đến heading") Sử dụng một class API client tùy chỉnh trong layer shared, bạn có thể chuẩn hóa cấu hình và làm việc với API trong dự án. Điều này cho phép bạn quản lý logging, header và định dạng trao đổi dữ liệu (như JSON hoặc XML) từ một nơi. Cách tiếp cận này giúp dễ dàng bảo trì và phát triển dự án vì nó đơn giản hóa các thay đổi và cập nhật tương tác với API. @/shared/api/api-client.ts ``` import { API_URL } from "@/shared/config"; export class ApiClient { private baseUrl: string; constructor(url: string) { this.baseUrl = url; } async handleResponse(response: Response): Promise { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } try { return await response.json(); } catch (error) { throw new Error("Error parsing JSON response"); } } public async get( endpoint: string, queryParams?: Record, ): Promise { const url = new URL(endpoint, this.baseUrl); if (queryParams) { Object.entries(queryParams).forEach(([key, value]) => { url.searchParams.append(key, value.toString()); }); } const response = await fetch(url.toString(), { method: "GET", headers: { "Content-Type": "application/json", }, }); return this.handleResponse(response); } public async post>( endpoint: string, body: TData, ): Promise { const response = await fetch(`${this.baseUrl}${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); return this.handleResponse(response); } } export const apiClient = new ApiClient(API_URL); ``` ## Xem thêm[​](#see-also "Link trực tiếp đến heading") * [(GitHub) Project mẫu](https://github.com/ruslan4432013/fsd-react-query-example) * [(CodeSandbox) Project mẫu](https://codesandbox.io/p/github/ruslan4432013/fsd-react-query-example/main) * [Về query factory](https://tkdodo.eu/blog/the-query-options-api) --- # Sử dụng với SvelteKit Có thể triển khai FSD trong dự án SvelteKit, nhưng xảy ra xung đột do sự khác biệt giữa yêu cầu cấu trúc của dự án SvelteKit và các nguyên tắc của FSD: * Ban đầu, SvelteKit cung cấp cấu trúc file bên trong thư mục `src/routes`, trong khi ở FSD thì routing phải là một phần của layer `app`. * SvelteKit đề xuất đặt mọi thứ không liên quan đến routing trong thư mục `src/lib`. ## Hãy thiết lập config[​](#hãy-thiết-lập-config "Link trực tiếp đến heading") svelte.config.ts ``` import adapter from '@sveltejs/adapter-auto'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config}*/ const config = { preprocess: [vitePreprocess()], kit: { adapter: adapter(), files: { routes: 'src/app/routes', // move routing inside the app layer lib: 'src', appTemplate: 'src/app/index.html', // Move the application entry point inside the app layer assets: 'public' }, alias: { '@/*': 'src/*' // Create an alias for the src directory } } }; export default config; ``` ## Di chuyển file routing vào `src/app`.[​](#di-chuyển-file-routing-vào-srcapp "Link trực tiếp đến heading") Hãy tạo một layer app, di chuyển điểm đầu vào `index.html` của app vào đó, và tạo một thư mục routes. Vậy, cấu trúc file của bạn nên trông như thế này: ``` ├── src │ ├── app │ │ ├── index.html │ │ ├── routes │ ├── pages # FSD Pages folder ``` Bây giờ, bạn có thể tạo các route cho pages trong `app` và kết nối các page từ `pages` với chúng. Ví dụ, để thêm một home page vào dự án của bạn, bạn cần thực hiện các bước sau: * Thêm một page slice bên trong layer `pages` * Thêm route tương ứng vào thư mục `routes` từ layer `app` * Canh chỉnh page từ slice với route Để tạo một page slice, hãy sử dụng [CLI](https://github.com/feature-sliced/cli): ``` fsd pages home ``` Tạo file `home-page.svelte` bên trong segment ui, truy cập nó bằng Public API src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page.svelte'; ``` Tạo một route cho page này bên trong layer `app`: ``` ├── src │ ├── app │ │ ├── routes │ │ │ ├── +page.svelte │ │ ├── index.html │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.svelte │ │ │ ├── index.ts ``` Thêm page component của bạn vào bên trong file `+page.svelte`: src/app/routes/+page.svelte ``` ``` ## Xem thêm[​](#xem-thêm "Link trực tiếp đến heading") * [Documentation on changing directory config in SvelteKit](https://kit.svelte.dev/docs/configuration#files) --- # Tài liệu cho LLMs Trang này cung cấp các liên kết và hướng dẫn cho các LLM crawler. * Spec: ### Files[​](#files "Link trực tiếp đến heading") * [llms.txt](/documentation/vi/llms.txt) * [llms-full.txt](/documentation/vi/llms-full.txt) ### Ghi chú[​](#ghi-chú "Link trực tiếp đến heading") * Các file được phục vụ từ root của site, bất kể đường dẫn trang hiện tại. * Trong các triển khai có base URL không phải root (ví dụ `/documentation/`), các liên kết phía trên sẽ được tự động thêm prefix. --- # Layer Layer là cấp độ đầu tiên của hệ thống phân cấp tổ chức trong Feature-Sliced Design. Mục đích của chúng là phân tách code dựa trên mức độ trách nhiệm cần thiết và số lượng module khác trong app mà nó phụ thuộc vào. Mỗi layer mang ý nghĩa ngữ nghĩa đặc biệt để giúp bạn xác định mức độ trách nhiệm mà bạn nên phân bổ cho code của mình. Có tổng cộng **7 layer**, được sắp xếp từ nhiều trách nhiệm và dependency nhất đến ít nhất: ![A file system tree, with a single root folder called src and then seven subfolders: app, processes, pages, widgets, features, entities, shared. The processes folder is slightly faded out.](/documentation/vi/img/layers/folders-graphic-light.svg#light-mode-only) ![A file system tree, with a single root folder called src and then seven subfolders: app, processes, pages, widgets, features, entities, shared. The processes folder is slightly faded out.](/documentation/vi/img/layers/folders-graphic-dark.svg#dark-mode-only) 1. App 2. Processes (deprecated) 3. Pages 4. Widgets 5. Features 6. Entities 7. Shared Bạn không cần phải sử dụng mọi layer trong dự án của mình — chỉ thêm chúng nếu bạn nghĩ nó mang lại giá trị cho dự án của bạn. Thông thường, hầu hết các dự án frontend sẽ có ít nhất các layer Shared, Pages và App. Trong thực tế, layer là các folder với tên viết thường (ví dụ: `📁 shared`, `📁 pages`, `📁 app`). Việc thêm layer mới *không được khuyến nghị* vì ngữ nghĩa của chúng đã được chuẩn hóa. ## Import rule trên layer[​](#import-rule-trên-layer "Link trực tiếp đến heading") Layer được tạo thành từ các *slice* — các nhóm module có tính gắn kết cao. Dependency giữa các slice được điều chỉnh bởi **import rule trên layer**: > *Một module (file) trong slice chỉ có thể import các slice khác khi chúng nằm trên các layer thấp hơn một cách nghiêm ngặt.* Ví dụ, folder `📁 ~/features/aaa` là một slice với tên "aaa". Một file bên trong nó, `~/features/aaa/api/request.ts`, không thể import code từ bất kỳ file nào trong `📁 ~/features/bbb`, nhưng có thể import code từ `📁 ~/entities` và `📁 ~/shared`, cũng như bất kỳ code anh em nào từ `📁 ~/features/aaa`, ví dụ `~/features/aaa/lib/cache.ts`. Layer App và Shared là **ngoại lệ** của quy tắc này — chúng vừa là layer vừa là slice cùng một lúc. Slice phân chia code theo business domain, và hai layer này là ngoại lệ vì Shared không có business domain, và App kết hợp tất cả business domain. Trong thực tế, điều này có nghĩa là layer App và Shared được tạo thành từ các segment, và các segment có thể import lẫn nhau một cách tự do. ## Định nghĩa layer[​](#định-nghĩa-layer "Link trực tiếp đến heading") Phần này mô tả ý nghĩa ngữ nghĩa của từng layer để tạo ra trực giác về loại code nào thuộc về đó. ### Shared[​](#shared "Link trực tiếp đến heading") Layer này tạo thành nền tảng cho phần còn lại của app. Đây là nơi tạo kết nối với thế giới bên ngoài, ví dụ: backend, third-party library, environment. Đây cũng là nơi định nghĩa các library có tính chứa đựng cao của riêng bạn. Layer này, giống như layer App, *không chứa slice*. Slice được dùng để chia layer thành các business domain, nhưng business domain không tồn tại trong Shared. Điều này có nghĩa là tất cả file trong Shared có thể tham chiếu và import lẫn nhau. Dưới đây là các segment mà bạn thường có thể tìm thấy trong layer này: * `📁 api` — API client và có thể cả các function để thực hiện request đến các endpoint backend cụ thể. * `📁 ui` — bộ UI kit của ứng dụng.
Các component trên layer này không nên chứa business logic, nhưng có thể có chủ đề business. Ví dụ, bạn có thể đặt logo công ty và layout trang ở đây. Các component có UI logic cũng được cho phép (ví dụ: autocomplete hoặc search bar). * `📁 lib` — tập hợp các internal library.
Folder này không nên được coi như helper hoặc utility ([đọc ở đây tại sao những folder này thường trở thành bãi rác](https://dev.to/sergeysova/why-utils-helpers-is-a-dump-45fo)). Thay vào đó, mỗi library trong folder này nên có một lĩnh vực tập trung, ví dụ: date, color, text manipulation, v.v. Lĩnh vực tập trung đó nên được ghi lại trong file README. Các developer trong team của bạn nên biết có thể thêm gì và không thể thêm gì vào những library này. * `📁 config` — environment variable, global feature flag và các cấu hình global khác cho app của bạn. * `📁 routes` — route constant hoặc pattern để matching route. * `📁 i18n` — setup code cho translation, global translation string. Bạn được tự do thêm nhiều segment hơn, nhưng hãy đảm bảo rằng tên của những segment này mô tả mục đích của nội dung, không phải bản chất của nó. Ví dụ, `components`, `hooks`, và `types` là những tên segment tệ vì chúng không hữu ích khi bạn đang tìm kiếm code. ### Entities[​](#entities "Link trực tiếp đến heading") Các slice trên layer này đại diện cho các khái niệm từ thế giới thực mà dự án đang làm việc. Thông thường, chúng là các thuật ngữ mà business sử dụng để mô tả sản phẩm. Ví dụ, một mạng xã hội có thể làm việc với các business entity như User, Post và Group. Một entity slice có thể chứa data storage (`📁 model`), data validation schema (`📁 model`), các function API request liên quan đến entity (`📁 api`), cũng như visual representation của entity này trong interface (`📁 ui`). Visual representation không cần phải tạo ra một UI block hoàn chỉnh — nó chủ yếu nhằm tái sử dụng cùng một appearance trên nhiều page trong app, và các business logic khác nhau có thể được gắn vào nó thông qua props hoặc slot. #### Mối quan hệ entity[​](#mối-quan-hệ-entity "Link trực tiếp đến heading") Entity trong FSD là các slice, và mặc định, các slice không thể biết về nhau. Tuy nhiên, trong đời thực, các entity thường tương tác với nhau, và đôi khi một entity sở hữu hoặc chứa các entity khác. Vì vậy, business logic của những tương tác này tốt nhất nên được giữ ở các layer cao hơn, như Features hoặc Pages. Khi data object của một entity chứa các data object khác, thường là ý tưởng tốt để làm cho kết nối giữa các entity trở nên rõ ràng và bỏ qua slice isolation bằng cách tạo cross-reference API với ký hiệu `@x`. Lý do là các entity được kết nối cần được refactor cùng nhau, vì vậy tốt nhất là làm cho kết nối không thể bỏ sót. For example: entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` entities/song/@x/artist.ts ``` export type { Song } from "../model/song.ts"; ``` Tìm hiểu thêm về ký hiệu `@x` trong phần [Public API cho cross-import](/documentation/vi/docs/reference/public-api.md#public-api-for-cross-imports). ### Features[​](#features "Link trực tiếp đến heading") Layer này dành cho các tương tác chính trong app của bạn, những thứ mà người dùng quan tâm để làm. Các tương tác này thường liên quan đến các business entity, vì đó là nội dung của app. Một nguyên tắc quan trọng để sử dụng layer Features hiệu quả là: **không phải mọi thứ đều cần là feature**. Một chỉ báo tốt để biết thứ gì cần là feature là nó được tái sử dụng trên nhiều page. Ví dụ, nếu app có nhiều editor, và tất cả chúng đều có comment, thì comment là một feature được tái sử dụng. Hãy nhớ rằng slice là cơ chế để tìm code nhanh chóng, và nếu có quá nhiều feature, những cái quan trọng sẽ bị chìm nghỉm. Lý tưởng, khi bạn đến một dự án mới, bạn sẽ khám phá tính năng của nó bằng cách xem qua các page và feature. Khi quyết định thứ gì nên là feature, hãy tối ưu hóa cho trải nghiệm của người mới vào dự án để nhanh chóng khám phá các khu vực code lớn quan trọng. Một feature slice có thể chứa UI để thực hiện tương tác như form (`📁 ui`), các API call cần thiết để thực hiện action (`📁 api`), validation và internal state (`📁 model`), feature flag (`📁 config`). ### Widgets[​](#widgets "Link trực tiếp đến heading") Layer Widgets được thiết kế cho các UI block lớn tự đủ. Widget hữu ích nhất khi chúng được tái sử dụng trên nhiều page, hoặc khi page mà chúng thuộc về có nhiều block độc lập lớn, và đây là một trong số chúng. Nếu một UI block tạo nên phần lớn nội dung thú vị trên page, và không bao giờ được tái sử dụng, nó **không nên là widget**, và thay vào đó nên được đặt trực tiếp bên trong page đó. mẹo Nếu bạn đang sử dụng nested routing system (như router của [Remix](https://remix.run)), có thể hữu ích khi sử dụng layer Widgets giống như cách mà flat routing system sẽ sử dụng layer Pages — để tạo các router block đầy đủ, hoàn chỉnh với data fetching liên quan, loading state, và error boundary. Tương tự, bạn có thể lưu page layout trên layer này. ### Pages[​](#pages "Link trực tiếp đến heading") Page là thứ tạo nên các website và application (cũng được biết đến là screen hoặc activity). Một page thường tương ứng với một slice, tuy nhiên, nếu có nhiều page rất giống nhau, chúng có thể được nhóm vào một slice, ví dụ registration và login form. Không có giới hạn về lượng code bạn có thể đặt trong page slice miễn là team của bạn vẫn thấy dễ navigate. Nếu một UI block trên page không được tái sử dụng, hoàn toàn ổn khi giữ nó bên trong page slice. Trong page slice bạn thường có thể tìm thấy UI của page cũng như loading state và error boundary (`📁 ui`) và các data fetching và mutating request (`📁 api`). Không phổ biến đề page có data model riêng biệt, và các bit state nhỏ có thể được giữ trong chính các component. ### Processes[​](#processes "Link trực tiếp đến heading") cẩn thận Layer này đã bị deprecated. Phiên bản hiện tại của spec khuyên tránh nó và chuyển nội dung của nó sang `features` và `app` thay vào đó. Process là escape hatch cho các tương tác nhiều page. Layer này cố tình được để không định nghĩa. Hầu hết các ứng dụng không nên sử dụng layer này, và giữ logic cấp router và cấp server trên layer App. Chỉ cân nhắc sử dụng layer này khi layer App phát triển đủ lớn để trở nên không thể bảo trì và cần giảm tải. ### App[​](#app "Link trực tiếp đến heading") Mọi loại vấn đề app-wide, cả trong nghĩa kỹ thuật (ví dụ: context provider) và trong nghĩa business (ví dụ: analytics). Layer này thường không chứa slice, cũng giống như Shared, thay vào đó có các segment trực tiếp. Dưới đây là các segment mà bạn thường có thể tìm thấy trong layer này: * `📁 routes` — router configuration * `📁 store` — global store configuration * `📁 styles` — global style * `📁 entrypoint` — entrypoint đến application code, framework-specific --- # Public API Public API là một *hợp đồng* giữa một nhóm module, như một slice, và code sử dụng nó. Nó cũng hoạt động như một cổng kiểm soát, chỉ cho phép truy cập đến các đối tượng nhất định và chỉ thông qua public API đó. Trong thực tế, nó thường được triển khai dưới dạng file index với các re-export: pages/auth/index.js ``` export { LoginPage } from "./ui/LoginPage"; export { RegisterPage } from "./ui/RegisterPage"; ``` ## Điều gì tạo nên một public API tốt?[​](#điều-gì-tạo-nên-một-public-api-tốt "Link trực tiếp đến heading") Một public API tốt làm cho việc sử dụng và tích hợp slice vào code khác trở nên thuận tiện và đáng tin cậy. Điều này có thể đạt được bằng cách thiết lập ba mục tiêu sau: 1. Phần còn lại của ứng dụng phải được bảo vệ khỏi các thay đổi cấu trúc của slice, như refactoring 2. Những thay đổi đáng kể trong hành vi của slice mà phá vỡ các kỳ vọng trước đó phải gây ra thay đổi trong public API 3. Chỉ những phần cần thiết của slice mới nên được expose Mục tiêu cuối cùng có một số hàm ý thực tế quan trọng. Có thể rất hấp dẫn khi tạo wildcard re-export cho mọi thứ, đặc biệt trong giai đoạn phát triển đầu của slice, vì bất kỳ đối tượng mới nào bạn export từ các file cũng sẽ tự động được export từ slice: Bad practice, features/comments/index.js ``` // ❌ BAD CODE BELOW, DON'T DO THIS export * from "./ui/Comment"; // 👎 don't try this at home export * from "./model/comments"; // 💩 this is bad practice ``` Điều này làm tổn hại khả năng khám phá của slice vì bạn không thể dễ dàng biết được interface của slice này là gì. Không biết interface có nghĩa là bạn phải đào sâu vào code của slice để hiểu cách tích hợp nó. Một vấn đề khác là bạn có thể vô tình expose các module internal, điều này sẽ khiến refactoring trở nên khó khăn nếu ai đó bắt đầu phụ thuộc vào chúng. ## Public API cho cross-imports[​](#public-api-for-cross-imports "Link trực tiếp đến heading") Cross-import là tình huống khi một slice import từ slice khác trên cùng layer. Thông thường điều này bị cấm bởi [import rule on layers](/documentation/vi/docs/reference/layers.md#import-rule-on-layers), nhưng thường có những lý do chính đáng để cross-import. Ví dụ, các business entity thường tham chiếu lẫn nhau trong thế giới thực, và tốt nhất là phản ánh những mối quan hệ này trong code thay vì tránh chúng. Cho mục đích này, có một loại public API đặc biệt, còn được biết đến với tên gọi `@x`-notation. Nếu bạn có entity A và B, và entity B cần import từ entity A, thì entity A có thể khai báo một public API riêng chỉ dành cho entity B. * `📂 entities` * `📂 A` * `📂 @x` * `📄 B.ts` — một public API đặc biệt chỉ dành cho code bên trong `entities/B/` * `📄 index.ts` — public API thông thường Sau đó code bên trong `entities/B/` có thể import từ `entities/A/@x/B`: ``` import type { EntityA } from "entities/A/@x/B"; ``` Ký hiệu `A/@x/B` có nghĩa là "A crossed with B". ghi chú Hãy cố gắng giữ cross-import ở mức tối thiểu, và **chỉ sử dụng ký hiệu này trên layer Entities**, nơi mà việc loại bỏ cross-import thường là không hợp lý. ## Vấn đề với index file[​](#vấn-đề-với-index-file "Link trực tiếp đến heading") Các index file như `index.js`, còn được gọi là barrel file, là cách phổ biến nhất để định nghĩa public API. Chúng dễ tạo, nhưng được biết đến là gây ra vấn đề với một số bundler và framework nhất định. ### Circular import[​](#circular-import "Link trực tiếp đến heading") Circular import là khi hai hoặc nhiều file import lẫn nhau theo vòng tròn. ![Three files importing each other in a circle](/documentation/vi/img/circular-import-light.svg#light-mode-only)![Three files importing each other in a circle](/documentation/vi/img/circular-import-dark.svg#dark-mode-only) Minh họa ở trên: ba file, `fileA.js`, `fileB.js`, và `fileC.js`, import lẫn nhau theo vòng tròn. Những tình huống này thường khó khăn để bundler xử lý, và trong một số trường hợp chúng thậm chí có thể dẫn đến runtime error khó debug. Circular import có thể xảy ra mà không cần index file, nhưng việc có index file tạo ra cơ hội rõ ràng để vô tình tạo circular import. Điều này thường xảy ra khi bạn có hai object được expose trong public API của slice, ví dụ `HomePage` và `loadUserStatistics`, và `HomePage` cần truy cập `loadUserStatistics`, nhưng nó làm như thế này: pages/home/ui/HomePage.jsx ``` import { loadUserStatistics } from "../"; // importing from pages/home/index.js export function HomePage() { /* … */ } ``` pages/home/index.js ``` export { HomePage } from "./ui/HomePage"; export { loadUserStatistics } from "./api/loadUserStatistics"; ``` Tình huống này tạo ra circular import, vì `index.js` import `ui/HomePage.jsx`, nhưng `ui/HomePage.jsx` import `index.js`. Để ngăn chặn vấn đề này, hãy xem xét hai nguyên tắc sau. Nếu bạn có hai file, và một file import từ file kia: * Khi chúng ở trong cùng slice, luôn sử dụng import *relative* và viết đầy đủ đường dẫn import * Khi chúng ở trong các slice khác nhau, luôn sử dụng import *absolute*, ví dụ với alias ### Bundle lớn và tree-shaking bị hỏng trong Shared[​](#large-bundles "Link trực tiếp đến heading") Một số bundler có thể gặp khó khăn trong việc tree-shake (loại bỏ code không được import) khi bạn có index file re-export mọi thứ. Thông thường đây không phải là vấn đề cho public API, vì nội dung của một module thường có liên quan chặt chẽ với nhau, nên bạn hiếm khi cần import một thứ và tree-shake đi thứ khác. Tuy nhiên, có hai trường hợp rất phổ biến khi các quy tắc thông thường của public API trong FSD có thể dẫn đến vấn đề — `shared/ui` và `shared/lib`. Hai folder này đều là tập hợp các thứ không liên quan mà thường không cần thiết tất cả ở một nơi. Ví dụ, `shared/ui` có thể có module cho mỗi component trong UI library: * `📂 shared/ui/` * `📁 button` * `📁 text-field` * `📁 carousel` * `📁 accordion` Vấn đề này trở nên tồi tệ hơn khi một trong những module này có dependency nặng, như syntax highlighter hoặc drag'n'drop library. Bạn không muốn kéo chúng vào mọi page sử dụng thứ gì đó từ `shared/ui`, ví dụ như một button. Nếu bundle của bạn phát triển không mong muốn do một public API duy nhất trong `shared/ui` hoặc `shared/lib`, được khuyến nghị thay vào đó hãy có một index file riêng cho mỗi component hoặc library: * `📂 shared/ui/` * `📂 button` * `📄 index.js` * `📂 text-field` * `📄 index.js` Sau đó các consumer của những component này có thể import chúng trực tiếp như thế này: pages/sign-in/ui/SignInPage.jsx ``` import { Button } from '@/shared/ui/button'; import { TextField } from '@/shared/ui/text-field'; ``` ### Không có bảo vệ thực sự chống lại việc bỏ qua public API[​](#không-có-bảo-vệ-thực-sự-chống-lại-việc-bỏ-qua-public-api "Link trực tiếp đến heading") Khi bạn tạo index file cho slice, bạn không thực sự cấm ai đó không sử dụng nó và import trực tiếp. Điều này đặc biệt là vấn đề với auto-import, vì có nhiều nơi mà một object có thể được import, nên IDE phải quyết định cho bạn. Đôi khi nó có thể chọn import trực tiếp, phá vỡ quy tắc public API trên slice. Để tự động phát hiện những vấn đề này, chúng tôi khuyên sử dụng [Steiger](https://github.com/feature-sliced/steiger), một architectural linter với ruleset cho Feature-Sliced Design. ### Hiệu suất bundler kém hơn trên các dự án lớn[​](#hiệu-suất-bundler-kém-hơn-trên-các-dự-án-lớn "Link trực tiếp đến heading") Việc có một lượng lớn index file trong dự án có thể làm chậm development server, như TkDodo đã lưu ý trong [bài viết "Please Stop Using Barrel Files"](https://tkdodo.eu/blog/please-stop-using-barrel-files) của anh ấy. Có một số điều bạn có thể làm để giải quyết vấn đề này: 1. Lời khuyên giống như trong vấn đề ["Bundle lớn và tree-shaking bị hỏng trong Shared"](#large-bundles) — có index file riêng cho từng component/library trong `shared/ui` và `shared/lib` thay vì một file lớn 2. Tránh có index file trong segment trên các layer có slice.
Ví dụ, nếu bạn có index cho feature "comments", `📄 features/comments/index.js`, thì không có lý do gì để có thêm index cho segment `ui` của feature đó, `📄 features/comments/ui/index.js`. 3. Nếu bạn có một dự án rất lớn, có khả năng cao là ứng dụng của bạn có thể được chia thành nhiều chunk lớn.
Ví dụ, Google Docs có trách nhiệm rất khác nhau cho document editor và file browser. Bạn có thể tạo monorepo setup nơi mỗi package là một FSD root riêng biệt, với bộ layer riêng. Một số package chỉ có thể có layer Shared và Entities, những package khác có thể chỉ có Pages và App, những package khác nữa có thể bao gồm Shared nhỏ của riêng mình, nhưng vẫn sử dụng cái lớn từ package khác. --- # Slice và segment ## Slice[​](#slice "Link trực tiếp đến heading") Slice là cấp độ thứ hai trong hệ thống phân cấp tổ chức của Feature-Sliced Design. Mục đích chính của chúng là nhóm code theo ý nghĩa của nó đối với sản phẩm, business, hoặc chỉ đơn giản là application. Tên của các slice không được chuẩn hóa vì chúng được xác định trực tiếp bởi business domain của ứng dụng của bạn. Ví dụ, một photo gallery có thể có các slice `photo`, `effects`, `gallery-page`. Một mạng xã hội sẽ yêu cầu các slice khác nhau, ví dụ `post`, `comments`, `news-feed`. Các layer Shared và App không chứa slice. Đó là vì Shared không nên chứa business logic nào cả, do đó không có ý nghĩa gì đối với sản phẩm, và App chỉ nên chứa code liên quan đến toàn bộ ứng dụng, vì vậy không cần phải chia tách. ### Zero coupling và high cohesion[​](#zero-coupling-high-cohesion "Link trực tiếp đến heading") Slice được thiết kế để là các nhóm file code độc lập và có tính gắn kết cao. Hình minh họa dưới đây có thể giúp hình dung các khái niệm khó hiểu về *cohesion* và *coupling*: ![](/documentation/vi/img/coupling-cohesion-light.svg#light-mode-only)![](/documentation/vi/img/coupling-cohesion-dark.svg#dark-mode-only) Image inspired by Một slice lý tưởng là độc lập với các slice khác trên layer của nó (zero coupling) và chứa hầu hết code liên quan đến mục tiêu chính của nó (high cohesion). Tính độc lập của các slice được thực thi bởi [import rule trên layer](/documentation/vi/docs/reference/layers.md#import-rule-on-layers): > *Một module (file) trong slice chỉ có thể import các slice khác khi chúng được đặt trên các layer thấp hơn một cách nghiêm ngặt.* ### Public API rule trên slice[​](#public-api-rule-trên-slice "Link trực tiếp đến heading") Bên trong slice, code có thể được tổ chức theo bất kỳ cách nào mà bạn muốn. Điều đó không gây ra vấn đề gì miễn là slice cung cấp public API tốt cho các slice khác sử dụng nó. Điều này được thực thi với **public API rule trên slice**: > *Mỗi slice (và segment trên các layer không có slice) phải chứa một định nghĩa public API.* > > *Các module bên ngoài slice/segment này chỉ có thể tham chiếu public API, không phải cấu trúc file nội bộ của slice/segment.* Đọc thêm về lý lẽ của public API và best practice để tạo một cái trong [Public API reference](/documentation/vi/docs/reference/public-api.md). ### Nhóm slice[​](#nhóm-slice "Link trực tiếp đến heading") Các slice liên quan chặt chẽ có thể được nhóm về mặt cấu trúc trong một folder, nhưng chúng nên thực hiện các quy tắc cô lập giống như các slice khác — không nên có **code sharing** trong folder đó. ![Features \"compose\", \"like\" and \"delete\" grouped in a folder \"post\". In that folder there is also a file \"some-shared-code.ts\" that is crossed out to imply that it\'s not allowed.](/documentation/vi/assets/images/graphic-nested-slices-b9c44e6cc55ecdbf3e50bf40a61e5a27.svg) ## Segment[​](#segment "Link trực tiếp đến heading") Segment là cấp độ thứ ba và cuối cùng trong hệ thống phân cấp tổ chức, và mục đích của chúng là nhóm code theo bản chất kỹ thuật của nó. Có một số tên segment được chuẩn hóa: * `ui` — mọi thứ liên quan đến hiển thị UI: UI component, date formatter, style, v.v. * `api` — tương tác backend: request function, data type, mapper, v.v. * `model` — data model: schema, interface, store, và business logic. * `lib` — library code mà các module khác trên slice này cần. * `config` — configuration file và feature flag. Xem [trang Layer](/documentation/vi/docs/reference/layers.md#layer-definitions) để biết ví dụ về cách sử dụng từng segment này trên các layer khác nhau. Bạn cũng có thể tạo custom segment. Những nơi phổ biến nhất cho custom segment là layer App và layer Shared, nơi mà slice không có ý nghĩa. Hãy đảm bảo rằng tên của những segment này mô tả mục đích của nội dung, không phải bản chất của nó. Ví dụ, `components`, `hooks`, và `types` là những tên segment tệ vì chúng không hữu ích khi bạn đang tìm kiếm code. --- ### Business logic rõ ràng Kiến trúc dễ khám phá nhờ phân chia theo domain scopes ---