diff --git a/config/docusaurus/i18n.js b/config/docusaurus/i18n.js index 2cef5f905..6ff7baf75 100644 --- a/config/docusaurus/i18n.js +++ b/config/docusaurus/i18n.js @@ -3,7 +3,7 @@ const { DEFAULT_LOCALE } = require("./consts"); /** @type {import('@docusaurus/types').DocusaurusConfig["i18n"]} */ const i18n = { defaultLocale: DEFAULT_LOCALE, - locales: ["ru", "en", "uz", "kr", "ja"], + locales: ["ru", "en", "uz", "kr", "ja", "zh"], localeConfigs: { ru: { label: "Русский", @@ -20,6 +20,9 @@ const i18n = { ja: { label: "日本語", }, + zh: { + label: "中文", + }, }, }; diff --git a/i18n/zh/code.json b/i18n/zh/code.json new file mode 100644 index 000000000..26fdb6399 --- /dev/null +++ b/i18n/zh/code.json @@ -0,0 +1,386 @@ +{ + "pages.home.features.title": { + "message": "特性", + "description": "Features" + }, + "pages.home.features.logic.title": { + "message": "明确的业务逻辑", + "description": "Feature title" + }, + "pages.home.features.logic.description": { + "message": "通过领域范围实现易于发现的架构", + "description": "Feature description" + }, + "pages.home.features.adaptability.title": { + "message": "适应性", + "description": "Feature title" + }, + "pages.home.features.adaptability.description": { + "message": "架构组件可以灵活地替换和添加以满足新需求", + "description": "Feature description" + }, + "pages.home.features.debt.title": { + "message": "技术债务与重构", + "description": "Feature title" + }, + "pages.home.features.debt.description": { + "message": "每个模块都可以独立修改/重写而不产生副作用", + "description": "Feature description" + }, + "pages.home.features.shared.title": { + "message": "明确的代码复用", + "description": "Feature title" + }, + "pages.home.features.shared.description": { + "message": "在 DRY 原则和本地定制之间保持平衡", + "description": "Feature description" + }, + "pages.home.concepts.title": { + "message": "概念", + "description": "Concepts" + }, + "pages.home.concepts.public.title": { + "message": "公共 API", + "description": "Concept title" + }, + "pages.home.concepts.public.description": { + "message": "每个模块必须在顶层声明其公共 API", + "description": "Concept description" + }, + "pages.home.concepts.isolation.title": { + "message": "隔离", + "description": "Concept title" + }, + "pages.home.concepts.isolation.description": { + "message": "模块不应直接依赖于同一层级或上层的其他模块", + "description": "Concept description" + }, + "pages.home.concepts.needs.title": { + "message": "需求驱动", + "description": "Concept title" + }, + "pages.home.concepts.needs.description": { + "message": "面向业务和用户需求", + "description": "Concept description" + }, + "pages.home.scheme.title": { + "message": "架构图", + "description": "Scheme" + }, + "pages.home.companies.using": { + "message": "使用 FSD 的公司", + "description": "Companies using FSD" + }, + "pages.home.companies.add_me": { + "message": "您的公司正在使用 FSD?", + "description": "FSD is used in your company?" + }, + "pages.home.companies.tell_us": { + "message": "告诉我们", + "description": "Tell us" + }, + "pages.examples.title": { + "message": "示例", + "description": "Page title" + }, + "pages.examples.subtitle": { + "message": "使用 Feature-Sliced Design 构建的网站列表", + "description": "Page subtitle" + }, + "pages.versions.title": { + "message": "Feature-Sliced Design 版本", + "description": "Feature-Sliced Design versions" + }, + "pages.versions.current": { + "message": "当前已发布版本的文档可以在这里找到", + "description": "Description for current version" + }, + "pages.versions.legacy": { + "message": "{of} 旧版本的文档可以在这里找到", + "description": "Description for legacy version" + }, + "pages.nav.title": { + "message": "🧭 导航", + "description": "NavPage title" + }, + "pages.nav.legacy.title": { + "message": "旧版路由", + "description": "NavPage section=legacy title" + }, + "pages.nav.legacy.details": { + "message": "在文档重构后,一些路由已经更改。下面您可以找到可能正在寻找的页面。", + "description": "NavPage section=legacy details" + }, + "pages.nav.legacy.subdetails": { + "message": "但为了兼容性,旧链接会有重定向", + "description": "NavPage section=legacy subdetails" + }, + "features.feedback-badge.label": { + "message": "分享您对文档的反馈 🤙", + "description": "Feedback share button label" + }, + "features.feedback-badge.url": { + "message": "https://forms.gle/nsYua6bMMG5iBB3v7", + "description": "Feedback share form url" + }, + "features.feedback-doc.button-text": { + "message": "反馈", + "description": "The text on a floating button to leave feedback about the docs" + }, + "features.feedback-doc.email-placeholder": { + "message": "您的邮箱(可选)", + "description": "The placeholder for email input" + }, + "features.feedback-doc.error-message": { + "message": "出了点问题。请稍后再试。", + "description": "The error message displayed when feedback form submission fails" + }, + "features.feedback-doc.modal-title-error-403": { + "message": "请求 URL 与该项目在 PushFeedback 中定义的 URL 不匹配。", + "description": "The title of the modal displayed when feedback form submission fails with 403 error" + }, + "features.feedback-doc.modal-title-error-404": { + "message": "我们在 PushFeedback 中找不到提供的项目 ID。", + "description": "The title of the modal displayed when feedback form submission fails with 404 error" + }, + "features.feedback-doc.message-placeholder": { + "message": "评论", + "description": "The placeholder for message input" + }, + "features.feedback-doc.modal-title": { + "message": "分享您的反馈", + "description": "The title of the modal to leave feedback about the docs" + }, + "features.feedback-doc.modal-title-error": { + "message": "糟糕!", + "description": "The title of the modal displayed when feedback form submission fails" + }, + "features.feedback-doc.modal-title-success": { + "message": "感谢您的反馈!", + "description": "The title of the modal displayed when feedback form submission succeeds" + }, + "features.feedback-doc.screenshot-button-text": { + "message": "截图", + "description": "The text on a button to take a screenshot" + }, + "features.feedback-doc.screenshot-topbar-text": { + "message": "选择页面上的一个元素", + "description": "The text displayed in the top bar when selecting an element to take a screenshot" + }, + "features.feedback-doc.send-button-text": { + "message": "发送", + "description": "The text on a button to send feedback" + }, + "features.feedback-doc.rating-placeholder": { + "message": "这个页面有帮助吗?", + "description": "The placeholder for rating input" + }, + "features.feedback-doc.rating-stars-placeholder": { + "message": "您如何评价这个页面", + "description": "The placeholder for rating stars input" + }, + "features.hero.tagline": { + "message": "前端项目的架构方法论", + "description": "Architectural methodology for frontend projects" + }, + "features.hero.get_started": { + "message": "开始使用", + "description": "Get Started" + }, + "features.hero.examples": { + "message": "示例", + "description": "Examples" + }, + "features.hero.previous": { + "message": "旧版本", + "description": "Previous version" + }, + "shared.wip.title": { + "message": "文章正在编写中", + "description": "Admonition title" + }, + "shared.wip.subtitle": { + "message": "为了使文章更快发布,您可以:", + "description": "Admonition subtitle" + }, + "shared.wip.var.feedback.base": { + "message": "📢 分享您的反馈", + "description": "Variant for contribute (base)" + }, + "shared.wip.var.feedback.link": { + "message": "在文章中(评论/表情反应)", + "description": "Variant for contribute (link)" + }, + "shared.wip.var.material.base": { + "message": "💬 收集相关的", + "description": "Variant for contribute (base)" + }, + "shared.wip.var.material.link": { + "message": "来自聊天的主题相关资料", + "description": "Variant for contribute (link)" + }, + "shared.wip.var.contribute.base": { + "message": "⚒️ 贡献", + "description": "Variant for contribute (base)" + }, + "shared.wip.var.contribute.link": { + "message": "以任何其他方式", + "description": "Variant for contribute (link)" + }, + "theme.NotFound.title": { + "message": "页面未找到", + "description": "The title of the 404 page" + }, + "theme.NotFound.p1": { + "message": "我们找不到您要查找的内容。", + "description": "The first paragraph of the 404 page" + }, + "theme.NotFound.p2": { + "message": "请联系链接到原始 URL 的网站所有者,告知他们链接已损坏。", + "description": "The 2nd paragraph of the 404 page" + }, + "theme.AnnouncementBar.closeButtonAriaLabel": { + "message": "关闭", + "description": "The ARIA label for close button of announcement bar" + }, + "theme.blog.paginator.navAriaLabel": { + "message": "博客列表页面导航", + "description": "The ARIA label for the blog pagination" + }, + "theme.blog.paginator.newerEntries": { + "message": "较新的条目", + "description": "The label used to navigate to the newer blog posts page (previous page)" + }, + "theme.blog.paginator.olderEntries": { + "message": "较旧的条目", + "description": "The label used to navigate to the older blog posts page (next page)" + }, + "theme.blog.post.readingTime.plurals": { + "message": "阅读时间 1 分钟|阅读时间 {readingTime} 分钟", + "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.tags.tagsListLabel": { + "message": "标签:", + "description": "The label alongside a tag list" + }, + "theme.blog.post.readMore": { + "message": "阅读更多", + "description": "The label used in blog post item excerpts to link to full blog posts" + }, + "theme.blog.post.paginator.navAriaLabel": { + "message": "博客文章页面导航", + "description": "The ARIA label for the blog posts pagination" + }, + "theme.blog.post.paginator.newerPost": { + "message": "较新的文章", + "description": "The blog post button label to navigate to the newer/previous post" + }, + "theme.blog.post.paginator.olderPost": { + "message": "较旧的文章", + "description": "The blog post button label to navigate to the older/next post" + }, + "theme.tags.tagsPageTitle": { + "message": "标签", + "description": "The title of the tag list page" + }, + "theme.blog.post.plurals": { + "message": "1 篇文章|{count} 篇文章", + "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.tagTitle": { + "message": "{nPosts} 篇标记为 \"{tagName}\" 的文章", + "description": "The title of the page for a blog tag" + }, + "theme.tags.tagsPageLink": { + "message": "查看所有标签", + "description": "The label of the link targeting the tag list page" + }, + "theme.CodeBlock.copyButtonAriaLabel": { + "message": "复制代码到剪贴板", + "description": "The ARIA label for copy code blocks button" + }, + "theme.CodeBlock.copied": { + "message": "已复制", + "description": "The copied button label on code blocks" + }, + "theme.CodeBlock.copy": { + "message": "复制", + "description": "The copy button label on code blocks" + }, + "theme.docs.sidebar.expandButtonTitle": { + "message": "展开侧边栏", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.sidebar.expandButtonAriaLabel": { + "message": "展开侧边栏", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.paginator.navAriaLabel": { + "message": "文档页面导航", + "description": "The ARIA label for the docs pagination" + }, + "theme.docs.paginator.previous": { + "message": "上一页", + "description": "The label used to navigate to the previous doc" + }, + "theme.docs.paginator.next": { + "message": "下一页", + "description": "The label used to navigate to the next doc" + }, + "theme.docs.sidebar.collapseButtonTitle": { + "message": "收起侧边栏", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.docs.sidebar.collapseButtonAriaLabel": { + "message": "收起侧边栏", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.docs.sidebar.responsiveCloseButtonLabel": { + "message": "关闭菜单", + "description": "The ARIA label for close button of mobile doc sidebar" + }, + "theme.docs.sidebar.responsiveOpenButtonLabel": { + "message": "打开菜单", + "description": "The ARIA label for open button of mobile doc sidebar" + }, + "theme.docs.versions.unreleasedVersionLabel": { + "message": "这是 {siteTitle} {versionLabel} 版本的未发布文档。", + "description": "The label used to tell the user that he's browsing an unreleased doc version" + }, + "theme.docs.versions.unmaintainedVersionLabel": { + "message": "这是 {siteTitle} {versionLabel} 的文档,该版本不再积极维护。", + "description": "The label used to tell the user that he's browsing an unmaintained doc version" + }, + "theme.docs.versions.latestVersionSuggestionLabel": { + "message": "如需最新文档,请参阅 {latestVersionLink}({versionLabel})。", + "description": "The label userd to tell the user that he's browsing an unmaintained doc version" + }, + "theme.docs.versions.latestVersionLinkLabel": { + "message": "最新版本", + "description": "The label used for the latest version suggestion link label" + }, + "theme.common.editThisPage": { + "message": "编辑此页面", + "description": "The link label to edit the current page" + }, + "theme.common.headingLinkTitle": { + "message": "标题的直接链接", + "description": "Title for link to heading" + }, + "theme.lastUpdated.atDate": { + "message": " 于 {date}", + "description": "The words used to describe on which date a page has been last updated" + }, + "theme.lastUpdated.byUser": { + "message": " 由 {user}", + "description": "The words used to describe by who the page has been last updated" + }, + "theme.lastUpdated.lastUpdatedAtBy": { + "message": "最后更新{atDate}{byUser}", + "description": "The sentence used to display when a page has been last updated, and by who" + }, + "theme.common.skipToMainContent": { + "message": "跳转到主要内容", + "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" + } +} diff --git a/i18n/zh/docusaurus-plugin-content-docs/community/index.mdx b/i18n/zh/docusaurus-plugin-content-docs/community/index.mdx new file mode 100644 index 000000000..cc2989ead --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/community/index.mdx @@ -0,0 +1,40 @@ +--- +hide_table_of_contents: true +--- + +# 💫 社区 + +

+社区资源,补充材料 +

+ +## 主要资源 + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { StarOutlined, SearchOutlined, TeamOutlined, VerifiedOutlined } from "@ant-design/icons"; + + + + + diff --git a/i18n/zh/docusaurus-plugin-content-docs/community/team.mdx b/i18n/zh/docusaurus-plugin-content-docs/community/team.mdx new file mode 100644 index 000000000..12fce9675 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/community/team.mdx @@ -0,0 +1,18 @@ +--- +sidebar_class_name: sidebar-item--wip +sidebar_position: 2 +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 团队 + + + +## 核心团队 + +### 倡导者 + +## 贡献者 + +## 公司 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current.json b/i18n/zh/docusaurus-plugin-content-docs/current.json new file mode 100644 index 000000000..a33a0a94c --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,38 @@ +{ + "version.label": { + "message": "v2.1", + "description": "The label for version current" + }, + "sidebar.getstartedSidebar.category.Tutorials": { + "message": "教程", + "description": "The label for category Tutorials in sidebar getstartedSidebar" + }, + "sidebar.aboutSidebar.category.Alternatives": { + "message": "替代方案", + "description": "The label for category Alternatives in sidebar aboutSidebar" + }, + "sidebar.aboutSidebar.category.Promote": { + "message": "推广", + "description": "The label for category Promote in sidebar aboutSidebar" + }, + "sidebar.guidesSidebar.category.Examples": { + "message": "示例", + "description": "The label for category Examples in sidebar guidesSidebar" + }, + "sidebar.guidesSidebar.category.Migration": { + "message": "迁移", + "description": "The label for category Migration in sidebar guidesSidebar" + }, + "sidebar.guidesSidebar.category.Tech": { + "message": "技术", + "description": "The label for category Tech in sidebar guidesSidebar" + }, + "sidebar.conceptsSidebar.category.Issues": { + "message": "问题", + "description": "The label for category Issues in sidebar conceptsSidebar" + }, + "sidebar.referenceSidebar.category.Layer": { + "message": "层", + "description": "The label for category Layer in sidebar referenceSidebar" + } +} diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/alternatives.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/about/alternatives.mdx new file mode 100644 index 000000000..558d36f15 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/alternatives.mdx @@ -0,0 +1,153 @@ +--- +sidebar_class_name: sidebar-item--wip +sidebar_position: 3 +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# Alternatives + + + +History of architecture approaches + +## Big Ball of Mud + + + +> What is it; Why is it so common; When it starts to bring problems; What to do and how does FSD help in this + +- [(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 + + + +> About the approach; About applicability in the frontend; Methodology position + +About obsolescence, about a new view from the methodology + +Why component-containers approach is evil? + +- [(Article) Den Abramov-Presentation and Container Components (TLDR: deprecated)](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) + + +## Design Principles + + + +> What are we talking about; FSD position + +SOLID, GRASP, KISS, YAGNI, ... - and why they don't work well together in practice + +And how does it aggregate these practices + +- [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Design Principles)](https://youtu.be/SnzPAr_FJ7w?t=380) + + +## DDD + + + +> About the approach; Why does it work poorly in practice + +What is the difference, how does it improve applicability, where does it adopt practices + +- [(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 + + + +> About the approach; About applicability in the frontend; FSD position + +How are they similar (to many), how are they different + +- [(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 + + + +> About applicability in the frontend; Why frameworks do not solve problems; why there is no single approach; FSD position + +Framework-agnostic, conventional-approach + +- [(Article) About the reasons for creating the methodology (fragment about frameworks)](/docs/about/motivation) +- [(Thread) About the applicability of the methodology for different frameworks](https://t.me/feature_sliced/3867) + + +## Atomic Design + +### What is it? +In Atomic Design, the scope of responsibility is divided into standardized layers. + +Atomic Design is broken down into **5 layers** (from top to bottom): + +1. `pages` - Functionality similar to the `pages` layer in FSD. +2. `templates` - Components that define the structure of a page without tying to specific content. +3. `organisms` - Modules consisting of molecules that have business logic. +4. `molecules` - More complex components that generally do not contain business logic. +5. `atoms` - UI components without business logic. + +Modules at one layer interact only with modules in the layers below, similar to FSD. +That is, molecules are built from atoms, organisms from molecules, templates from organisms, and pages from templates. +Atomic Design also implies the use of Public API within modules for isolation. + +### Applicability to frontend + +Atomic Design is relatively common in projects. Atomic Design is more popular among web designers than in development. +Web designers often use Atomic Design to create scalable and easily maintainable designs. +In development, Atomic Design is often mixed with other architectural methodologies. + +However, since Atomic Design focuses on UI components and their composition, a problem arises with implementing +business logic within the architecture. + +The problem is that Atomic Design does not provide a clear level of responsibility for business logic, +leading to its distribution across various components and levels, complicating maintenance and testing. +The business logic becomes blurred, making it difficult to clearly separate responsibilities and rendering +the code less modular and reusable. + +### How does it relate to FSD? + +In the context of FSD, some elements of Atomic Design can be applied to create flexible and scalable UI components. +The `atoms` and `molecules` layers can be implemented in `shared/ui` in FSD, simplifying the reuse and +maintenance of basic UI elements. + +``` +├── shared +│ ├── ui +│ │ ├── atoms +│ │ ├── molecules +│ ... +``` +A comparison of FSD and Atomic Design shows that both methodologies strive for modularity and reusability +but focus on different aspects. Atomic Design is oriented towards visual components and their composition. +FSD focuses on dividing the application's functionality into independent modules and their interconnections. + +- [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 + + + +> About the approach; About applicability in the frontend; FSD position + +About compatibility, historical development and comparison + +- [(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) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/index.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/about/index.mdx new file mode 100644 index 000000000..0dd119a9b --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/index.mdx @@ -0,0 +1,44 @@ +--- +hide_table_of_contents: true +pagination_prev: reference/index +--- + +# 🍰 关于 + +背景导向 + +

+关于方法论、团队、社区和开发历史的一般信息 +

+ +## Main + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { StarOutlined, TrophyOutlined, BulbOutlined, TeamOutlined } from "@ant-design/icons"; + + + + + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/mission.md b/i18n/zh/docusaurus-plugin-content-docs/current/about/mission.md new file mode 100644 index 000000000..39bdec79d --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/mission.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 1 +--- + +# 使命 + +在这里我们描述方法论适用性的目标和限制——这是我们在开发该方法论时所遵循的指导原则 + +- 我们的目标是在理念和简单性之间取得平衡 +- 我们无法制造一个适合所有人的银弹 + +**尽管如此,该方法论应该对相当广泛的开发者群体来说是亲近且可访问的** + +## 目标 + +### 对广泛开发者的直观清晰度 + +该方法论应该是可访问的 - 对于项目中的大部分团队成员 + +*因为即使有了所有未来的工具,如果只有经验丰富的高级开发者/领导者才能理解该方法论,那也是不够的* + +### 解决日常问题 + +该方法论应该阐述我们在开发项目时遇到的日常问题的原因和解决方案 + +**并且还要为所有这些提供工具(cli、linters)** + +让开发者可以使用一种*经过实战检验*的方法,让他们能够绕过架构和开发中的长期问题 + +> *@sergeysova: 想象一下,一个开发者在该方法论的框架内编写代码,他遇到问题的频率减少了10倍,仅仅因为其他人已经考虑并解决了许多问题。* + +## 限制 + +我们不想*强加我们的观点*,同时我们理解*作为开发者,我们的许多习惯每天都在干扰我们* + +每个人在设计和开发系统方面都有自己的经验水平,**因此,值得理解以下几点:** + +- **不会起作用**:非常简单、非常清晰、适用于所有人 + > *@sergeysova: 某些概念在你遇到问题并花费多年时间解决它们之前,是无法直观理解的。* + > + > - *在数学世界中:是图论。* + > - *在物理学中:量子力学。* + > - *在编程中:应用程序架构。* + +- **可能且期望的**:简单性、可扩展性 + +## 参见 + +- [架构问题][refs-architecture--problems] + +[refs-architecture--problems]: /docs/about/understanding/architecture#problems diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/motivation.md b/i18n/zh/docusaurus-plugin-content-docs/current/about/motivation.md new file mode 100644 index 000000000..999836bc7 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/motivation.md @@ -0,0 +1,148 @@ +--- +sidebar_position: 2 +--- + +# Motivation + +The main idea of **Feature-Sliced Design** is to facilitate and reduce the cost of developing complex and developing projects, based on [combining research results, discussing the experience of various kinds of a wide range of developers][ext-discussions]. + +Obviously, this will not be a silver bullet, and of course, the methodology will have its own [limits of applicability][refs-mission]. + +Nevertheless, there are reasonable questions regarding *the feasibility of such a methodology as a whole* + +:::note + +More details [discussed in the discussion][disc-src] + +::: + +## Why are there not enough existing solutions? + +> It usually, these arguments: +> +> - *"Why you need some new methodology, if you already have long-established approaches and principles of design such as `SOLID`, `KISS`, `YAGNI`, `DDD`, `GRASP`, `DRY`, etc."* +> - *"All the problems are solved by good project documentation, tests, and structured processes"* +> - *"Problems would not have happened if all developers are following all the above"* +> - *"Everything was invented before you, you just can't use it"* +> - *"Take \{FRAMEWORK_NAME\} - everything has already been decided for you there"* + +### Principles alone are not enough + +**The existence of principles alone is not enough to design a good architecture** + +Not everyone knows them completely, even fewer understand and apply them correctly + +*The design principles are too general, and do not give a specific answer to the question: "How to design the structure and architecture of a scalable and flexible application?"* + +### Processes don't always work + +*Documentation/Tests/Processes* are, of course, good, but alas, even at high costs for them - **they do not always solve the problems posed by the architecture and the introduction of new people into the project** + +- The time of entry of each developer into the project is not greatly reduced, because the documentation will most often come out huge / outdated +- Constantly make sure that everyone understands architecture in the same way-it also requires a huge amount of resources +- Do not forget about the bus-factor + +### Existing frameworks cannot be applied everywhere + +- Existing solutions usually have a high entry threshold, which makes it difficult to find new developers +- Also, most often, the choice of technology has already been determined before the onset of serious problems in the project, and therefore you need to be able to "work with what is" - **without being tied to the technology** + +> Q: *"In my project `React/Vue/Redux/Effector/Mobx/{YOUR_TECH}` - how can I better build the structure of entities and the relationships between them?"* + +### As a result + +We get *"unique as snowflakes"* projects, each of which requires a long immersion of the employee, and knowledge that is unlikely to be applicable on another project + +> @sergeysova: *"This is exactly the situation that currently exists in our field of frontend development: each lead will invent different architectures and project structures, while it is not a fact that these structures will pass the test of time, as a result, a maximum of two people can develop the project besides him, and each new developer needs to be immersed again."* + +## Why do developers need the methodology? + +### Focus on business features, not on architecture problems + +The methodology allows you to save resources on designing a scalable and flexible architecture, instead directing the attention of developers to the development of the main functionality. At the same time, the architectural solutions themselves are standardized from project to project. + +*A separate question is that the methodology should earn the trust of the community, so that another developer can get acquainted with it and rely on it in solving the problems of his project within the time available to him* + +### An experience-proven solution + +The methodology is designed for developers who are aimed at *a proven solution for designing complex business logic* + +*However, it is clear that the methodology is generally about a set of best-practices, articles that address certain problems and cases during development. Therefore, the methodology will also be useful for the rest of the developers-who somehow face problems during development and design* + +### Project Health + +The methodology will allow *to solve and track the problems of the project in advance, without requiring a huge amount of resources* + +**Most often, technical debt accumulates and accumulates over time, and the responsibility for its resolution lies on both the lead and the team** + +The methodology will allow you to *warn* possible problems in the scaling and development of the project in advance + +## Why does a business need a methodology? + +### Fast onboarding + +With the methodology, you can hire a person to the project who **is already previously familiar with this approach, and not train again** + +*People start to understand and benefit the project faster, and there are additional guarantees to find people for the next iterations of the project* + +### An experience-proven solution + +With the methodology, the business will get *a solution for most of the issues that arise during the development of systems* + +Since most often a business wants to get a framework / solution that would solve the lion's share of problems during the development of the project + +### Applicability for different stages of the project + +The methodology can benefit the project *both at the stage of project support and development, and at the MVP stage* + +Yes, the most important thing for MVP is *"features, not the architecture laid down for the future"*. But even in conditions of limited deadlines, knowing the best-practices from the methodology, you can *"do with little blood"*, when designing the MVP version of the system, finding a reasonable compromise +(rather than modeling features "at random") + +*The same can be said about testing* + +## When is our methodology not needed? + +- If the project will live for a short time +- If the project does not need a supported architecture +- If the business does not perceive the connection between the code base and the speed of feature delivery +- If it is more important for the business to close orders as soon as possible, without further support + +### Business Size + +- **Small business** - most often needs a ready-made and very fast solution. Only when the business grows (at least to almost average), he understands that in order for customers to continue using, it is necessary, among other things, to devote time to the quality and stability of the solutions being developed +- **Medium-sized business** - usually understands all the problems of development, and even if it is necessary to *"arrange a race for features"*, he still spends time on quality improvements, refactoring and tests (and of course-on an extensible architecture) +- **Big business** - usually already has an extensive audience, staff, and a much more extensive set of its practices, and probably even its own approach to architecture, so the idea of taking someone else's comes to them not so often + +## Plans + +The main part of the goals [is set out here][refs-mission--goals], but in addition, it is worth talking about our expectations from the methodology in the future + +### Combining experience + +Now we are trying to combine all our diverse experience of the `core-team`, and get a methodology hardened by practice as a result + +Of course, we can get Angular 3.0 as a result, but it is much more important here to **investigate the very problem of designing the architecture of complex systems** + +*And yes - we have complaints about the current version of the methodology, but we want to work together to come to a single and optimal solution (taking into account, among other things, the experience of the community)* + +### Life outside the specification + +If everything goes well, then the methodology will not be limited only to the specification and the toolkit + +- Perhaps there will be reports, articles +- There may be `CODE_MODEs` for migrations to other technologies of projects written according to the methodology +- It is possible that as a result we will be able to reach the maintainers of large technological solutions + - *Especially for React, compared to other frameworks - this is the main problem, because it does not say how to solve certain problems* + +## See also + +- [(Discussion) Don't need a methodology?][disc-src] +- [About the methodology's mission: goals and limitations][refs-mission] +- [Types of knowledge in the project][refs-knowledge] + +[refs-mission]: /docs/about/mission +[refs-mission--goals]: /docs/about/mission#goals +[refs-knowledge]: /docs/about/understanding/knowledge-types + +[disc-src]: https://github.com/feature-sliced/documentation/discussions/27 +[ext-discussions]: https://github.com/feature-sliced/documentation/discussions diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/_category_.yaml b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/_category_.yaml new file mode 100644 index 000000000..ac0af8d34 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/_category_.yaml @@ -0,0 +1,3 @@ +label: 推广 +position: 11 +collapsed: true diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/for-company.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/for-company.mdx new file mode 100644 index 000000000..caa383bdc --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/for-company.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 4 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# Promote in company + + + +## Do the project and the company need a methodology? + +> About the justification of the application, Those duty + +## How can I submit a methodology to a business? + +## How to prepare and justify a plan to move to the methodology? \ No newline at end of file diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/for-team.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/for-team.mdx new file mode 100644 index 000000000..ee05767d4 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/for-team.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 3 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 在团队中推广 + + + +- 入职新成员 +- 开发指南("在哪里搜索 N 模块"等...) +- 任务的新方法 + +## 参见 +- [(线程) 旧方法的简单性和正念的重要性](https://t.me/feature_sliced/3360) +- [(线程) 关于按 layers 搜索的便利性](https://t.me/feature_sliced/1918) \ No newline at end of file diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/integration.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/integration.mdx new file mode 100644 index 000000000..45e9f44ea --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/integration.mdx @@ -0,0 +1,25 @@ +--- +sidebar_position: 1 +--- + +# 集成方面 + +## 总结 + +前 5 分钟(俄语): + + + +## 另外 + +**优势:** +- [概览](/docs/get-started/overview) +- 代码审查 +- 入职 + + +**缺点:** +- 心理复杂性 +- 高准入门槛 +- "Layers 地狱" +- 基于 feature 方法的典型问题 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/partial-application.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/partial-application.mdx new file mode 100644 index 000000000..38a7b0136 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/promote/partial-application.mdx @@ -0,0 +1,12 @@ +--- +sidebar_position: 2 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# Partial Application + + + +> How to partially apply the methodology? Does it make sense? What if I ignore it? diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/_category_.yaml b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/_category_.yaml new file mode 100644 index 000000000..5690b49cf --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/_category_.yaml @@ -0,0 +1,2 @@ +label: 理解 +position: 3 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/abstractions.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/abstractions.mdx new file mode 100644 index 000000000..47e469143 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/abstractions.mdx @@ -0,0 +1,26 @@ +--- +sidebar_position: 6 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# Abstractions + + + +## The law of leaky abstractions + +## Why are there so many abstractions + +> Abstractions help to cope with the complexity of the project. The question is - will these abstractions be specific only for this project, or will we try to derive general abstractions based on the specifics of the frontend + +> Architecture and applications in general are inherently complex, and the only question is how to better distribute and describe this complexity + +## About scopes of responsibility + +> About optional abstractions + +## See also +- [About the need for new layers](https://t.me/feature_sliced/2801) +- [About the difficulty in understanding the methodology and layers](https://t.me/feature_sliced/2619) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/architecture.md b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/architecture.md new file mode 100644 index 000000000..5e873c508 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/architecture.md @@ -0,0 +1,97 @@ +--- +sidebar_position: 1 +--- + +# 关于架构 + +## 问题 + +通常,当由于项目中的某些问题导致开发停止时,就会提出关于架构的讨论。 + +### Bus-factor 和入职 + +只有有限的人数理解项目及其架构 + +**示例:** + +- *"很难将一个人加入开发中"* +- *"对于每个问题,每个人都有自己的解决方案意见"(让我们嫌妒 angular)* +- *"我不理解这个大型单体块中发生了什么"* + +### 隐式和不可控制的后果 + +开发/重构过程中有很多隐式的副作用 *("一切都依赖于一切")* + +**示例:** + +- *"feature 导入 feature"* +- *"我更新了一个页面的 store,另一个页面的功能就失效了"* +- *"逻辑散布在整个应用程序中,无法追踪哪里是开始,哪里是结束"* + +### 不可控制的逻辑重用 + +很难重用/修改现有逻辑 + +同时,通常存在[两个极端](https://github.com/feature-sliced/documentation/discussions/14): + +- 要么为每个模块完全从头开始编写逻辑 *(在现有代码库中可能存在重复)* +- 要么倾向于将所有实现的模块转移到 `shared` 文件夹,从而创建一个大型的模块转储场 *(其中大多数只在一个地方使用)* + +**示例:** + +- *"我的项目中有 **N** 个相同业务逻辑的实现,我仍然在为此付出代价"* +- *"项目中有 6 个不同的按钮/弹窗/... 组件"* +- *"helpers 的转储场"* + +## 需求 + +因此,提出*理想架构的期望需求*似乎是合乎逻辑的: + +:::note + +无论何处提到"容易",都意味着"对广泛的开发者来说相对容易",因为很明显[不可能为绝对所有人制作理想的解决方案](/docs/about/mission#limitations) + +::: + +### 明确性 + +- 应该**易于掌握和解释**项目及其架构给团队 +- 结构应该反映项目的真实**业务价值** +- 抽象之间必须有明确的**副作用和连接** +- 应该**易于检测重复逻辑**而不干扰独特实现 +- 项目中不应该有**逻辑分散** +- 对于良好的架构,不应该有**太多异构抽象和规则** + +### 控制 + +- 良好的架构应该**加速任务解决和功能引入** +- 应该能够控制项目的开发 +- 应该易于**扩展、修改、删除代码** +- 必须遵守功能的**分解和隔离** +- 系统的每个组件都必须**易于替换和移除** + - *[无需为变更优化][ext-kof-not-modification] - 我们无法预测未来* + - *[更好地为删除优化][ext-kof-but-removing] - 基于已存在的上下文* + +### 适应性 + +- 良好的架构应该适用于**大多数项目** + - *具有现有基础设施解决方案* + - *在开发的任何阶段* +- 不应该依赖于框架和平台 +- 应该能够**轻松扩展项目和团队**,具有开发并行化的可能性 +- 应该易于**适应不断变化的需求和环境** + +## 参见 + +- [(React Berlin Talk) Oleg Isonen - Feature Driven Architecture][ext-kof] +- [(React SPB Meetup #1) Sergey Sova - Feature Slices][ext-slices-spb] +- [(文章) 关于项目模块化][ext-medium] +- [(文章) 关于关注点分离和按功能构建][ext-ryanlanciaux] + +[ext-kof-not-modification]: https://youtu.be/BWAeYuWFHhs?t=1631 +[ext-kof-but-removing]: https://youtu.be/BWAeYuWFHhs?t=1666 + +[ext-slices-spb]: https://t.me/feature_slices +[ext-kof]: https://youtu.be/BWAeYuWFHhs +[ext-medium]: https://alexmngn.medium.com/why-react-developers-should-modularize-their-applications-d26d381854c1 +[ext-ryanlanciaux]: https://ryanlanciaux.com/blog/2017/08/20/a-feature-based-approach-to-react-development/ diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/knowledge-types.md b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/knowledge-types.md new file mode 100644 index 000000000..53ea83fb9 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/knowledge-types.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 3 +sidebar_label: Knowledge types +--- + +# Knowledge types in the project + +The following "types of knowledge" can be distinguished in any project: + +* **Fundamental knowledge** + Knowledge that does not change much over time, such as algorithms, computer science, programming language mechanisms and its APIs. + +* **Technology stack** + Knowledge of the set of technical solutions used in a project, including programming languages, frameworks, and libraries. + +* **Project knowledge** + Knowledge that is specific to the current project and not valuable outside of it. This knowledge is essential for newly-onboarded developers to be able to contribute effectively. + +:::note + +**Feature-Sliced Design** is designed to reduce reliance on "project knowledge", take more responsibility, and make it easier to onboard new team members. + +::: + +## See also {#see-also} + +- [(Video 🇷🇺) Ilya Klimov - On Types of Knowledge][ext-klimov] + +[ext-klimov]: https://youtu.be/4xyb_tA-uw0?t=249 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/naming.md b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/naming.md new file mode 100644 index 000000000..5a86f8ef0 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/naming.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 4 +--- + +# 命名 + +不同的开发者有不同的经验和上下文,当相同的实体被不同地命名时,这可能导致团队中的误解。例如: + +- 用于显示的组件可以被称为 "ui"、"components"、"ui-kit"、"views"… +- 在整个应用程序中重用的代码可以被称为 "core"、"shared"、"app"… +- 业务逻辑代码可以被称为 "store"、"model"、"state"… + +## Feature-Sliced Design 中的命名 {#naming-in-fsd} + +该方法论使用特定的术语,例如: + +- "app"、"process"、"page"、"feature"、"entity"、"shared" 作为 layer 名称, +- "ui"、"model"、"lib"、"api"、"config" 作为 segment 名称。 + +坚持使用这些术语非常重要,以防止团队成员和加入项目的新开发者之间的混淆。使用标准名称也有助于向社区寻求帮助。 + +## 命名冲突 {#when-can-naming-interfere} + +当 FSD 方法论中使用的术语与业务中使用的术语重叠时,可能发生命名冲突: + +- `FSD#process` vs 应用程序中的模拟进程, +- `FSD#page` vs 日志页面, +- `FSD#model` vs 汽车型号。 + +例如,开发者在代码中看到 "process" 这个词时,会花费额外的时间试图弄清楚指的是哪个进程。这样的**冲突可能会破坏开发过程**。 + +当项目术语表包含 FSD 特有的术语时,在与团队和技术不相关的各方讨论这些术语时要格外小心。 + +为了与团队有效沟通,建议使用缩写 "FSD" 作为方法论术语的前缀。例如,在谈论进程时,您可能会说:"我们可以将这个进程放在 FSD features layer 上。" + +相反,在与非技术利益相关者沟通时,最好限制使用 FSD 术语,并避免提及代码库的内部结构。 + +## 参见 {#see-also} + +- [(讨论) 命名的适应性][disc-src] +- [(讨论) Entity 命名调查][disc-naming] +- [(讨论) "processes" vs "flows" vs ...][disc-processes] +- [(讨论) "model" vs "store" vs ...][disc-model] + +[disc-model]: https://github.com/feature-sliced/documentation/discussions/68 +[disc-naming]: https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-464894 +[disc-processes]: https://github.com/feature-sliced/documentation/discussions/20 +[disc-src]: https://github.com/feature-sliced/documentation/discussions/16 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/needs-driven.md b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/needs-driven.md new file mode 100644 index 000000000..9a06ff0bb --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/needs-driven.md @@ -0,0 +1,162 @@ +--- +sidebar_position: 2 +--- + +# Needs driven + +:::note TL;DR + +— _Can't you formulate the goal that the new feature will solve? Or maybe the problem is that the task itself is not formulated? **The point is also that the methodology helps to pull out the problematic definition of tasks and goals**_ + +— _project does not live in static - requirements and functionality are constantly changing. Over time, the code turns into mush, because at the start the project was designed only for the initial impression of wishes. **And the task of a good architecture is also to be sharpened for changing development conditions.**_ + +::: + + + + +## Why? + +To choose a clear name for an entity and understand its components, **you need to clearly understand what task will be solved with the help of all this code.** + +> _@sergeysova: During development, we try to give each entity or function a name that clearly reflects the intentions and meaning of the code being executed._ + +_After all, without understanding the task, it is impossible to write the right tests that cover the most important cases, put down errors that help the user in the right places, even it is banal not to interrupt the user's flow because of fixable non-critical errors._ + +## What tasks are we talking about? + +Frontend develops applications and interfaces for end users, so we solve the tasks of these consumers. + +When a person comes to us, **he wants to solve some of his pain or close a need.** + +_The task of managers and analysts is to formulate this need, and implement developers taking into account the features of web development (loss of communication, backend error, typo, missed the cursor or finger)._ + +**This very goal, with which the user came, is the task of the developers.** + +> _One small solved problem is a feature in the Feature-Sliced Design methodology — you need to cut the entire scope of project tasks into small goals._ + +## How does this affect development? + +### Task decomposition + +When a developer begins to implement a task, in order to simplify the understanding and support of the code, he mentally **cuts it into stages**: + +* first _split into top-level entities_ and _implement them_, +* then these entities _split into smaller ones_ +* and so on + +_In the process of splitting into entities, the developer is forced to give them a name that would clearly reflect his idea and help to understand what task the code solves when reading the listing_ +_At the same time, we do not forget that we are trying to help the user reduce pain or realize needs_ + +### Understanding the essence of the task + +But to give a clear name to an entity, **the developer must know enough about its purpose** + +* how is he going to use this entity, +* what part of the user's task does it implement, where else can this entity be applied, +* in what other tasks can it participate, +* and so on + +It is not difficult to draw a conclusion: **while the developer will reflect on the name of entities within the framework of the methodology, he will be able to find poorly formulated tasks even before writing the code.** + +> How to give a name to an entity if you do not understand well what tasks it can solve, how can you even divide a task into entities if you do not understand it well? + +## How to formulate it? + +**To formulate a task that is solved by features, you need to understand the task itself**, and this is already the responsibility of the project manager and analysts. + +_The methodology can only tell the developer what tasks the product manager should pay close attention to._ + +> _@sergeysova: the Whole frontend is primarily a display of information, any component in the first turn, displays, and then the task "to show the user something" has no practical value._ +> +> _Even without taking into account the specifics of the frontend can ask, "why do I have to show you", so you can continue to ask until't get out of pain or the need of the consumer._ + +As soon as we were able to get to the basic needs or pains, we can go back and figure out **how exactly your product or service can help the user with his goals** + +Any new task in your tracker is aimed at solving business problems, and the business tries to solve the user's tasks at the same time earning money on it. This means that each task has certain goals, even if they are not spelled out in the description text. + +_**The developer must clearly understand what goal this or that task is pursuing**, but not every company can afford to build processes perfectly, although this is a separate conversation, nevertheless, the developer may well "ping" the right managers himself to find out this and do his part of the work effectively._ + +## And what is the benefit? + +Now let's look at the whole process from beginning to end. + +### 1. Understanding user tasks + +When a developer understands his pain and how the business closes them, he can offer solutions that are not available to the business due to the specifics of web development. + +> But of course, all this can work only if the developer is not indifferent to what he is doing and for what, otherwise _why then the methodology and some approaches?_ + +### 2. Structuring and ordering + +With the understanding of tasks comes **a clear structure both in the head and in the tasks along with the code** + +### 3. Understanding the feature and its components + +**One feature is one useful functionality for the user** + +* When several features are implemented in one feature, this is **a violation of borders** +* The feature can be indivisible and growing - **and this is not bad** +* **Bad** - when the feature does not answer the question _"What is the business value for the user?"_ +* There can be no "map-office" feature + * But `booking-meeting-on-the-map`, `search-for-an-employee`, `change-of-workplace` - **yes** + +> _@sergeysova: The point is that the feature contains only code that implements the functionality itself_, without unnecessary details and internal solutions (ideally)* +> +> *Open the feature code **and see only what relates to the task** - no more* + +### 4. Profit + +Business very rarely turns its course radically in the other direction, which means **the reflection of business tasks in the frontend application code is a very significant profit.** + +_Then you don't have to explain to each new team member what this or that code does, and in general why it was added - **everything will be explained through the business tasks that are already reflected in the code.**_ + +> What is called ["Business Language" in Domain Driven Development][ext-ubiq-lang] + +--- + +## Back to reality + +If business processes are understood and good names are given at the design stage - _then it is not particularly problematic to transfer this understanding and logic to the code._ + +**However, in practice**, tasks and functionality are usually developed "too" iteratively and (or) there is no time to think through the design. + +**As a result, the feature makes sense today, and if you expand this feature in a month, you can rewrite the gender of the project.** + +> *[[From the discussion][disc-src]]: The developer tries to think 2-3 steps ahead, taking into account future wishes, but here he rests on his own experience* +> +> _Burns experience engineer usually immediately looking 10 steps ahead, and understand where one feature to divide and combine with the other_ +> +> _But sometimes that comes the task which had to face the experience, and nowhere to take the understanding of how literacy to decompose, with the least unfortunate consequences in the future_ + +## The role of methodology + +**The methodology helps to solve the problems of developers, so that it is easier to solve the problems of users.** + +There is no solution to the problems of developers only for the sake of developers + +But in order for the developer to solve his tasks, **you need to understand the user's tasks** - on the contrary, it will not work + +### Methodology requirements + +It becomes clear that you need to identify at least two requirements for **Feature-Sliced Design**: + +1. The methodology should tell **how to create features, processes and entities** + + * Which means it should clearly explain _how to divide the code between them_, which means that the naming of these entities should also be laid down in the specification. + +2. The methodology should help the architecture **[easily adapt to the changing requirements of the project][refs-arch--adaptability]** + +## See also + +* [(Post) Stimulation for a clear formulation of tasks (+ discussion)][disc-src] + > _**The current article** is an adaptation of this discussion, you can read the full uncut version at the link_ +* [(Discussion) How to break the functionality and what it is][tg-src] +* [(Article) "How to better organize your applications"][ext-medium] + +[refs-arch--adaptability]: architecture#adaptability + +[ext-medium]: https://alexmngn.medium.com/how-to-better-organize-your-react-applications-2fd3ea1920f1 +[disc-src]: https://t.me/sergeysova/318 +[tg-src]: https://t.me/atomicdesign/18972 +[ext-ubiq-lang]: https://thedomaindrivendesign.io/developing-the-ubiquitous-language diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/signals.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/signals.mdx new file mode 100644 index 000000000..2a85ecc80 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/about/understanding/signals.mdx @@ -0,0 +1,21 @@ +--- +sidebar_position: 5 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# Signals of architecture + + + +> If there is a limitation on the part of the architecture, then there are obvious reasons for this, and consequences if they are ignored + +> The methodology and architecture gives signals, and how to deal with it depends on what risks you are ready to take on and what is most suitable for your team) + +## See also + +- [(Thread) About signals from architecture and dataflow](https://t.me/feature_sliced/2070) +- [(Thread) About the fundamental nature of architecture](https://t.me/feature_sliced/2492) +- [(Thread) About highlighting weak points](https://t.me/feature_sliced/3979) +- [(Thread) How to understand that the data model is swollen](https://t.me/feature_sliced/4228) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/branding.md b/i18n/zh/docusaurus-plugin-content-docs/current/branding.md new file mode 100644 index 000000000..1efe978a3 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/branding.md @@ -0,0 +1,82 @@ +import useBaseUrl from '@docusaurus/useBaseUrl'; + +# 品牌指南 + +FSD 的视觉身份基于其核心概念:`分层`、`切片式自包含部分`、`部分和组合`、`分段`。 +但我们也倾向于设计简单、美丽的身份,它应该传达 FSD 的哲学并易于识别。 + +**请按原样使用 FSD 的身份,不要更改,但可以使用我们的资产以方便您使用。**此品牌指南将帮助您正确使用 FSD 的身份。 + +:::caution 兼容性 + +FSD 以前有[另一个遗留身份](https://drive.google.com/drive/folders/11Y-3qZ_C9jOFoW2UbSp11YasOhw4yBdl?usp=sharing)。旧设计不能代表方法论的核心概念。此外,它是作为纯粹的草稿创建的,应该被实现。 + +为了兼容和长期使用品牌,我们在一年内(2021-2022)进行了谨慎的重新品牌化。**这样您在使用 FSD 身份时可以放心 🍰** + +*但请优先使用实际身份,而不是旧的!* + +::: + +## 标题 + +- ✅ **正确:** `Feature-Sliced Design`、`FSD` +- ❌ **错误:** `Feature-Sliced`、`Feature Sliced`、`FeatureSliced`、`feature-sliced`、`feature sliced`、`FS` + +## Emoji + +蛋糕 🍰 图像很好地代表了 FSD 的核心概念,所以它被选为我们的标志性 emoji + +> 示例:*"🍰 前端项目的架构设计方法论"* + +## Logo 和调色板 + +FSD 有几种适用于不同上下文的 logo 变体,但建议优先使用 **primary** + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
主题Logo (Ctrl/Cmd + 点击下载)用法
primary
(#29BEDC, #517AED)
logo-primary在大多数情况下首选
flat
(#3193FF)
logo-flatFor one-color context
monochrome
(#FFF)
logo-monocrhomeFor grayscale context
square
(#3193FF)
logo-squareFor square boundaries
+ +## Banners & Schemes + +banner-primary +banner-monochrome + +## Social Preview + +Work in progress... + +## Presentation template + +Work in progress... + +## See also + +- [Discussion (github)](https://github.com/feature-sliced/documentation/discussions/399) +- [History of development with references (figma)](https://www.figma.com/file/RPphccpoeasVB0lMpZwPVR/FSD-Brand?node-id=0%3A1) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/get-started/cheatsheet.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/cheatsheet.mdx new file mode 100644 index 000000000..2d999f1da --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/cheatsheet.mdx @@ -0,0 +1,34 @@ +--- +# sidebar_position: 3 +unlisted: true +--- + +# 分解速查表 + +在您决定如何分解 UI 时,将此作为快速参考。下面还提供了 PDF 版本,您可以打印出来放在枕头下。 + +## 选择层 + +[下载 PDF](/files/choosing-a-layer-en.pdf) + +![所有层的定义和自检问题](/img/choosing-a-layer-en.jpg) + +## 示例 + +### Tweet + +![decomposed-tweet-bordered-bgLight](/img/decompose-twitter.png) + +### GitHub + +![decomposed-github-bordered](/img/decompose-github.jpg) + +## 另请参阅 +- [(Thread) features 和 entities 的一般逻辑](https://t.me/feature_sliced/4262) +- [(Thread) 臃肿逻辑的分解](https://t.me/feature_sliced/4210) +- [(Thread) 关于在分解过程中理解责任区域](https://t.me/feature_sliced/4088) +- [(Thread) Product List widget 的分解](https://t.me/feature_sliced/3828) +- [(Article) 逻辑分解的不同方法](https://www.pluralsight.com/guides/how-to-organize-your-react-+-redux-codebase) +- [(Thread) 关于 features 和 entities 之间的区别](https://t.me/feature_sliced/3776) +- [(Thread) 关于事物和实体之间的区别 (2)](https://t.me/feature_sliced/3248) +- [(Thread) 关于分解标准的应用](https://t.me/feature_sliced/3833) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/get-started/faq.md b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/faq.md new file mode 100644 index 000000000..eb1e0e0ca --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/faq.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 20 +pagination_next: guides/index +--- + +# 常见问题 + +:::info + +您可以在我们的 [Telegram 聊天][telegram]、[Discord 社区][discord] 和 [GitHub Discussions][github-discussions] 中提问。 + +::: + +### 有工具包或代码检查器吗? + +有!我们有一个名为 [Steiger][ext-steiger] 的代码检查器来检查您项目的架构,以及通过 CLI 或 IDE 的[文件夹生成器][ext-tools]。 + +### 在哪里存储页面的布局/模板? + +如果您需要纯标记布局,您可以将它们保存在 `shared/ui` 中。如果您需要在内部使用更高的 layers,有几个选项: + +- 也许您根本不需要布局?如果布局只有几行,在每个页面中复制代码而不是试图抽象它可能是合理的。 +- 如果您确实需要布局,您可以将它们作为单独的 widgets 或 pages,并在 App 中的路由配置中组合它们。嵌套路由是另一个选项。 + +### feature 和 entity 之间有什么区别? + +_entity_ 是您的应用程序正在处理的现实生活概念。_feature_ 是为您的应用程序用户提供现实生活价值的交互,是人们想要对您的 entities 做的事情。 + +有关更多信息和示例,请参阅 [slices][reference-entities] 的参考页面。 + +### 我可以将 pages/features/entities 嵌入彼此吗? + +可以,但这种嵌入应该在更高的 layers 中发生。例如,在 widget 内部,您可以导入两个 features,然后将一个 feature 作为 props/children 插入到另一个 feature 中。 + +您不能从一个 feature 导入另一个 feature,这被 [**layers 上的导入规则**][import-rule-layers] 禁止。 + +### Atomic Design 怎么办? + +该方法论的当前版本不要求也不禁止将 Atomic Design 与 Feature-Sliced Design 一起使用。 + +例如,Atomic Design [可以很好地应用](https://t.me/feature_sliced/1653)于模块的 `ui` segment。 + +### 有关于 FSD 的有用资源/文章等吗? + +有!https://github.com/feature-sliced/awesome + +### 为什么我需要 Feature-Sliced Design? + +它帮助您和您的团队在主要价值组件方面快速概览项目。标准化架构有助于加快入职速度并解决关于代码结构的争议。请参阅[动机][motivation]页面了解更多关于为什么创建 FSD 的信息。 + +### 新手开发者需要架构/方法论吗? + +更倾向于需要 + +*通常,当您独自设计和开发项目时,一切都很顺利。但如果开发过程中有暂停,团队中添加了新的开发者 - 那么问题就会出现* + +### 如何处理授权上下文? + +在[这里](/docs/guides/examples/auth)有答案 + +[ext-steiger]: https://github.com/feature-sliced/steiger +[ext-tools]: https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools +[import-rule-layers]: /docs/reference/layers#import-rule-on-layers +[reference-entities]: /docs/reference/layers#entities +[motivation]: /docs/about/motivation +[telegram]: https://t.me/feature_sliced +[discord]: https://discord.gg/S8MzWTUsmp +[github-discussions]: https://github.com/feature-sliced/documentation/discussions diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/get-started/index.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/index.mdx new file mode 100644 index 000000000..50a86a57d --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/index.mdx @@ -0,0 +1,38 @@ +--- +hide_table_of_contents: true +pagination_prev: intro +--- + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { RocketOutlined, PlaySquareOutlined, QuestionCircleOutlined } from "@ant-design/icons"; + +# 🚀 开始使用 + +

+欢迎!本节将帮助您熟悉 Feature-Sliced Design 的应用和方法论的基础知识。您还将了解该方法论的主要优势以及创建它的原因。 +

+ + + + +{/* */} diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/get-started/overview.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/overview.mdx new file mode 100644 index 000000000..85e59b90a --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/overview.mdx @@ -0,0 +1,143 @@ +--- +sidebar_position: 1 +--- + +# 概览 + +**Feature-Sliced Design**(FSD)是一种用于构建前端应用程序的架构方法论。简单来说,它是组织代码的规则和约定的汇编。该方法论的主要目的是在不断变化的业务需求面前,使项目更加易于理解和稳定。 + +除了一系列约定外,FSD 还是一个工具链。我们有一个 [代码检查器][ext-steiger] 来检查您项目的架构,通过 CLI 或 IDE 的[文件夹生成器][ext-tools],以及丰富的[示例][examples]库。 + +## 它适合我吗? {#is-it-right-for-me} + +FSD 可以在任何规模的项目和团队中实施。如果您的项目符合以下条件,那么它就适合您: + +- 您正在做**前端**开发(网页、移动端、桌面端等 UI) +- 您正在构建一个**应用程序**,而不是一个库 + +就是这样!对于您使用的编程语言、UI 框架或状态管理器没有任何限制。您也可以逐步采用 FSD,在 monorepos 中使用它,并通过将应用程序分解为包并在其中单独实施 FSD 来扩展到很大的长度。 + +如果您已经有了一个架构并正在考虑切换到 FSD,请确保当前的架构在您的团队中**造成了麻烦**。例如,如果您的项目变得过于庞大和相互连接,无法高效地实现新功能,或者如果您期望有很多新成员加入团队。如果当前的架构运作良好,也许不值得更改。但如果您确实决定迁移,请参阅[迁移][migration]部分获取指导。 + +## 基本示例 {#basic-example} + +这里是一个实现了 FSD 的简单项目: + +- `📁 app` +- `📁 pages` +- `📁 shared` + +这些顶级文件夹被称为_层_。让我们更深入地看看: + +- `📂 app` + - `📁 routes` + - `📁 analytics` +- `📂 pages` + - `📁 home` + - `📂 article-reader` + - `📁 ui` + - `📁 api` + - `📁 settings` +- `📂 shared` + - `📁 ui` + - `📁 api` + +`📂 pages` 内的文件夹被称为_切片_。它们按领域分割层(在这种情况下,按页面分割)。 + +`📂 app`、`📂 shared` 和 `📂 pages/article-reader` 内的文件夹被称为_段_,它们按技术目的分割切片(或层),即代码的用途。 + +## 概念 {#concepts} + +Layers、slices 和 segments 形成这样的层次结构: + +
+ ![Hierarchy of FSD concepts, described below](/img/visual_schema.jpg) + +
+

上图显示:三个支柱,从左到右分别标记为 "Layers"、"Slices" 和 "Segments"。

+

"Layers" 支柱包含七个从上到下排列的部分,分别标记为 "app"、"processes"、"pages"、"widgets"、"features"、"entities" 和 "shared"。"processes" 部分被划掉了。"entities" 部分连接到第二个支柱 "Slices",表示第二个支柱是 "entities" 的内容。

+

"Slices" 支柱包含三个从上到下排列的部分,分别标记为 "user"、"post" 和 "comment"。"post" 部分以同样的方式连接到第三个支柱 "Segments",表示它是 "post" 的内容。

+

"Segments" 支柱包含三个从上到下排列的部分,分别标记为 "ui"、"model" 和 "api"。

+
+
+ +### Layers {#layers} + +Layers 在所有 FSD 项目中都是标准化的。您不必使用所有的 layers,但它们的名称很重要。目前有七个(从上到下): + +1. **App** — 使应用程序运行的一切 — 路由、入口点、全局样式、providers。 +2. **Processes**(已废弃)— 复杂的跨页面场景。 +3. **Pages** — 完整页面或嵌套路由中页面的大部分。 +4. **Widgets** — 大型自包含的功能或 UI 块,通常提供整个用例。 +5. **Features** — 整个产品功能的_可重用_实现,即为用户带来业务价值的操作。 +6. **Entities** — 项目处理的业务实体,如 `user` 或 `product`。 +7. **Shared** — 可重用功能,特别是当它与项目/业务的具体细节分离时,但不一定如此。 + +:::warning + +Layers **App** 和 **Shared** 与其他 layers 不同,它们没有 slices,直接分为 segments。 + +然而,所有其他 layers — **Entities**、**Features**、**Widgets** 和 **Pages**,保持您必须首先创建 slices 的结构,在其中创建 segments。 + +::: +Layers 的技巧是一个 layer 上的模块只能了解并从严格位于下方的 layers 的模块中导入。 + +### Slices {#slices} + +接下来是 slices,它们按业务领域分割代码。您可以自由选择它们的名称,并根据需要创建任意数量。Slices 通过将逻辑相关的模块保持在一起,使您的代码库更容易导航。 + +Slices 不能使用同一 layer 上的其他 slices,这有助于实现高聚合性和低耦合性。 + +### Segments {#segments} + +Slices 以及 layers App 和 Shared 由 segments 组成,segments 按代码的目的对代码进行分组。Segment 名称不受标准约束,但有几个最常见目的的传统名称: + +- `ui` — 与 UI 显示相关的一切:UI 组件、日期格式化程序、样式等。 +- `api` — 后端交互:请求函数、数据类型、mappers 等。 +- `model` — 数据模型:schemas、interfaces、stores 和业务逻辑。 +- `lib` — 此 slice 上其他模块需要的库代码。 +- `config` — 配置文件和 feature flags。 + +通常这些 segments 对于大多数 layers 来说已经足够,您只会在 Shared 或 App 中创建自己的 segments,但这不是一个规则。 + +## 优势 {#advantages} + +- **统一性** + 由于结构是标准化的,项目变得更加统一,这使得团队新成员的入职更加容易。 + +- **面对变化和重构的稳定性** + 一个 layer 上的模块不能使用同一 layer 上的其他模块,或者上层的 layers。 + 这允许您进行独立的修改,而不会对应用程序的其余部分产生不可预见的后果。 + +- **可控的逻辑重用** + 根据 layer,您可以使代码非常可重用或非常本地化。 + 这在遵循 **DRY** 原则和实用性之间保持平衡。 + +- **面向业务和用户需求** + 应用程序被分割为业务领域,并鼓励在命名中使用业务语言,这样您可以在不完全理解项目的所有其他不相关部分的情况下做有用的产品工作。 + +## 渐进式采用 {#incremental-adoption} + +如果您有一个现有的代码库想要迁移到 FSD,我们建议以下策略。我们在自己的迁移经验中发现它很有用。 + +1. 首先逐模块地慢慢塑造 App 和 Shared layers 以创建基础。 + +2. 使用粗略的笔触将所有现有 UI 分布在 Widgets 和 Pages 中,即使它们有违反 FSD 规则的依赖。 + +3. 开始逐渐解决导入违规,并提取 Entities,甚至可能提取 Features。 + +建议在重构时避免添加大型新实体,或者只重构项目的某些部分。 + +## 下一步 {#next-steps} + +- **想要好好掌握如何用 FSD 思维?**查看[Tutorial][tutorial]。 +- **喜欢从示例中学习?**我们在 [Examples][examples] 部分有很多内容。 +- **有问题?**访问我们的 [Telegram 聊天][ext-telegram] 并从社区获得帮助。 + +[tutorial]: /docs/get-started/tutorial +[examples]: /examples +[migration]: /docs/guides/migration/from-custom +[ext-steiger]: https://github.com/feature-sliced/steiger +[ext-tools]: https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools +[ext-telegram]: https://t.me/feature_sliced + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/get-started/tutorial.md b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/tutorial.md new file mode 100644 index 000000000..b57a0b49e --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/get-started/tutorial.md @@ -0,0 +1,2262 @@ +--- +sidebar_position: 2 +--- +# 教程 + +## 第一部分。理论上 + +本教程将检查 Real World App,也称为 Conduit。Conduit 是一个基本的 [Medium](https://medium.com/) 克隆 — 它让您阅读和编写文章,以及对他人的文章进行评论。 + +![Conduit home page](/img/tutorial/realworld-feed-anonymous.jpg) + +这是一个相当小的应用程序,所以我们将保持简单并避免过度分解。整个应用程序很可能只需要三个 layers:**App**、**Pages** 和 **Shared**。如果不是,我们将在过程中引入额外的 layers。准备好了吗? + +### 从列出页面开始 + +如果我们查看上面的截图,我们可以至少假设以下页面: + +- 主页(文章流) +- 登录和注册 +- 文章阅读器 +- 文章编辑器 +- 用户资料查看器 +- 用户资料编辑器(用户设置) + +这些页面中的每一个都将成为 Pages *layer* 上的自己的 *slice*。回忆一下概览中的内容,slices 简单来说就是 layers 内部的文件夹,而 layers 简单来说就是具有预定义名称的文件夹,如 `pages`。 + +因此,我们的 Pages 文件夹将如下所示: + +``` +📂 pages/ + 📁 feed/ + 📁 sign-in/ + 📁 article-read/ + 📁 article-edit/ + 📁 profile/ + 📁 settings/ +``` + +Feature-Sliced Design 与无规则代码结构的关键区别是页面不能相互引用。也就是说,一个页面不能从另一个页面导入代码。这是由于 **layers 上的导入规则**: + +*slice 中的模块(文件)只能在其他 slices 位于严格低于当前的 layers 时才能导入它们。* + +在这种情况下,页面是一个 slice,所以这个页面内部的模块(文件)只能引用下层 layers 的代码,而不能引用同一 layer Pages 的代码。 + +### 仔细查看 feed + +
+ ![Anonymous user’s perspective](/img/tutorial/realworld-feed-anonymous.jpg) +
+ _Anonymous user’s perspective_ +
+
+ +
+ ![Authenticated user’s perspective](/img/tutorial/realworld-feed-authenticated.jpg) +
+ _Authenticated user’s perspective_ +
+
+ +feed 页面上有三个动态区域: + +1. 带有登录状态指示的登录链接 +2. 触发 feed 中过滤的标签列表 +3. 一个/两个文章 feeds,每篇文章都有一个点赞按钮 + +登录链接是所有页面通用的头部的一部分,我们将单独重新访问它。 + +#### 标签列表 + +要构建标签列表,我们需要获取可用的标签,将每个标签渲染为芯片,并将选中的标签存储在客户端存储中。这些操作分别属于“API 交互”、“用户界面”和“存储”类别。在 Feature-Sliced Design 中,代码使用 *segments* 按目的分离。Segments 是 slices 中的文件夹,它们可以有描述目的的任意名称,但某些目的非常常见,以至于某些 segment 名称有约定: + +- 📂 `api/` 用于后端交互 +- 📂 `ui/` 用于处理渲染和外观的代码 +- 📂 `model/` 用于存储和业务逻辑 +- 📂 `config/` 用于 feature flags、环境变量和其他形式的配置 + +我们将获取标签的代码放入 `api`,标签组件放入 `ui`,存储交互放入 `model`。 + +#### 文章 + +使用相同的分组原则,我们可以将文章 feed 分解为相同的三个 segments: + +- 📂 `api/`: 获取带有点赞数的分页文章;点赞文章 +- 📂 `ui/`: + - 可以在选中标签时渲染额外选项卡的选项卡列表 + - 单个文章 + - 功能分页 +- 📂 `model/`: 当前加载的文章和当前页面的客户端存储(如果需要) + +### 重用通用代码 + +大多数页面在意图上非常不同,但某些东西在整个应用程序中保持不变 — 例如,符合设计语言的 UI 套件,或后端上使用相同认证方法的 REST API 来完成所有事情的约定。由于 slices 旨在被隔离,代码重用由更低的 layer **Shared** 促进。 + +Shared 与其他 layers 不同,它包含 segments 而不是 slices。这样,Shared layer 可以被认为是 layer 和 slice 之间的混合体。 + +通常,Shared 中的代码不是提前计划的,而是在开发过程中提取的,因为只有在开发过程中才能明确哪些代码部分实际上是共享的。然而,记住哪种代码自然属于 Shared 仍然是有帮助的: + +- 📂 `ui/` — the UI kit, pure appearance, no business logic. For example, buttons, modal dialogs, form inputs. +- 📂 `api/` — convenience wrappers around request making primitives (like `fetch()` on the Web) and, optionally, functions for triggering particular requests according to the backend specification. +- 📂 `config/` — parsing environment variables +- 📂 `i18n/` — configuration of language support +- 📂 `router/` — routing primitives and route constants + +这些只是 Shared 中 segment 名称的几个示例,但您可以省略其中任何一个或创建自己的。创建新 segments 时要记住的唯一重要事情是,segment 名称应该描述**目的(为什么),而不是本质(是什么)**。像 "components"、"hooks"、"modals" 这样的名称*不应该*使用,因为它们描述了这些文件是什么,但不能帮助在内部导航代码。这要求团队中的人在这样的文件夹中挖掘每个文件,并且也保持不相关的代码接近,这导致了重构影响的代码区域广泛,从而使代码审查和测试更加困难。 + +### 定义严格的 public API + +在 Feature-Sliced Design 的上下文中,术语 *public API* 指的是 slice 或 segment 声明项目中的其他模块可以从它导入什么。例如,在 JavaScript 中,这可以是一个 `index.js` 文件,从 slice 中的其他文件重新导出对象。这使得在 slice 内部重构代码的自由度成为可能,只要与外部世界的契约(即 public API)保持不变。 + +对于没有 slices 的 Shared layer,通常为每个 segment 定义单独的 public API 比定义 Shared 中所有内容的一个单一索引更方便。这使得从 Shared 的导入按意图自然地组织。对于具有 slices 的其他 layers,情况相反 — 通常每个 slice 定义一个索引并让 slice 决定外部世界未知的自己的 segments 集合更实用,因为其他 layers 通常有更少的导出。 + +我们的 slices/segments 将以以下方式相互出现: + +``` +📂 pages/ + 📂 feed/ + 📄 index + 📂 sign-in/ + 📄 index + 📂 article-read/ + 📄 index + 📁 … +📂 shared/ + 📂 ui/ + 📄 index + 📂 api/ + 📄 index + 📁 … +``` + +像 `pages/feed` 或 `shared/ui` 这样的文件夹内部的任何内容只有这些文件夹知道,其他文件不应该依赖这些文件夹的内部结构。 + +### UI 中的大型重用块 + +早些时候我们记录了要重新访问出现在每个页面上的头部。在每个页面上从头开始重建它是不切实际的,所以想要重用它是很自然的。我们已经有 Shared 来促进代码重用,然而,在 Shared 中放置大型 UI 块有一个警告 — Shared layer 不应该了解上面的任何 layers。 + +在 Shared 和 Pages 之间有三个其他 layers:Entities、Features 和 Widgets。某些项目可能在这些 layers 中有他们在大型可重用块中需要的东西,这意味着我们不能将该可重用块放在 Shared 中,否则它将从上层 layers 导入,这是被禁止的。这就是 Widgets layer 的用武之地。它位于 Shared、Entities 和 Features 之上,所以它可以使用它们所有。 + +在我们的情况下,头部非常简单 — 它是一个静态 logo 和顶级导航。导航需要向 API 发出请求以确定用户当前是否已登录,但这可以通过从 `api` segment 的简单导入来处理。因此,我们将把我们的头部保留在 Shared 中。 + +### 仔细查看带有表单的页面 + +让我们也检查一个用于编辑而不是阅读的页面。例如,文章编写器: + +![Conduit post editor](/img/tutorial/realworld-editor-authenticated.jpg) + +它看起来微不足道,但包含了我们尚未探索的应用程序开发的几个方面 — 表单验证、错误状态和数据持久化。 + +如果我们要构建这个页面,我们会从 Shared 中获取一些输入和按钮,并在此页面的 `ui` segment 中组合一个表单。然后,在 `api` segment 中,我们将定义一个变更请求以在后端创建文章。 + +为了在发送之前验证请求,我们需要一个验证模式,一个好地方是 `model` segment,因为它是数据模型。在那里我们将产生错误消息并使用 `ui` segment 中的另一个组件显示它们。 + +为了改善用户体验,我们还可以持久化输入以防止意外数据丢失。这也是 `model` segment 的工作。 + +### 总结 + +我们已经检查了几个页面并为我们的应用程序概述了初步结构: + +1. Shared layer + 1. `ui` 将包含我们可重用的 UI 套件 + 2. `api` 将包含我们与后端的原始交互 + 3. 其余将根据需要安排 +2. Pages layer — 每个页面都是一个单独的 slice + 1. `ui` 将包含页面本身及其所有部分 + 2. `api` 将包含更专门的数据获取,使用 `shared/api` + 3. `model` 可能包含我们将显示的数据的客户端存储 + +让我们开始构建吧! + +## 第二部分。在代码中 + +现在我们有了计划,让我们付诸实践。我们将使用 React 和 [Remix](https://remix.run)。 + +有一个为此项目准备的模板,从 GitHub 克隆它以获得先机:[https://github.com/feature-sliced/tutorial-conduit/tree/clean](https://github.com/feature-sliced/tutorial-conduit/tree/clean)。 + +使用 `npm install` 安装依赖项并使用 `npm run dev` 启动开发服务器。打开 [http://localhost:3000](http://localhost:3000),您应该看到一个空白应用程序。 + +### 布局页面 + +让我们首先为所有页面创建空白组件。在您的项目中运行以下命令: + +```bash +npx fsd pages feed sign-in article-read article-edit profile settings --segments ui +``` + +这将为每个页面创建像 `pages/feed/ui/` 这样的文件夹和一个索引文件 `pages/feed/index.ts`。 + +### 连接 feed 页面 + +让我们将应用程序的根路由连接到 feed 页面。在 `pages/feed/ui` 中创建一个组件 `FeedPage.tsx` 并将以下内容放入其中: + + +```tsx title="pages/feed/ui/FeedPage.tsx" +export function FeedPage() { + return ( +
+
+
+

conduit

+

A place to share your knowledge.

+
+
+
+ ); +} +``` + +然后在 feed 页面的 public API,`pages/feed/index.ts` 文件中重新导出此组件: + + + +```ts title="pages/feed/index.ts" +export { FeedPage } from "./ui/FeedPage"; +``` + +现在将它连接到根路由。在 Remix 中,路由是基于文件的,路由文件位于 `app/routes` 文件夹中,这与 Feature-Sliced Design 很好地契合。 + +在 `app/routes/_index.tsx` 中使用 `FeedPage` 组件: + +```tsx title="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; +``` + +然后,如果您运行开发服务器并打开应用程序,您应该会看到 Conduit 横幅! + +![The banner of Conduit](/img/tutorial/conduit-banner.jpg) + +### API 客户端 + +为了与 RealWorld 后端通信,让我们在 Shared 中创建一个方便的 API 客户端。创建两个 segments,`api` 用于客户端,`config` 用于像后端基础 URL 这样的变量: + +```bash +npx fsd shared --segments api config +``` + +然后创建 `shared/config/backend.ts`: + +```tsx title="shared/config/backend.ts" +export { mockBackendUrl as backendBaseUrl } from "mocks/handlers"; +``` + +```tsx title="shared/config/index.ts" +export { backendBaseUrl } from "./backend"; +``` + +由于 RealWorld 项目方便地提供了 [OpenAPI 规范](https://github.com/gothinkster/realworld/blob/main/api/openapi.yml),我们可以利用为我们的客户端自动生成的类型。我们将使用 [the `openapi-fetch` package](https://openapi-ts.pages.dev/openapi-fetch/),它附带一个额外的类型生成器。 + +运行以下命令生成最新的 API 类型: + +```bash +npm run generate-api-types +``` + +这将创建一个文件 `shared/api/v1.d.ts`。我们将使用此文件在 `shared/api/client.ts` 中创建一个类型化的 API 客户端: + +```tsx title="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 }); +``` + +```tsx title="shared/api/index.ts" +export { GET, POST, PUT, DELETE } from "./client"; +``` + +### feed 中的真实数据 + +我们现在可以继续向 feed 添加从后端获取的文章。让我们首先实现一个文章预览组件。 + +使用以下内容创建 `pages/feed/ui/ArticlePreview.tsx`: + +```tsx title="pages/feed/ui/ArticlePreview.tsx" +export function ArticlePreview({ article }) { /* TODO */ } +``` + +由于我们用 TypeScript 编写,有一个类型化的 article 对象会很好。如果我们探索生成的 `v1.d.ts`,我们可以看到 article 对象可以通过 `components["schemas"]["Article"]` 获得。所以让我们在 Shared 中创建一个包含我们数据模型的文件并导出模型: + +```tsx title="shared/api/models.ts" +import type { components } from "./v1"; + +export type Article = components["schemas"]["Article"]; +``` + +```tsx title="shared/api/index.ts" +export { GET, POST, PUT, DELETE } from "./client"; + +export type { Article } from "./models"; +``` + +现在我们可以回到文章预览组件并用数据填充标记。使用以下内容更新组件: + +```tsx title="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} +
  • + ))} +
+ +
+ ); +} +``` + +点赞按钮目前不做任何事情,我们将在到达文章阅读器页面并实现点赞功能时修复它。 + +现在我们可以获取文章并渲染出一堆这些卡片。在 Remix 中获取数据是通过 *loaders* 完成的 — 服务器端函数,获取页面所需的确切内容。Loaders 代表页面与 API 交互,所以我们将它们放在页面的 `api` segment 中: + +```tsx title="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 }); +}; +``` + +要将它连接到页面,我们需要从路由文件中以名称 `loader` 导出它: + +```tsx title="pages/feed/index.ts" +export { FeedPage } from "./ui/FeedPage"; +export { loader } from "./api/loader"; +``` + +```tsx title="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; +``` + +最后一步是在 feed 中渲染这些卡片。使用以下代码更新您的 `FeedPage`: + +```tsx title="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) => ( + + ))} +
+
+
+
+ ); +} +``` + +### 按标签过滤 + +关于标签,我们的工作是从后端获取它们并存储当前选中的标签。我们已经知道如何进行获取 — 这是来自 loader 的另一个请求。我们将使用来自已安装的 `remix-utils` 包的便利函数 `promiseHash`。 + +使用以下代码更新 loader 文件 `pages/feed/api/loader.ts`: + +```tsx title="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")), + }), + ); +}; +``` + +您可能会注意到我们将错误处理提取到一个通用函数 `throwAnyErrors` 中。它看起来非常有用,所以我们可能希望稍后重用它,但现在让我们先留意一下。 + +现在,到标签列表。它需要是交互式的 — 点击标签应该使该标签被选中。按照 Remix 约定,我们将使用 URL 搜索参数作为我们选中标签的存储。让浏览器处理存储,而我们专注于更重要的事情。 + +使用以下代码更新 `pages/feed/ui/FeedPage.tsx`: + +```tsx title="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) => ( + + ))} +
+ +
+
+
+
+
+ ); +} +``` + +然后我们需要在我们的 loader 中使用 `tag` 搜索参数。将 `pages/feed/api/loader.ts` 中的 `loader` 函数更改为以下内容: + +```tsx title="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")), + }), + ); +}; +``` + +就是这样,不需要 `model` segment。Remix 非常整洁。 + +### 分页 + +以类似的方式,我们可以实现分页。随意自己尝试一下或直接复制下面的代码。反正没有人会判断您。 + +```tsx title="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")), + }), + ); +}; +``` + +```tsx title="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) => ( + + ))} +
+ +
+
+
+
+
+ ); +} +``` + +这样也完成了。还有选项卡列表可以类似地实现,但让我们等到实现身份验证时再处理。说到这个! + +### 身份验证 + +身份验证涉及两个页面 — 一个用于登录,另一个用于注册。它们大部分相同,所以将它们保持在同一个 slice `sign-in` 中是有意义的,这样它们可以在需要时重用代码。 + +在 `pages/sign-in` 的 `ui` segment 中创建 `RegisterPage.tsx`,内容如下: + +```tsx title="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}
  • + ))} +
+ )} + +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ ); +} +``` + +我们现在有一个损坏的导入要修复。它涉及一个新的 segment,所以创建它: + +```bash +npx fsd pages sign-in -s api +``` + +然而,在我们可以实现注册的后端部分之前,我们需要一些供 Remix 处理会话的基础设施代码。这放在 Shared 中,以防其他页面需要它。 + +将以下代码放入 `shared/api/auth.server.ts`。这高度特定于 Remix,所以不要太担心,只需复制粘贴: + +```tsx title="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; +} +``` + +同时从旁边的 `models.ts` 文件中导出 `User` 模型: + +```tsx title="shared/api/models.ts" +import type { components } from "./v1"; + +export type Article = components["schemas"]["Article"]; +export type User = components["schemas"]["User"]; +``` + +在此代码能够工作之前,需要设置 `SESSION_SECRET` 环境变量。在项目根目录中创建一个名为 `.env` 的文件,写入 `SESSION_SECRET=`,然后在键盘上随意敲击一些键来创建一个长的随机字符串。您应该得到类似这样的东西: + +```bash title=".env" +SESSION_SECRET=dontyoudarecopypastethis +``` + +最后,向 public API 添加一些导出以使用此代码: + +```tsx title="shared/api/index.ts" +export { GET, POST, PUT, DELETE } from "./client"; + +export type { Article } from "./models"; + +export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; +``` + +现在我们可以编写与 RealWorld 后端通信以实际进行注册的代码。我们将其保存在 `pages/sign-in/api` 中。创建一个名为 `register.ts` 的文件,并将以下代码放入其中: + +```tsx title="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: "/", + }); + } +}; +``` + +```tsx title="pages/sign-in/index.ts" +export { RegisterPage } from './ui/RegisterPage'; +export { register } from './api/register'; +``` + +几乎完成了!只需要将页面和操作连接到 `/register` 路由。在 `app/routes` 中创建 `register.tsx`: + +```tsx title="app/routes/register.tsx" +import { RegisterPage, register } from "pages/sign-in"; + +export { register as action }; + +export default RegisterPage; +``` + +现在如果您转到 [http://localhost:3000/register](http://localhost:3000/register),您应该能够创建用户!应用程序的其余部分还不会对此做出反应,我们将立即解决这个问题。 + +以非常类似的方式,我们可以实现登录页面。尝试一下或直接获取代码并继续: + +```tsx title="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: "/", + }); + } +}; +``` + +```tsx title="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}
  • + ))} +
+ )} + +
+
+ +
+
+ +
+ +
+
+
+
+
+ ); +} +``` + +```tsx title="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'; +``` + +```tsx title="app/routes/login.tsx" +import { SignInPage, signIn } from "pages/sign-in"; + +export { signIn as action }; + +export default SignInPage; +``` + +现在让我们给用户一种实际达到这些页面的方法。 + +### 头部 + +正如我们在第一部分中讨论的,应用程序头部通常放在 Widgets 或 Shared 中。我们将其放在 Shared 中,因为它非常简单,所有业务逻辑都可以保持在它之外。让我们为它创建一个地方: + +```bash +npx fsd shared ui +``` + +现在创建 `shared/ui/Header.tsx`,内容如下: + +```tsx title="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 ( + + ); +} +``` + +从 `shared/ui` 导出此组件: + +```tsx title="shared/ui/index.ts" +export { Header } from "./Header"; +``` + +在头部中,我们依赖保存在 `shared/api` 中的上下文。也创建它: + +```tsx title="shared/api/currentUser.ts" +import { createContext } from "react"; + +import type { User } from "./models"; + +export const CurrentUser = createContext(null); +``` + +```tsx title="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"; +``` + +现在让我们将头部添加到页面。我们希望它出现在每一个页面上,所以简单地将其添加到根路由并用 `CurrentUser` 上下文提供者包装 outlet(页面将被渲染的地方)是有意义的。这样我们的整个应用程序以及头部都可以访问当前用户对象。我们还将添加一个 loader 来实际从 cookies 中获取当前用户对象。将以下内容放入 `app/root.tsx`: + +```tsx title="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 ( + + + + + + + + + + + + + +
+ + + + + + + + ); +} +``` + +在这一点,您应该在主页上得到以下结果: + +
+ ![The feed page of Conduit, including the header, the feed, and the tags. The tabs are still missing.](/img/tutorial/realworld-feed-without-tabs.jpg) + +
The feed page of Conduit, including the header, the feed, and the tags. The tabs are still missing.
+
+ +### 选项卡 + +现在我们可以检测身份验证状态,让我们也快速实现选项卡和帖子点赞来完成 feed 页面。我们需要另一个表单,但这个页面文件正在变得有点大,所以让我们将这些表单移动到相邻的文件中。我们将创建 `Tabs.tsx`、`PopularTags.tsx` 和 `Pagination.tsx`,内容如下: + +```tsx title="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")} + +
  • + )} +
+
+
+ ); +} +``` + +```tsx title="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) => ( + + ))} +
+ +
+ ); +} +``` + +```tsx title="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} +
  • + ) : ( +
  • + +
  • + ), + )} +
+ + ); +} +``` + +现在我们可以显著简化 feed 页面本身: + +```tsx title="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) => ( + + ))} + + +
+ +
+ +
+
+
+
+ ); +} +``` + +我们还需要在 loader 函数中考虑新选项卡: + +```tsx title="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")), + }), + ); +}; +``` + +在我们离开 feed 页面之前,让我们添加一些处理帖子点赞的代码。将您的 `ArticlePreview.tsx` 更改为以下内容: + +```tsx title="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} +
  • + ))} +
+ +
+ ); +} +``` + +此代码将向 `/article/:slug` 发送带有 `_action=favorite` 的 POST 请求以将文章标记为收藏。它还不会工作,但当我们开始处理文章阅读器时,我们也会实现这个功能。 + +这样我们就正式完成了 feed!太好了! + +### 文章阅读器 + +首先,我们需要数据。让我们创建一个 loader: + +```bash +npx fsd pages article-read -s api +``` + +```tsx title="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, + }), + ), + }), + ); +}; +``` + +```tsx title="pages/article-read/index.ts" +export { loader } from "./api/loader"; +``` + +现在我们可以通过创建一个名为 `article.$slug.tsx` 的路由文件将其连接到路由 `/article/:slug`: + +```tsx title="app/routes/article.$slug.tsx" +export { loader } from "pages/article-read"; +``` + +页面本身由三个主要块组成 — 带有操作的文章头部(重复两次)、文章主体和评论部分。这是页面的标记,它并不特别有趣: + +```tsx title="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} +
  • + ))} +
+
+
+ +
+ +
+ +
+ +
+ +
+
+
+ ); +} +``` + +更有趣的是 `ArticleMeta` 和 `Comments`。它们包含写操作,如点赞文章、留下评论等。要让它们工作,我们首先需要实现后端部分。在页面的 `api` segment 中创建 `action.ts`: + +```tsx title="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: "/" }); + }, + }); +}; +``` + +从 slice 中导出它,然后从路由中导出。趁着这个机会,让我们也连接页面本身: + +```tsx title="pages/article-read/index.ts" +export { ArticleReadPage } from "./ui/ArticleReadPage"; +export { loader } from "./api/loader"; +export { action } from "./api/action"; +``` + +```tsx title="app/routes/article.$slug.tsx" +import { ArticleReadPage } from "pages/article-read"; + +export { loader, action } from "pages/article-read"; + +export default ArticleReadPage; +``` + +现在,尽管我们还没有在阅读器页面上实现点赞按钮,但 feed 中的点赞按钮将开始工作!这是因为它一直在向这个路由发送"点赞"请求。试试看吧。 + +`ArticleMeta` 和 `Comments` 又是一堆表单。我们之前已经做过这个,让我们获取它们的代码并继续: + +```tsx title="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 + +    + + + ) : ( + <> + + +    + + + )} +
+
+ ); +} +``` + +```tsx title="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 && ( + +
+ + +
+
+ )} +
+
+ ))} +
+ ); +} +``` + +这样我们的文章阅读器也完成了!关注作者、点赞帖子和留下评论的按钮现在应该能按预期工作。 + +
+ ![Article reader with functioning buttons to like and follow](/img/tutorial/realworld-article-reader.jpg) + +
Article reader with functioning buttons to like and follow
+
+ +### 文章编辑器 + +这是我们将在本教程中涵盖的最后一个页面,这里最有趣的部分是我们将如何验证表单数据。 + +页面本身,`article-edit/ui/ArticleEditPage.tsx`,将非常简单,额外的复杂性被存储到其他两个组件中: + +```tsx title="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 ( +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+ ); +} +``` + +此页面获取当前文章(除非我们从头开始编写)并填写相应的表单字段。我们之前见过这个。有趣的部分是 `FormErrors`,因为它将接收验证结果并向用户显示。让我们看一下: + +```tsx title="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; +} +``` + +这里我们假设我们的 action 将返回 `errors` 字段,一个人类可读的错误消息数组。我们很快就会讲到 action。 + +另一个组件是标签输入。它只是一个普通的输入字段,附带所选标签的额外预览。这里没什么可看的: + +```tsx title="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} + + ))} +
+ + ); +} +``` + +现在,API 部分。loader 应该查看 URL,如果它包含文章 slug,那意味着我们正在编辑现有文章,应该加载其数据。否则,返回空。让我们创建该 loader: + +```ts title="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 将获取新的字段值,通过我们的数据模式运行它们,如果一切都正确,就将这些更改提交到后端,通过更新现有文章或创建新文章: + +```tsx title="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 }); + } +}; +``` + +模式同时作为 `FormData` 的解析函数,这使我们可以方便地获取干净的字段或只是抛出错误在末尾处理。这里是该解析函数的样子: + +```tsx title="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; + }; +} +``` + +可以说,它有点凗长和重复,但这是我们为人类可读错误付出的代价。这也可以是一个 Zod 模式,例如,但然后我们必须在前端渲染错误消息,这个表单不值得复杂化。 + +最后一步 — 将页面、loader 和 action 连接到路由。由于我们巧妙地支持创建和编辑,我们可以从 `editor._index.tsx` 和 `editor.$slug.tsx` 两者导出相同的东西: + +```tsx title="pages/article-edit/index.ts" +export { ArticleEditPage } from "./ui/ArticleEditPage"; +export { loader } from "./api/loader"; +export { action } from "./api/action"; +``` + +```tsx title="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; +``` + +我们现在完成了!登录并尝试创建一篇新文章。或者“忘记”编写文章并看到验证生效。 + +
+ ![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”.**](/img/tutorial/realworld-article-editor.jpg) + +
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”**.
+
+ +资料和设置页面与文章阅读器和编辑器非常相似,它们留作读者的练习,这就是您 :) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/_category_.yaml b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/_category_.yaml new file mode 100644 index 000000000..f11e3a954 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/_category_.yaml @@ -0,0 +1,2 @@ +label: 示例 +position: 1 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/api-requests.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/api-requests.mdx new file mode 100644 index 000000000..038525e51 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/api-requests.mdx @@ -0,0 +1,141 @@ +--- +sidebar_position: 4 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 处理 API 请求 {#handling-api-requests} + +## 共享 API 请求 {#shared-api-requests} + +首先将通用的 API 请求逻辑放在 `shared/api` 目录中。这使得在应用程序中重用请求变得容易,并有助于更快的原型开发。对于许多项目来说,这就是 API 调用所需的全部内容。 + +典型的文件结构是: +- 📂 shared + - 📂 api + - 📄 client.ts + - 📄 index.ts + - 📂 endpoints + - 📄 login.ts + +`client.ts` 文件集中了您的 HTTP 请求设置。它包装您选择的方法(如 `fetch()` 或 `axios` 实例)并处理常见配置,例如: + +- 后端基础 URL。 +- 默认头部(例如,用于身份验证)。 +- 数据序列化。 + +以下是 `axios` 和 `fetch` 的示例: + + + + +```ts title="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' } +}); +``` + + + +```ts title="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. +}; +``` + + + + +在 `shared/api/endpoints` 中组织您的单个 API 请求函数,按 API 端点分组。 + +:::note + +为了保持示例的重点,我们省略了表单交互和验证。有关 Zod 或 Valibot 等库的详细信息,请参阅[类型验证和 Schemas](/docs/guides/examples/types#type-validation-schemas-and-zod) 文章。 + +::: + +```ts title="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); +} +``` +在 `shared/api` 中使用 `index.ts` 文件来导出您的请求函数。 + +```ts title="shared/api/index.ts" +export { client } from './client'; // 如果您想导出客户端本身 +export { login } from './endpoints/login'; +export type { LoginCredentials } from './endpoints/login'; +``` + +## 特定 Slice 的 API 请求 {#slice-specific-api-requests} + +如果 API 请求仅由特定 slice(如单个页面或功能)使用且不会被重用,请将其放在该 slice 的 api segment 中。这样可以保持特定 slice 的逻辑整齐地包含在内。 + +- 📂 pages + - 📂 login + - 📄 index.ts + - 📂 api + - 📄 login.ts + - 📂 ui + - 📄 LoginPage.tsx + +```ts title="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); +} +``` + +您不需要在页面的公共 API 中导出 `login()` 函数,因为应用程序中的其他地方不太可能需要这个请求。 + +:::note + +避免过早地将 API 调用和响应类型放在 `entities` 层中。后端响应可能与您的前端实体需要的不同。`shared/api` 或 slice 的 `api` segment 中的 API 逻辑允许您适当地转换数据,保持实体专注于前端关注点。 + +::: + +## 使用客户端生成器 {#client-generators} + +如果您的后端有 OpenAPI 规范,像 [orval](https://orval.dev/) 或 [openapi-typescript](https://openapi-ts.dev/) 这样的工具可以为您生成 API 类型和请求函数。将生成的代码放在,例如 `shared/api/openapi` 中。确保包含 `README.md` 来记录这些类型是什么,以及如何生成它们。 + +## 与服务器状态库集成 {#server-state-libraries} + +当使用像 [TanStack Query (React Query)](https://tanstack.com/query/latest) 或 [Pinia Colada](https://pinia-colada.esm.dev/) 这样的服务器状态库时,您可能需要在 slices 之间共享类型或缓存键。将以下内容使用 `shared` 层: + +- API 数据类型 +- 缓存键 +- 通用查询/变更选项 + +有关如何使用服务器状态库的更多详细信息,请参阅 [React Query 文章](/docs/guides/tech/with-react-query) \ No newline at end of file diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/auth.md b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/auth.md new file mode 100644 index 000000000..01c922576 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/auth.md @@ -0,0 +1,173 @@ +--- +sidebar_position: 1 +--- + +# 身份验证 + +广义上,身份验证包含以下步骤: + +1. 从用户获取凭据 +1. 将它们发送到后端 +1. 存储 token 以进行经过身份验证的请求 + +## 如何从用户获取凭据 + +我们假设您的应用程序负责获取凭据。如果您通过 OAuth 进行身份验证,您可以简单地创建一个登录页面,其中包含指向 OAuth 提供商登录页面的链接,然后跳转到[步骤 3](#how-to-store-the-token-for-authenticated-requests)。 + +### 专用登录页面 + +通常,网站有专用的登录页面,您在其中输入用户名和密码。这些页面相当简单,所以不需要分解。登录和注册表单在外观上相当相似,所以它们甚至可以被组合在一个页面中。在 Pages layer 上为您的登录/注册页面创建一个 slice: + +- 📂 pages + - 📂 login + - 📂 ui + - 📄 LoginPage.tsx (or your framework's component file format) + - 📄 RegisterPage.tsx + - 📄 index.ts + - other pages… + +在这里我们创建了两个组件并在 slice 的 index 文件中导出它们。这些组件将包含表单,负责为用户提供可理解的控件来获取他们的凭据。 + +### 登录对话框 + +如果您的应用程序有一个可以在任何页面上使用的登录对话框,请考虑将该对话框设为 widget。这样,您仍然可以避免过多的分解,但可以自由地在任何页面上重用此对话框。 + +- 📂 widgets + - 📂 login-dialog + - 📂 ui + - 📄 LoginDialog.tsx + - 📄 index.ts + - other widgets… + +本指南的其余部分是为专用页面方法编写的,但相同的原则也适用于对话框 widget。 + +### 客户端验证 + +有时,特别是对于注册,执行客户端验证是有意义的,可以让用户快速知道他们犯了错误。验证可以在登录页面的 `model` segment 中进行。使用 schema 验证库,例如 JS/TS 的 [Zod][ext-zod],并将该 schema 暴露给 `ui` segment: + +```ts title="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"], +}); +``` + +然后,在 `ui` segment 中,您可以使用此 schema 来验证用户输入: + +```tsx title="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))}> + + + + + + + + +
+ ) +} +``` + +## 如何将凭据发送到后端 + +创建一个向后端登录端点发出请求的函数。此函数可以使用 mutation 库(例如 TanStack Query)直接在组件代码中调用,也可以作为状态管理器中的副作用调用。如 [API 请求指南][examples-api-requests] 中所述,您可以将请求放在 `shared/api` 中或登录页面的 `api` segment 中。 + +### 双因素认证 + +如果您的应用程序支持双因素认证(2FA),您可能需要重定向到另一个页面,用户可以在其中输入一次性密码。通常,您的 `POST /login` 请求会返回带有标志的用户对象,指示用户已启用 2FA。如果设置了该标志,请将用户重定向到 2FA 页面。 + +由于此页面与登录密切相关,您也可以将其保留在 Pages layer 上的同一个 slice `login` 中。 + +您还需要另一个请求函数,类似于我们上面创建的 `login()`。将它们放在一起,要么在 Shared 中,要么在 `login` 页面的 `api` segment 中。 + +## 如何存储 token 以进行经过身份验证的请求 {#how-to-store-the-token-for-authenticated-requests} + +无论您使用哪种身份验证方案,无论是简单的登录和密码、OAuth 还是双因素认证,最终您都会收到一个 token。应该存储此 token,以便后续请求可以识别自己。 + +Web 应用程序的理想 token 存储是 **cookie** — 它不需要手动 token 存储或处理。因此,cookie 存储几乎不需要从前端架构方面考虑。如果您的前端框架有服务器端(例如 [Remix][ext-remix]),那么您应该将服务器端 cookie 基础设施存储在 `shared/api` 中。在[教程的身份验证部分][tutorial-authentication]中有一个如何使用 Remix 做到这一点的示例。 + +但是,有时 cookie 存储不是一个选项。在这种情况下,您将必须手动存储 token。除了存储 token 之外,您可能还需要设置在 token 过期时刷新 token 的逻辑。使用 FSD,有几个地方可以存储 token,以及几种方法可以使其对应用程序的其余部分可用。 + +### 在 Shared 中 + +这种方法与在 `shared/api` 中定义的 API 客户端配合得很好,因为 token 可以自由地用于其他需要身份验证才能成功的请求函数。您可以让 API 客户端保持状态,无论是使用响应式存储还是简单的模块级变量,并在您的 `login()`/`logout()` 函数中更新该状态。 + +自动 token 刷新可以作为 API 客户端中的中间件实现 — 每次您发出任何请求时都可以执行的东西。它可以这样工作: + +- 认证并存储访问 token 以及刷新 token +- 发出任何需要身份验证的请求 +- 如果请求失败并返回指示 token 过期的状态码,并且存储中有 token,则发出刷新请求,存储新的 token,并重试原始请求 + +One of the drawbacks of this approach is that the logic of managing and refreshing the token doesn't have a dedicated place. This can be fine for some apps or teams, but if the token management logic is more complex, it may be preferable to separate responsibilities of making requests and managing tokens. You can do that by keeping your requests and API client in `shared/api`, but the token store and management logic in `shared/auth`. + +Another drawback of this approach is that if your backend returns an object of your current user's information along with the token, you have to store that somewhere or discard that information and request it again from an endpoint like `/me` or `/users/current`. + +### 在 Entities 中 + +FSD 项目通常有一个用户实体和/或当前用户实体。甚至可以是同一个实体。 + +:::note + +**当前用户**有时也被称为"viewer"或"me"。这是为了区分具有权限和私人信息的单个经过身份验证的用户与具有公开可访问信息的所有用户列表。 + +::: + +To store the token in the User entity, create a reactive store in the `model` segment. That store can contain both the token and the user object. + +Since the API client is usually defined in `shared/api` or spreaded across the entities, the main challenge to this approach is making the token available to other requests that need it without breaking [the import rule on layers][import-rule-on-layers]: + +> A module (file) in a slice can only import other slices when they are located on layers strictly below. + +There are several solutions to this challenge: + +1. **Pass the token manually every time you make a request** + This is the simplest solution, but it quickly becomes cumbersome, and if you don't have type safety, it's easy to forget. It's also not compatible with middlewares pattern for the API client in Shared. +1. **Expose the token to the entire app with a context or a global store like `localStorage`** + The key to retrieve the token will be kept in `shared/api` so that the API client can access it. The reactive store of the token will be exported from the User entity, and the context provider (if needed) will be set up on the App layer. This gives more freedom for designing the API client, however, this creates an implicit dependency on higher layers to provide context. When following this approach, consider providing helpful error messages if the context or `localStorage` are not set up correctly. +1. **Inject the token into the API client every time it changes** + If your store is reactive, you can create a subscription that will update the API client's token store every time the store in the entity changes. This is similar to the previous solution in that they both create an implicit dependency on higher layers, but this one is more imperative ("push"), while the previous one is more declarative ("pull"). + +Once you overcome the challenge of exposing the token that is stored in the entity's model, you can encode more business logic related to token management. For example, the `model` segment can contain logic to invalidate the token after a certain period of time, or to refresh the token when it expires. To actually make requests to the backend, use the `api` segment of the User entity or `shared/api`. + +### In Pages/Widgets (not recommended) + +It is discouraged to store app-wide state like an access token in pages or widgets. Avoid placing your token store in the `model` segment of the login page, instead choose from the first two solutions, Shared or Entities. + +## 登出和 token 失效 + +通常,应用程序没有专门的登出页面,但登出功能仍然非常重要。它包括对后端的经过身份验证的请求和对 token 存储的更新。 + +如果您将所有请求存储在 `shared/api` 中,请将登出请求函数保留在那里,靠近登录函数。否则,请考虑将登出请求函数保留在触发它的按钮旁边。例如,如果您有一个出现在每个页面上并包含登出链接的头部 widget,请将该请求放在该 widget 的 `api` segment 中。 + +token 存储的更新必须从登出按钮的位置触发,比如头部 widget。您可以在该 widget 的 `model` segment 中组合请求和存储更新。 + +### 自动登出 + +不要忘记为登出请求失败或刷新登录 token 请求失败时构建故障保护。在这两种情况下,您都应该清除 token 存储。如果您将 token 保存在 Entities 中,此代码可以放在 `model` segment 中,因为它是纯业务逻辑。如果您将 token 保存在 Shared 中,将此逻辑放在 `shared/api` 中可能会使 segment 膨胀并稀释其目的。如果您注意到您的 API segment 包含几个不相关的东西,请考虑将 token 管理逻辑拆分到另一个 segment 中,例如 `shared/auth`。 + +[tutorial-authentication]: /docs/get-started/tutorial#authentication +[import-rule-on-layers]: /docs/reference/layers#import-rule-on-layers +[examples-api-requests]: /docs/guides/examples/api-requests +[ext-remix]: https://remix.run +[ext-zod]: https://zod.dev + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/autocompleted.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/autocompleted.mdx new file mode 100644 index 000000000..fc36f87c5 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/autocompleted.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 5 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 自动完成 + + + + + +> 关于按层分解 + +## 另请参阅 +- [(讨论) 关于将方法论应用于加载字典的选择](https://github.com/feature-sliced/documentation/discussions/65#discussioncomment-480807) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/browser-api.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/browser-api.mdx new file mode 100644 index 000000000..e9a135403 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/browser-api.mdx @@ -0,0 +1,14 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 浏览器 API + + + +> 关于使用浏览器 API:localStorage、音频 API、蓝牙 API 等。 +> +> 您可以向 [@alex_novi](https://t.me/alex_novich) 询问更多详细信息 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/cms.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/cms.mdx new file mode 100644 index 000000000..808c2c0b1 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/cms.mdx @@ -0,0 +1,22 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# CMS + + + +## 功能可能不同 + +在一些项目中,所有功能都集中在来自服务器的数据中 + +> https://github.com/feature-sliced/documentation/discussions/65#discussioncomment-480785 + +## 如何更正确地使用 CMS 标记 + +> https://t.me/feature_sliced/1557 + +> https://t.me/feature_sliced/1553 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/feedback.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/feedback.mdx new file mode 100644 index 000000000..1014a4328 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/feedback.mdx @@ -0,0 +1,12 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 反馈 + + + +> 错误、警告、通知…… diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/i18n.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/i18n.mdx new file mode 100644 index 000000000..78a3e76d2 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/i18n.mdx @@ -0,0 +1,17 @@ +--- +sidebar_position: 6 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 国际化 + + + +## 在哪里放置它?如何使用它? + +- https://t.me/feature_sliced/4425 +- https://t.me/feature_sliced/2325 +- https://t.me/feature_sliced/1867 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/index.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/index.mdx new file mode 100644 index 000000000..77885004f --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/index.mdx @@ -0,0 +1,36 @@ +--- +hide_table_of_contents: true +--- + +# 示例 + +

+方法论应用的小型实用示例 +

+ +## Main + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { UserSwitchOutlined, LayoutOutlined, FontSizeOutlined } from "@ant-design/icons"; + + + + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/metric.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/metric.mdx new file mode 100644 index 000000000..e8bebaef5 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/metric.mdx @@ -0,0 +1,12 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 指标 + + + +> 关于在应用程序中初始化指标的方法 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/monorepo.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/monorepo.mdx new file mode 100644 index 000000000..113673987 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/monorepo.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 9 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 单体仓库 + + + +> 关于单体仓库的适用性,关于 bff,关于微应用 + +## 另请参阅 + +- [(讨论) 关于单体仓库和插件包](https://github.com/feature-sliced/documentation/discussions/50) +- [(Thread) 关于单体仓库的应用](https://t.me/feature_sliced/2412) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/page-layout.md b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/page-layout.md new file mode 100644 index 000000000..f912aeaf6 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/page-layout.md @@ -0,0 +1,102 @@ +--- +sidebar_position: 3 +--- + +# 页面布局 + +本指南探讨了_页面布局_的抽象 — 当多个页面共享相同的整体结构,仅在主要内容上有所不同时。 + +:::info + +本指南没有涵盖您的问题?请通过在本文上留下反馈(右侧的蓝色按钮)来发布您的问题,我们将考虑扩展本指南! + +::: + +## 简单布局 + +最简单的布局可以在此页面上看到。它有一个带有站点导航的头部、两个侧边栏和一个带有外部链接的页脚。没有复杂的业务逻辑,唯一的动态部分是侧边栏和头部右侧的切换器。这样的布局可以完全放置在 `shared/ui` 或 `app/layouts` 中,通过 props 填充侧边栏的内容: + +```tsx title="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 ( +
+
+ + +
+
+ + {/* 这里是主要内容的位置 */} + +
+
+
    +
  • GitHub
  • +
  • Twitter
  • +
+
+
+ ); +} +``` + +```ts title="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; +} +``` + +侧边栏的代码留给读者作为练习 😉。 + +## 在布局中使用 widgets + +有时您希望在布局中包含某些业务逻辑,特别是如果您使用像 [React Router][ext-react-router] 这样的路由器的深度嵌套路由。然后由于[层上的导入规则][import-rule-on-layers],您无法将布局存储在 Shared 或 Widgets 中: + +> slice 中的模块只能在其他 slices 位于严格较低的层时导入它们。 + +在我们讨论解决方案之前,我们需要讨论这是否首先是一个问题。您_真的需要_那个布局吗?如果需要,它_真的需要_成为一个 Widget 吗?如果所讨论的业务逻辑块在 2-3 个页面上重用,而布局只是该 widget 的一个小包装器,请考虑以下两个选项之一: + +1. **在 App 层内联编写布局,在那里配置路由** + 这对于支持嵌套的路由器来说很棒,因为您可以将某些路由分组并仅对它们应用布局。 + +2. **直接复制粘贴** + 抽象代码的冲动往往被过度高估。对于很少更改的布局来说尤其如此。在某个时候,如果其中一个页面需要更改,您可以简单地进行更改,而不会不必要地影响其他页面。如果您担心有人可能忘记更新其他页面,您总是可以留下描述页面之间关系的注释。 + +如果上述都不适用,有两种解决方案可以在布局中包含 widget: + +1. **使用 render props 或 slots** + 大多数框架允许您从外部传递一段 UI。在 React 中,这被称为 [render props][ext-render-props],在 Vue 中被称为 [slots][ext-vue-slots]。 +2. **将布局移动到 App 层** + 您也可以将布局存储在 App 层,例如在 `app/layouts` 中,并组合您想要的任何 widgets。 + +## 延伸阅读 + +- 在[教程][tutorial]中有一个如何使用 React 和 Remix(相当于 React Router)构建带有身份验证的布局的示例。 + +[tutorial]: /docs/get-started/tutorial +[import-rule-on-layers]: /docs/reference/layers#import-rule-on-layers +[ext-react-router]: https://reactrouter.com/ +[ext-render-props]: https://www.patterns.dev/react/render-props-pattern/ +[ext-vue-slots]: https://vuejs.org/guide/components/slots diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/platforms.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/platforms.mdx new file mode 100644 index 000000000..5928fee6d --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/platforms.mdx @@ -0,0 +1,12 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 桌面/触摸平台 + + + +> 关于方法论在桌面/触摸平台上的应用 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/ssr.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/ssr.mdx new file mode 100644 index 000000000..1d7bbe54c --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/ssr.mdx @@ -0,0 +1,12 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# SSR + + + +> 关于使用方法论实现 SSR diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/theme.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/theme.mdx new file mode 100644 index 000000000..5fdad3804 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/theme.mdx @@ -0,0 +1,19 @@ +--- +sidebar_position: 4 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 主题 + + + +## 我应该把主题和调色板的工作放在哪里? + +> https://t.me/feature_sliced/4410 + +## 关于主题、i18n 逻辑位置的讨论 + +> https://youtu.be/b_nBvHWqxP8?t=133 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/types.md b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/types.md new file mode 100644 index 000000000..1ebc33cc2 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/types.md @@ -0,0 +1,440 @@ +--- +sidebar_position: 2 +--- + +# 类型 + +本指南涉及来自类型化语言(如 TypeScript)的数据类型,并描述它们在 FSD 中的适用位置。 + +:::info + +本指南没有涵盖您的问题?请通过在本文上留下反馈(右侧的蓝色按钮)来发布您的问题,我们将考虑扩展本指南! + +::: + +## 实用类型 + +实用类型是本身没有太多意义的类型,通常与其他类型一起使用。例如: + +
+ +```ts +type ArrayValues = T[number]; +``` + +
+ Source: https://github.com/sindresorhus/type-fest/blob/main/source/array-values.d.ts +
+ +
+ +要使实用类型在整个项目中可用,可以安装像 [`type-fest`][ext-type-fest] 这样的库,或者在 `shared/lib` 中创建您自己的库。确保清楚地指出哪些新类型_应该_添加到此库中,哪些类型_不属于_那里。例如,将其命名为 `shared/lib/utility-types` 并在其中添加一个 README,描述您团队中什么是实用类型。 + +不要高估实用类型的潜在可重用性。仅仅因为它可以被重用,并不意味着它会被重用,因此,并非每个实用类型都需要在 Shared 中。一些实用类型放在需要它们的地方就很好: + +- 📂 pages + - 📂 home + - 📂 api + - 📄 ArrayValues.ts (utility type) + - 📄 getMemoryUsageMetrics.ts (the code that uses the utility type) + +:::warning + +抵制创建 `shared/types` 文件夹或向您的 slices 添加 `types` segment 的诱惑。"types"类别类似于"components"或"hooks"类别,它描述的是内容是什么,而不是它们的用途。Segments 应该描述代码的目的,而不是本质。 + +::: + +## 业务实体及其交叉引用 + +应用程序中最重要的类型之一是业务实体的类型,即您的应用程序处理的现实世界的事物。例如,在音乐流媒体应用程序中,您可能有业务实体 _Song_、_Album_ 等。 + +业务实体通常来自后端,因此第一步是为后端响应添加类型。为每个端点创建一个请求函数,并为此函数的响应添加类型是很方便的。为了额外的类型安全,您可能希望通过像 [Zod][ext-zod] 这样的 schema 验证库来运行响应。 + +例如,如果您将所有请求保存在 Shared 中,您可以这样做: + +```ts title="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>); +} +``` + +您可能会注意到 `Song` 类型引用了不同的实体 `Artist`。这是将请求存储在 Shared 中的好处 — 现实世界的类型通常是相互交织的。如果我们将此函数保存在 `entities/song/api` 中,我们将无法简单地从 `entities/artist` 导入 `Artist`,因为 FSD 通过[层上的导入规则][import-rule-on-layers]限制 slices 之间的交叉导入: + +> slice 中的模块只能在其他 slices 位于严格较低的层时导入它们。 + +有两种方法来处理这个问题: + +1. **参数化您的类型** + 您可以让您的类型接受类型参数作为与其他实体连接的插槽,甚至可以对这些插槽施加约束。例如: + + ```ts title="entities/song/model/song.ts" + interface Song { + id: number; + title: string; + artists: Array; + } + ``` + + 这对某些类型比其他类型效果更好。像 `Cart = { items: Array }` 这样的简单类型可以很容易地与任何类型的产品一起工作。更连接的类型,如 `Country` 和 `City`,可能不那么容易分离。 + +2. **交叉导入(但要正确地做)** + 要在 FSD 中的实体之间进行交叉导入,您可以为每个将要交叉导入的 slice 使用特殊的公共 API。例如,如果我们有实体 `song`、`artist` 和 `playlist`,后两者需要引用 `song`,我们可以在 `song` 实体中使用 `@x` 符号为它们创建两个特殊的公共 API: + + - 📂 entities + - 📂 song + - 📂 @x + - 📄 artist.ts (供 `artist` 实体导入的公共 API) + - 📄 playlist.ts (供 `playlist` 实体导入的公共 API) + - 📄 index.ts (常规公共 API) + + 文件 `📄 entities/song/@x/artist.ts` 的内容类似于 `📄 entities/song/index.ts`: + + ```ts title="entities/song/@x/artist.ts" + export type { Song } from "../model/song.ts"; + ``` + + 然后 `📄 entities/artist/model/artist.ts` 可以像这样导入 `Song`: + + ```ts title="entities/artist/model/artist.ts" + import type { Song } from "entities/song/@x/artist"; + + export interface Artist { + name: string; + songs: Array; + } + ``` + + 通过在实体之间建立显式连接,我们掌握相互依赖关系并保持良好的域分离水平。 + +## 数据传输对象和映射器 {#data-transfer-objects-and-mappers} + +数据传输对象,或 DTO,是一个描述来自后端的数据形状的术语。有时,DTO 可以直接使用,但有时对前端来说不太方便。这就是映射器发挥作用的地方 — 它们将 DTO 转换为更方便的形状。 + +### 在哪里放置 DTO + +如果您在单独的包中有后端类型(例如,如果您在前端和后端之间共享代码),那么只需从那里导入您的 DTO 就完成了!如果您不在后端和前端之间共享代码,那么您需要将 DTO 保存在前端代码库的某个地方,我们将在下面探讨这种情况。 + +如果您的请求函数在 `shared/api` 中,那么 DTO 应该放在那里,就在使用它们的函数旁边: + +```ts title="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>); +} +``` + +如前一节所述,将请求和 DTO 存储在 Shared 中的好处是能够引用其他 DTO。 + +### 在哪里放置映射器 + +映射器是接受 DTO 进行转换的函数,因此,它们应该位于 DTO 定义附近。在实践中,这意味着如果您的请求和 DTO 在 `shared/api` 中定义,那么映射器也应该放在那里: + +```ts title="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; + /** The full title of the song, including the disc number. */ + 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)); +} +``` + +如果您的请求和存储在实体 slices 中定义,那么所有这些代码都会放在那里,请记住 slices 之间交叉导入的限制: + +```ts title="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; +} +``` + +```ts title="entities/song/api/mapper.ts" +import type { SongDTO } from "./dto"; + +export interface Song { + id: string; + title: string; + /** The full title of the song, including the disc number. */ + 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), + }; +} +``` + +```ts title="entities/song/api/listSongs.ts" +import { adaptSongDTO } from "./mapper"; + +export function listSongs() { + return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); +} +``` + +```ts title="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); + }) + }, +}); +``` + +### 如何处理嵌套 DTO + +最有问题的部分是当来自后端的响应包含多个实体时。例如,如果歌曲不仅包含作者的 ID,还包含整个作者对象。在这种情况下,实体不可能不相互了解(除非我们想要丢弃数据或与后端团队进行坚定的对话)。与其想出 slices 之间间接连接的解决方案(例如将操作分派到其他 slices 的通用中间件),不如使用 `@x` 符号进行显式交叉导入。以下是我们如何使用 Redux Toolkit 实现它: + +```ts title="entities/song/model/songs.ts" +import { + createSlice, + createEntityAdapter, + createAsyncThunk, + createSelector, +} from '@reduxjs/toolkit' +import { normalize, schema } from 'normalizr' + +import { getSong } from "../api/getSong"; + +// 定义 normalizr 实体 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) + // 规范化数据,以便 reducers 可以加载可预测的 payload,如: + // `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 +``` + +```ts title="entities/song/@x/artist.ts" +export { fetchSong } from "../model/songs"; +``` + +```ts title="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) => { + // 通过在这里插入艺术家来处理相同的获取结果 + artistAdapter.upsertMany(state, action.payload.artists) + }) + }, +}) + +const reducer = slice.reducer +export default reducer +``` + +这稍微限制了 slice 隔离的好处,但它准确地表示了我们无法控制的这两个实体之间的连接。如果这些实体要被重构,它们必须一起重构。 + +## 全局类型和 Redux + +全局类型是将在整个应用程序中使用的类型。根据它们需要了解的内容,有两种全局类型: +1. 没有任何应用程序特定内容的通用类型 +2. 需要了解整个应用程序的类型 + +第一种情况很容易解决 — 将您的类型放在 Shared 中的适当 segment 中。例如,如果您有一个用于分析的全局变量接口,您可以将其放在 `shared/analytics` 中。 + +:::warning + +避免创建 `shared/types` 文件夹。它仅基于"是一个类型"的属性将不相关的事物分组,而该属性在项目中搜索代码时通常没有用。 + +::: + +第二种情况在没有 RTK 的 Redux 项目中很常见。您的最终存储类型只有在将所有 reducer 添加在一起后才可用,但此存储类型需要对您在应用程序中使用的选择器可用。例如,这是您的典型存储定义: + +```ts title="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; +``` + +在 `shared/store` 中拥有类型化的 Redux hooks `useAppDispatch` 和 `useAppSelector` 会很好,但由于[层上的导入规则][import-rule-on-layers],它们无法从 App 层导入 `RootState` 和 `AppDispatch`: + +> slice 中的模块只能在其他 slices 位于严格较低的层时导入它们。 + +在这种情况下,推荐的解决方案是在 Shared 和 App 层之间创建隐式依赖关系。这两种类型 `RootState` 和 `AppDispatch` 不太可能改变,Redux 开发者会熟悉它们,所以我们不必太担心它们。 + +在 TypeScript 中,您可以通过将类型声明为全局来做到这一点: + +```ts title="app/store/index.ts" +/* 与之前代码块中的内容相同… */ + +declare type RootState = ReturnType; +declare type AppDispatch = typeof store.dispatch; +``` + +```ts title="shared/store/index.ts" +import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux"; + +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector: TypedUseSelectorHook = useSelector; +``` + +## 枚举 + +枚举的一般规则是它们应该**尽可能接近使用位置**定义。当枚举表示特定于单个功能的值时,它应该在同一功能中定义。 + +segment 的选择也应该由使用位置决定。例如,如果您的枚举包含屏幕上 toast 的位置,它应该放在 `ui` segment 中。如果它表示后端操作的加载状态,它应该放在 `api` segment 中。 + +一些枚举在整个项目中确实是通用的,如一般的后端响应状态或设计系统令牌。在这种情况下,您可以将它们放在 Shared 中,并根据枚举所代表的内容选择 segment(响应状态用 `api`,设计令牌用 `ui` 等)。 + +## 类型验证 schemas 和 Zod + +如果您想验证您的数据符合某种形状或约束,您可以定义一个验证 schema。在 TypeScript 中,这项工作的流行库是 [Zod][ext-zod]。验证 schemas 也应该尽可能与使用它们的代码放在一起。 + +验证 schemas 类似于映射器(如[数据传输对象和映射器](#data-transfer-objects-and-mappers)部分所讨论的),它们接受数据传输对象并解析它,如果解析失败则产生错误。 + +验证最常见的情况之一是来自后端的数据。通常,当数据与 schema 不匹配时,您希望请求失败,因此将 schema 放在与请求函数相同的位置是有意义的,这通常是 `api` segment。 + +如果您的数据通过用户输入(如表单)传入,验证应该在输入数据时进行。您可以将 schema 放在 `ui` segment 中,紧挨着表单组件,或者如果 `ui` segment 太拥挤,可以放在 `model` segment 中。 + +## 组件 props 和 context 的类型定义 + +一般来说,最好将 props 或 context 接口保存在使用它们的组件或 context 的同一文件中。如果您有一个单文件组件的框架,如 Vue 或 Svelte,并且您无法在同一文件中定义 props 接口,或者您想在几个组件之间共享该接口,请在同一文件夹中创建一个单独的文件,通常是 `ui` segment。 + +以下是 JSX(React 或 Solid)的示例: + +```ts title="pages/home/ui/RecentActions.tsx" +interface RecentActionsProps { + actions: Array<{ id: string; text: string }>; +} + +export function RecentActions({ actions }: RecentActionsProps) { + /* … */ +} +``` + +以下是将接口存储在 Vue 的单独文件中的示例: + +```ts title="pages/home/ui/RecentActionsProps.ts" +export interface RecentActionsProps { + actions: Array<{ id: string; text: string }>; +} +``` + +```html title="pages/home/ui/RecentActions.vue" + +``` + +## 环境声明文件 (`*.d.ts`) + +一些包,例如 [Vite][ext-vite] 或 [ts-reset][ext-ts-reset],需要环境声明文件才能在您的应用程序中工作。通常,它们不大也不复杂,所以它们通常不需要任何架构,只需将它们放在 `src/` 文件夹中即可。为了保持 `src` 更有组织,您可以将它们保存在 App 层的 `app/ambient/` 中。 + +其他包根本没有类型定义,您可能希望将它们声明为无类型或甚至为它们编写自己的类型定义。这些类型定义的好地方是 `shared/lib`,在像 `shared/lib/untyped-packages` 这样的文件夹中。在那里创建一个 `%LIBRARY_NAME%.d.ts` 文件并声明您需要的类型: + +```ts title="shared/lib/untyped-packages/use-react-screenshot.d.ts" +// 这个库没有类型定义,我们不想费心编写自己的。 +declare module "use-react-screenshot"; +``` + +## 类型的自动生成 + +从外部源生成类型是很常见的,例如,从 OpenAPI schema 生成后端类型。在这种情况下,为这些类型在您的代码库中创建一个专门的位置,如 `shared/api/openapi`。理想情况下,您还应该在该文件夹中包含一个 README,描述这些文件是什么、如何重新生成它们等。 + +[import-rule-on-layers]: /docs/reference/layers#import-rule-on-layers +[ext-type-fest]: https://github.com/sindresorhus/type-fest +[ext-zod]: https://zod.dev +[ext-vite]: https://vitejs.dev +[ext-ts-reset]: https://www.totaltypescript.com/ts-reset diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/white-labels.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/white-labels.mdx new file mode 100644 index 000000000..feacf8dc1 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/examples/white-labels.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 8 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 白标 + + + +> Figma、品牌 uikit、模板、品牌适应性 + +## 另请参阅 + +- [(Thread) 关于白标(品牌)项目的应用](https://t.me/feature_sliced/1543) +- [(演示文稿) 关于白标应用程序和设计](http://yadi.sk/i/5IdhzsWrpO3v4Q) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/index.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/index.mdx new file mode 100644 index 000000000..77dfa1266 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/index.mdx @@ -0,0 +1,46 @@ +--- +hide_table_of_contents: true +pagination_prev: get-started/index +--- + +# 🎯 指南 + +实践导向 + +

+关于使用 Feature-Sliced Design 的实用指南和示例。还描述了迁移指南和有害实践手册。当您试图实现特定功能或想要看看方法论在"实战"中的表现时,这是最有用的。 +

+ +## Main + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { ToolOutlined, ImportOutlined, BugOutlined, FunctionOutlined } from "@ant-design/icons"; + + + + + + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/_category_.yaml b/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/_category_.yaml new file mode 100644 index 000000000..70c237145 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/_category_.yaml @@ -0,0 +1,2 @@ +label: 代码异味和问题 +position: 4 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/cross-imports.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/cross-imports.mdx new file mode 100644 index 000000000..709ff1bb0 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/cross-imports.mdx @@ -0,0 +1,21 @@ +--- +sidebar_position: 4 +sidebar_class_name: sidebar-item--wip +pagination_next: reference/index +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 交叉导入 + + + +> 当层或抽象开始承担超出其应有责任时,就会出现交叉导入。这就是为什么方法论识别出新的层,允许您解耦这些交叉导入 + +## 另请参阅 + +- [(Thread) 关于交叉端口的所谓不可避免性](https://t.me/feature_sliced/4515) +- [(Thread) 关于解决实体中的交叉端口](https://t.me/feature_sliced/3678) +- [(Thread) 关于交叉导入和责任](https://t.me/feature_sliced/3287) +- [(Thread) 关于 segments 之间的导入](https://t.me/feature_sliced/4021) +- [(Thread) 关于 shared 内部的交叉导入](https://t.me/feature_sliced/3618) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/desegmented.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/desegmented.mdx new file mode 100644 index 000000000..9b011b03a --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/desegmented.mdx @@ -0,0 +1,97 @@ +--- +sidebar_position: 2 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 去分段化 + + + +## 情况 + +在项目中经常出现这样的情况:与主题领域中特定域相关的模块被不必要地去分段化并分散在项目周围 + +```sh +├── 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/ +``` + +## 问题 + +该问题至少表现为违反了**高内聚**原则和过度拉伸**变更轴** + +## 如果您忽略它 + +- 如果需要涉及逻辑,例如交付 - 我们必须记住它位于多个地方,并涉及代码中的多个地方 - 这不必要地拉伸了我们的**变更轴** +- 如果我们需要研究用户的逻辑,我们将不得不遍历整个项目来详细研究**actions、epics、constants、entities、components** - 而不是将其放在一个地方 +- 隐式连接和不断增长的主题领域的不可控性 +- 使用这种方法,眼睛经常会模糊,您可能不会注意到我们如何"为了常量而创建常量",在相应的项目目录中创建垃圾场 + +## 解决方案 + +将与特定域/用户案例相关的所有模块 - 直接彼此相邻放置 + +这样,在研究特定模块时,其所有组件都并排放置,而不是分散在项目周围 + +> 它还增加了代码库的可发现性和清晰度以及模块之间的关系 + +```diff +- ├── 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/ +``` + +## See also + +* [(Article) About Low Coupling and High Cohesion clearly](https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/) +* [(Article) Low Coupling and High Cohesion. The Law of Demeter](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx new file mode 100644 index 000000000..2db0ec256 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx @@ -0,0 +1,46 @@ +--- +sidebar_position: 3 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 路由 + + + +## 情况 + +页面的 URL 在页面下方的层中硬编码 + +```tsx title="entities/post/card" + + + + ... + +``` + +## 问题 + +URL 没有集中在页面层中,根据责任范围,它们应该属于页面层 + +## 如果您忽略它 + +那么,在更改 URL 时,您必须记住这些 URL(以及 URL/重定向的逻辑)可以在除页面之外的所有层中 + +这也意味着现在即使是一个简单的产品卡片也承担了页面的部分责任,这模糊了项目的逻辑 + +## 解决方案 + +确定如何从页面级别及以上处理 URL/重定向 + +通过组合/props/工厂传递到下面的层 + +## 另请参阅 + +- [(Thread) 如果我在 entities/features/widgets 中"缝合"路由会怎样](https://t.me/feature_sliced/4389) +- [(Thread) 为什么只在页面中模糊路由逻辑](https://t.me/feature_sliced/3756) diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/_category_.yaml b/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/_category_.yaml new file mode 100644 index 000000000..21d96b96f --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/_category_.yaml @@ -0,0 +1,2 @@ +label: 迁移 +position: 2 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-custom.md b/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-custom.md new file mode 100644 index 000000000..fbd122396 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-custom.md @@ -0,0 +1,313 @@ +--- +sidebar_position: 1 +sidebar_label: From a custom architecture +--- + +# 从自定义架构迁移 + +本指南描述了一种在从自定义自制架构迁移到 Feature-Sliced Design 时可能有用的方法。 + +这里是典型自定义架构的文件夹结构。我们将在本指南中将其作为示例使用。 +点击蓝色箭头打开文件夹。 + +
+ 📁 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
  • +
+
+ +## 在您开始之前 {#before-you-start} + +The most important question to ask your team when considering to switch to Feature-Sliced Design is — _do you really need it?_ We love Feature-Sliced Design, but even we recognize that some projects are perfectly fine without it. + +Here are some reasons to consider making the switch: + +1. New team members are complaining that it's hard to get to a productive level +2. Making modifications to one part of the code **often** causes another unrelated part to break +3. Adding new functionality is difficult due to the sheer amount of things you need to think about + +**Avoid switching to FSD against the will of your teammates**, even if you are the lead. +First, convince your teammates that the benefits outweigh the cost of migration and the cost of learning a new architecture instead of the established one. + +Also keep in mind that any kind of architectural changes are not immediately observable to the management. Make sure they are on board with the switch before starting and explain to them why it might benefit the project. + +:::tip + +If you need help convincing the project manager that FSD is beneficial, consider some of these points: +1. Migration to FSD can happen incrementally, so it will not halt the development of new features +2. A good architecture can significantly decrease the time that a new developer needs to get productive +3. FSD is a documented architecture, so the team doesn't have to continuously spend time on maintaining their own documentation + +::: + +--- + +If you made the decision to start migrating, then the first thing you want to do is to set up an alias for `📁 src`. It will be helpful later to refer to top-level folders. We will consider `@` as an alias for `./src` for the rest of this guide. + +## Step 1. Divide the code by pages {#divide-code-by-pages} + +Most custom architectures already have a division by pages, however small or large in logic. If you already have `📁 pages`, you may skip this step. + +If you only have `📁 routes`, create `📁 pages` and try to move as much component code from `📁 routes` as possible. Ideally, you would have a tiny route and a larger page. As you're moving code, create a folder for each page and add an index file: + +:::note + +For now, it's okay if your pages reference each other. You can tackle that later, but for now, focus on establishing a prominent division by pages. + +::: + +Route file: + +```js title="src/routes/products.[id].js" +export { ProductPage as default } from "@/pages/product" +``` + +Page index file: + +```js title="src/pages/product/index.js" +export { ProductPage } from "./ProductPage.jsx" +``` + +Page component file: + +```jsx title="src/pages/product/ProductPage.jsx" +export function ProductPage(props) { + return
; +} +``` + +## Step 2. Separate everything else from the pages {#separate-everything-else-from-pages} + +Create a folder `📁 src/shared` and move everything that doesn't import from `📁 pages` or `📁 routes` there. Create a folder `📁 src/app` and move everything that does import the pages or routes there, including the routes themselves. + +Remember that the Shared layer doesn't have slices, so it's fine if segments import from each other. + +You should end up with a file structure like this: + +
+ 📁 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
    • +
    +
    +
  • +
+
+ +## Step 3. Tackle cross-imports between pages {#tackle-cross-imports-between-pages} + + + + +Find all instances where one page is importing from the other and do one of the two things: + +1. Copy-paste the imported code into the depending page to remove the dependency +2. Move the code to a proper segment in Shared: + - if it's a part of the UI kit, move it to `📁 shared/ui`; + - if it's a configuration constant, move it to `📁 shared/config`; + - if it's a backend interaction, move it to `📁 shared/api`. + +:::note + +**Copy-pasting isn't architecturally wrong**, in fact, sometimes it may be more correct to duplicate than to abstract into a new reusable module. The reason is that sometimes the shared parts of pages start drifting apart, and you don't want dependencies getting in your way in these cases. + +However, there is still sense in the DRY ("don't repeat yourself") principle, so make sure you're not copy-pasting business logic. Otherwise you will need to remember to fix bugs in several places at once. + +::: + +## Step 4. Unpack the Shared layer {#unpack-shared-layer} + +You might have a lot of stuff in the Shared layer on this step, and you generally want to avoid that. The reason is that the Shared layer may be a dependency for any other layer in your codebase, so making changes to that code is automatically more prone to unintended consequences. + +Find all the objects that are only used on one page and move it to the slice of that page. And yes, _that applies to actions, reducers, and selectors, too_. There is no benefit in grouping all actions together, but there is benefit in colocating relevant actions close to their usage. + +You should end up with a file structure like this: + +
+ 📁 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
    • +
    +
    +
  • +
+
+ +## Step 5. Organize code by technical purpose {#organize-by-technical-purpose} + +In FSD, division by technical purpose is done with _segments_. There are a few common ones: + +- `ui` — everything related to UI display: UI components, date formatters, styles, etc. +- `api` — backend interactions: request functions, data types, mappers, etc. +- `model` — the data model: schemas, interfaces, stores, and business logic. +- `lib` — library code that other modules on this slice need. +- `config` — configuration files and feature flags. + +You can create your own segments, too, if you need. Make sure not to create segments that group code by what it is, like `components`, `actions`, `types`, `utils`. Instead, group the code by what it's for. + +Reorganize your pages to separate code by segments. You should already have a `ui` segment, now it's time to create other segments, like `model` for your actions, reducers, and selectors, or `api` for your thunks and mutations. + +Also reorganize the Shared layer to remove these folders: +- `📁 components`, `📁 containers` — most of it should become `📁 shared/ui`; +- `📁 helpers`, `📁 utils` — if there are some reused helpers left, group them together by function, like dates or type conversions, and move theses groups to `📁 shared/lib`; +- `📁 constants` — again, group by function and move to `📁 shared/config`. + +## Optional steps {#optional-steps} + +### Step 6. Form entities/features from Redux slices that are used on several pages {#form-entities-features-from-redux} + +Usually, these reused Redux slices will describe something relevant to the business, for example, products or users, so these can be moved to the Entities layer, one entity per one folder. If the Redux slice is related to an action that your users want to do in your app, like comments, then you can move it to the Features layer. + +Entities and features are meant to be independent from each other. If your business domain contains inherent connections between entities, refer to the [guide on business entities][business-entities-cross-relations] for advice on how to organize these connections. + +The API functions related to these slices can stay in `📁 shared/api`. + +### Step 7. Refactor your modules {#refactor-your-modules} + +The `📁 modules` folder is commonly used for business logic, so it's already pretty similar in nature to the Features layer from FSD. Some modules might also be describe large chunks of the UI, like an app header. In that case, you should migrate them to the Widgets layer. + +### Step 8. Form a clean UI foundation in `shared/ui` {#form-clean-ui-foundation} + +`📁 shared/ui` should ideally contain a set of UI elements that don't have any business logic encoded in them. They should also be highly reusable. + +Refactor the UI components that used to be in `📁 components` and `📁 containers` to separate out the business logic. Move that business logic to the higher layers. If it's not used in too many places, you could even consider copy-pasting. + +## See also {#see-also} + +- [(Talk in Russian) Ilya Klimov — Крысиные бега бесконечного рефакторинга: как не дать техническому долгу убить мотивацию и продукт](https://youtu.be/aOiJ3k2UvO4) + +[ext-steiger]: https://github.com/feature-sliced/steiger +[business-entities-cross-relations]: /docs/guides/examples/types#business-entities-and-their-cross-references diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-v1.md b/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-v1.md new file mode 100644 index 000000000..e8e4b88b1 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-v1.md @@ -0,0 +1,171 @@ +--- +sidebar_position: 2 +--- + +# 从 v1 到 v2 的迁移 + +## 为什么是 v2? + +**feature-slices** 的原始概念于 2018 年[被宣布][ext-tg-spb]。 + +从那时起,该方法论发生了许多变化,但同时**[基本原则得到了保留][ext-v1]**: + +- 使用*标准化*的前端项目结构 +- 首先按照*业务逻辑*分割应用程序 +- 使用*隔离的 features* 来防止隐式副作用和循环依赖 +- 使用 *Public API* 并禁止深入模块的"内部" + +同时,在方法论的上一个版本中,仍然存在**薄弱环节**: + +- 有时会导致样板代码 +- 有时会导致代码库的过度复杂化和抽象之间不明显的规则 +- 有时会导致隐式的架构解决方案,这阻止了项目的提升和新人的入职 + +方法论的新版本([v2][ext-v2])旨在**消除这些缺点,同时保留该方法的现有优势**。 + +Since 2018, [has also developed][ext-fdd-issues] another similar methodology - [**feature-driven**][ext-fdd], which was first announced by [Oleg Isonen][ext-kof]. + +After merging of the two approaches, we have **improved and refined existing practices** - towards greater flexibility, clarity and efficiency in application. + +> As a result, this has even affected the name of the methodology - *"feature-slice**d**"* + +## Why does it make sense to migrate the project to v2? + +> `WIP:` The current version of the methodology is under development and some details *may change* + +#### 🔍 More transparent and simple architecture + +The methodology (v2) offers **more intuitive and more common abstractions and ways of separating logic among developers.** + +All this has an extremely positive effect on attracting new people, as well as studying the current state of the project, and distributing the business logic of the application. + +#### 📦 More flexible and honest modularity + +The methodology (v2) allows **to distribute logic in a more flexible way:** + +- With the ability to refactor isolated parts from scratch +- With the ability to rely on the same abstractions, but without unnecessary interweaving of dependencies +- With simpler requirements for the location of the new module *(layer => slice => segment)* + +#### 🚀 More specifications, plans, community + +At the moment, the `core-team` is actively working on the latest (v2) version of the methodology + +So it is for her: + +- there will be more described cases / problems +- there will be more guides on the application +- there will be more real examples +- in general, there will be more documentation for onboarding new people and studying the concepts of the methodology +- the toolkit will be developed in the future to comply with the concepts and conventions on architecture + +> Of course, there will be user support for the first version as well - but the latest version is still a priority for us +> +> In the future, with the next major updates, you will still have access to the current version (v2) of the methodology, **without risks for your teams and projects** + +## Changelog + +### `BREAKING` Layers + +Now the methodology assumes explicit allocation of layers at the top level + +- `/app` > `/processes` > **`/pages`** > **`/features`** > `/entities` > `/shared` +- *That is, not everything is now treated as features/pages* +- This approach allows you to [explicitly set rules for layers][ext-tg-v2-draft]: +- The **higher the layer** of the module is located , the more **context** it has + + *(in other words-each module of the layer - can import only the modules of the underlying layers, but not higher)* + +- The **lower the layer of the** module is located , the more **danger and responsibility** to make changes to it + + *(because it is usually the underlying layers that are more overused)* + +### `BREAKING` Shared + +The infrastructure abstractions `/ui`, `/lib`, `/api`, which used to lie in the src root of the project, are now separated by a separate directory `/src/shared` + +- `shared/ui` - Still the same general uikit of the application (optional) + - *At the same time, no one forbids using `Atomic Design` here as before* +- `shared/lib` - A set of auxiliary libraries for implementing logic + - *Still - without a dump of helpers* +- `shared/api` - A common entry point for accessing the API + - *Can also be registered locally in each feature / page - but it is not recommended* +- As before - there should be no explicit binding to business logic in `shared` + - *If necessary, you need to take this relationship to the `entities` level or even higher* + +### `NEW` Entities, Processes + +In v2 **, other new abstractions** have been added to eliminate the problems of logic complexity and high coupling. + +- `/entities` - layer **business entities** containing slices that are related directly to the business models or synthetic entities required only on frontend + - *Examples: `user`, `i18n`, `order`, `blog`* +- `/processes` - layer **business processes**, penetrating app + - **The layer is optional**, it is usually recommended to use it when *the logic grows and begins to blur in several pages* + - *Examples: `payment`, `auth`, `quick-tour`* + +### `BREAKING` Abstractions & Naming + +Now specific abstractions and [clear recommendations for naming them][refs-adaptability]are defined + +[disc-process]: https://github.com/feature-sliced/documentation/discussions/20 +[disc-features]: https://github.com/feature-sliced/documentation/discussions/23 +[disc-entities]: https://github.com/feature-sliced/documentation/discussions/18#discussioncomment-422649 +[disc-shared]: https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453020 + +[disc-ui]: https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453132 +[disc-model]: https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-472645 +[disc-api]: https://github.com/feature-sliced/documentation/discussions/66 + +#### Layers + +- `/app` — **application initialization layer** + - *Previous versions: `app`, `core`,`init`, `src/index` (and this happens)* +- `/processes` — [**business process layer**][disc-process] + - *Previous versions: `processes`, `flows`, `workflows`* +- `/pages` — **application page layer** + - *Previous versions: `pages`, `screens`, `views`, `layouts`, `components`, `containers`* +- `/features` — [**functionality parts layer**][disc-features] + - *Previous versions: `features`, `components`, `containers`* +- `/entities` — [**business entity layer**][disc-entities] + - *Previous versions: `entities`, `models`, `shared`* +- `/shared` — [**layer of reused infrastructure code**][disc-shared] 🔥 + - *Previous versions: `shared`, `common`, `lib`* + +#### Segments + +- `/ui` — [**UI segment**][disc-ui] 🔥 + - *Previous versions: `ui`, `components`, `view`* +- `/model` — [**BL-segment**][disc-model] 🔥 + - *Previous versions: `model`, `store`, `state`, `services`, `controller`* +- `/lib` — segment **of auxiliary code** + - *Previous versions: `lib`, `libs`, `utils`, `helpers`* +- `/api` — [**API segment**][disc-api] + - *Previous versions: `api`, `service`, `requests`, `queries`* +- `/config` — **application configuration segment** + - *Previous versions: `config`, `env`, `get-env`* + +### `REFINED` Low coupling + +Now it is much easier to [observe the principle of low coupling][refs-low-coupling] between modules, thanks to the new layers. + +*At the same time, it is still recommended to avoid as much as possible cases where it is extremely difficult to "uncouple" modules* + +## See also + +- [Notes from the report "React SPB Meetup #1"][ext-tg-spb] +- [React Berlin Talk - Oleg Isonen "Feature Driven Architecture"][ext-kof-fdd] +- [Comparison with v1 (community-chat)](https://t.me/feature_sliced/493) +- [New ideas v2 with explanations (atomicdesign-chat)][ext-tg-v2-draft] +- [Discussion of abstractions and naming for the new version of the methodology (v2)](https://github.com/feature-sliced/documentation/discussions/31) + +[refs-low-coupling]: /docs/reference/slices-segments#zero-coupling-high-cohesion +[refs-adaptability]: /docs/about/understanding/naming + +[ext-v1]: https://feature-sliced.github.io/featureslices.dev/v1.0.html +[ext-tg-spb]: https://t.me/feature_slices +[ext-fdd]: https://github.com/feature-sliced/documentation/tree/rc/feature-driven +[ext-fdd-issues]: https://github.com/kof/feature-driven-architecture/issues +[ext-v2]: https://github.com/feature-sliced/documentation +[ext-kof]: https://github.com/kof +[ext-kof-fdd]: https://www.youtube.com/watch?v=BWAeYuWFHhs +[ext-tg-v2-draft]: https://t.me/atomicdesign/18708 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-v2-0.md b/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-v2-0.md new file mode 100644 index 000000000..af057a421 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/migration/from-v2-0.md @@ -0,0 +1,45 @@ +--- +sidebar_position: 3 +--- + +# Migration from v2.0 to v2.1 + +The main change in v2.1 is the new mental model for decomposing an interface — pages first. + +In v2.0, FSD would recommend identifying entities and features in your interface, considering even the smallest bits of entity representation and interactivity for decomposition. Then you would build widgets and pages from entities and features. In this model of decomposition, most of the logic was in entities and features, and pages were just compositional layers that didn't have much significance on their own. + +In v2.1, we recommend starting with pages, and possibly even stopping there. Most people already know how to separate the app into individual pages, and pages are also a common starting point when trying to locate a component in the codebase. In this new model of decomposition, you keep most of the UI and logic in each individual page, maintaining a reusable foundation in Shared. If a need arises to reuse business logic across several pages, you can move it to a layer below. + +Another addition to Feature-Sliced Design is the standardization of cross-imports between entities with the `@x`-notation. + +## How to migrate {#how-to-migrate} + +There are no breaking changes in v2.1, which means that a project written with FSD v2.0 is also a valid project in FSD v2.1. However, we believe that the new mental model is more beneficial for teams and especially onboarding new developers, so we recommend making minor adjustments to your decomposition. + +### Merge slices + +A simple way to start is by running our linter, [Steiger][steiger], on the project. Steiger is built with the new mental model, and the most helpful rules will be: + +- [`insignificant-slice`][insignificant-slice] — if an entity or feature is only used in one page, this rule will suggest merging that entity or feature into the page entirely. +- [`excessive-slicing`][excessive-slicing] — if a layer has too many slices, it's usually a sign that the decomposition is too fine-grained. This rule will suggest merging or grouping some slices to help project navigation. + +```bash +npx steiger src +``` + +This will help you identify which slices are only used once, so that you could reconsider if they are really necessary. In such considerations, keep in mind that a layer forms some kind of global namespace for all the slices inside of it. Just as you wouldn't pollute the global namespace with variables that are only used once, you should treat a place in the namespace of a layer as valuable, to be used sparingly. + +### Standardize cross-imports + +If you had cross-imports between in your project before (we don't judge!), you may now take advantage of a new notation for cross-importing in Feature-Sliced Design — the `@x`-notation. It looks like this: + +```ts title="entities/B/some/file.ts" +import type { EntityA } from "entities/A/@x/B"; +``` + +For more details, check out the [Public API for cross-imports][public-api-for-cross-imports] section in the reference. + +[insignificant-slice]: https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice +[steiger]: https://github.com/feature-sliced/steiger +[excessive-slicing]: https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/excessive-slicing +[public-api-for-cross-imports]: /docs/reference/public-api#public-api-for-cross-imports diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/_category_.yaml b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/_category_.yaml new file mode 100644 index 000000000..fbd193502 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/_category_.yaml @@ -0,0 +1,2 @@ +label: 技术 +position: 3 diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-electron.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-electron.mdx new file mode 100644 index 000000000..dc779d722 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-electron.mdx @@ -0,0 +1,134 @@ +--- +sidebar_position: 10 +--- +# Usage with Electron + +Electron applications have a special architecture consisting of multiple processes with different responsibilities. Applying FSD in such a context requires adapting the structure to the Electron specifics. + +```sh +└── 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) +``` + +## Public API rules +Each process must have its own public API. For example, you can't import modules from `main` to `renderer`. +Only the `src/shared` folder is public for both processes. +It's also necessary for describing contracts for process interaction. + +## Additional changes to the standard structure +It's suggested to use a new `ipc` segment, where interaction between processes takes place. +The `pages` and `widgets` layers, based on their names, should not be present in `src/main`. You can use `features`, `entities` and `shared`. +The `app` layer in `src` contains entry points for `main` and `renderer`, as well as the IPC. +It's not desirable for segments in the `app` layer to have intersection points + +## Interaction example + +```typescript title="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; +``` + +```typescript title="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; + }; +} +``` + +```typescript title="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']; +}; +``` + +```typescript title="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); +``` + +```typescript title="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', + }; + }); +}; +``` + +```typescript title="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' }; +}; +``` + +## See also +- [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) \ No newline at end of file diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-nextjs.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-nextjs.mdx new file mode 100644 index 000000000..70eba9ee8 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-nextjs.mdx @@ -0,0 +1,197 @@ +--- +sidebar_position: 1 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Usage with Next.js + +FSD is compatible with Next.js in both the App Router version and the Pages Router version if you solve the main conflict — the `app` and `pages` folders. + +## App Router {#app-router} + +### Conflict between FSD and Next.js in the `app` layer {#conflict-between-fsd-and-nextjs-in-the-app-layer} + +Next.js suggests using the `app` folder to define application routes. It expects files in the `app` folder to correspond to pathnames. This routing mechanism **does not align** with the FSD concept, as it's not possible to maintain a flat slice structure. + +The solution is to move the Next.js `app` folder to the project root and import FSD pages from `src`, where the FSD layers are, into the Next.js `app` folder. + +You will also need to add a `pages` folder to the project root, otherwise Next.js will try to use `src/pages` as the Pages Router even if you use the App Router, which will break the build. It's also a good idea to put a `README.md` file inside this root `pages` folder describing why it is necessary, even though it's empty. + +```sh +├── 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 +``` + +Example of re-exporting a page from `src/pages` in the Next.js `app`: + +```tsx title="app/example/page.tsx" +export { ExamplePage as default, metadata } from '@/pages/example'; +``` + +### Middleware {#middleware} + +If you use middleware in your project, it must be located in the project root alongside the Next.js `app` and `pages` folders. + +### Instrumentation {#instrumentation} + +The `instrumentation.js` file allows you to monitor the performance and behavior of your application. If you use it, it must be located in the project root, similar to `middleware.js`. + +## Pages Router {#pages-router} + +### Conflict between FSD and Next.js in the `pages` layer {#conflict-between-fsd-and-nextjs-in-the-pages-layer} + +Routes should be placed in the `pages` folder in the root of the project, similar to `app` folder for the App Router. The structure inside `src` where the layer folders are located remains unchanged. + +```sh +├── 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 +``` + +Example of re-exporting a page from `src/pages` in the Next.js `pages`: + +```tsx title="pages/example/index.tsx" +export { Example as default } from '@/pages/example'; +``` + +### Custom `_app` component {#custom-_app-component} + +You can place your Custom App component in `src/app/_app` or `src/app/custom-app`: + +```tsx title="src/app/custom-app/custom-app.tsx" +import type { AppProps } from 'next/app'; + +export const MyApp = ({ Component, pageProps }: AppProps) => { + return ( + <> +

My Custom App component

+ + + ); +}; +``` + +```tsx title="pages/_app.tsx" +export { App as default } from '@/app/custom-app'; +``` + +## Route Handlers (API routes) {#route-handlers-api-routes} + +Use the `api-routes` segment in the `app` layer to work with Route Handlers. + +Be mindful when writing backend code in the FSD structure — FSD is primarily intended for frontends, meaning that's what people will expect to find. +If you need a lot of endpoints, consider separating them into a different package in a monorepo. + + + + + +```tsx title="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', + }); + } +}; +``` + +```tsx title="app/api/example/route.ts" +export { getExampleData as GET } from '@/app/api-routes'; +``` + + + + + +```tsx title="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; +``` + +```tsx title="src/app/api-routes/index.ts" +export { getExampleData } from './get-example-data'; +``` + +```tsx title="app/api/example.ts" +import { getExampleData } from '@/app/api-routes'; + +export const config = getExampleData.config; +export default getExampleData.handler; +``` + + + + + +## Additional recommendations {#additional-recommendations} + +- Use the `db` segment in the `shared` layer to describe database queries and their further use in higher layers. +- Caching and revalidating queries logic is better kept in the same place as the queries themselves. + +## See also {#see-also} + +- [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) + +[project-knowledge]: /docs/about/understanding/knowledge-types +[ext-app-router-stackblitz]: https://stackblitz.com/edit/stackblitz-starters-aiez55?file=README.md diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-nuxtjs.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-nuxtjs.mdx new file mode 100644 index 000000000..929bdcaf5 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-nuxtjs.mdx @@ -0,0 +1,179 @@ +--- +sidebar_position: 10 +--- +# Usage with NuxtJS + +It is possible to implement FSD in a NuxtJS project, but conflicts arise due to the differences between NuxtJS project structure requirements and FSD principles: + +- Initially, NuxtJS offers a project file structure without a `src` folder, i.e. in the root of the project. +- The file routing is in the `pages` folder, while in FSD this folder is reserved for the flat slice structure. + + +## Adding an alias for the `src` directory + +Add an `alias` object to your config: +```ts title="nuxt.config.ts" +export default defineNuxtConfig({ + devtools: { enabled: true }, // Not FSD related, enabled at project startup + alias: { + "@": '../src' + }, +}) +``` +## Choose how to configure the router + +In NuxtJS, there are two ways to customize the routing - using a config and using a file structure. +In the case of file-based routing, you will create index.vue files in folders inside the app/routes directory, and in the case of configure, you will configure the routers in the `router.options.ts` file. + + +### Routing using config + +In the `app` layer, create a `router.options.ts` file, and export a config object from it: +```ts title="app/router.options.ts" +import type { RouterConfig } from '@nuxt/schema'; + +export default { + routes: (_routes) => [], +}; + +``` + +To add a `Home` page to your project, you need to do the following steps: +- Add a page slice inside the `pages` layer +- Add the appropriate route to the `app/router.config.ts` config + + +To create a page slice, let's use the [CLI](https://github.com/feature-sliced/cli): + +```shell +fsd pages home +``` + +Create a ``home-page.vue`` file inside the ui segment, access it using the Public API + +```ts title="src/pages/home/index.ts" +export { default as HomePage } from './ui/home-page'; +``` + +Thus, the file structure will look like this: +```sh +|── src +│ ├── app +│ │ ├── router.config.ts +│ ├── pages +│ │ ├── home +│ │ │ ├── ui +│ │ │ │ ├── home-page.vue +│ │ │ ├── index.ts +``` +Finally, let's add a route to the config: + +```ts title="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 + +First of all, create a `src` directory in the root of your project, and create app and pages layers inside this directory and a routes folder inside the app layer. +Thus, your file structure should look like this: + +```sh +├── src +│ ├── app +│ │ ├── routes +│ ├── pages # Pages folder, related to FSD +``` + +In order for NuxtJS to use the routes folder inside the `app` layer for file routing, you need to modify `nuxt.config.ts` as follows: +```ts title="nuxt.config.ts" +export default defineNuxtConfig({ + devtools: { enabled: true }, // Not FSD related, enabled at project startup + alias: { + "@": '../src' + }, + dir: { + pages: './src/app/routes' + } +}) +``` + +Now, you can create routes for pages within `app` and connect pages from `pages` to them. + +For example, to add a `Home` page to your project, you need to do the following steps: +- Add a page slice inside the `pages` layer +- Add the corresponding route inside the `app` layer +- Connect the page from the slice with the route + +To create a page slice, let's use the [CLI](https://github.com/feature-sliced/cli): + +```shell +fsd pages home +``` + +Create a ``home-page.vue`` file inside the ui segment, access it using the Public API + +```ts title="src/pages/home/index.ts" +export { default as HomePage } from './ui/home-page'; +``` + +Create a route for this page inside the `app` layer: + +```sh + +├── src +│ ├── app +│ │ ├── routes +│ │ │ ├── index.vue +│ ├── pages +│ │ ├── home +│ │ │ ├── ui +│ │ │ │ ├── home-page.vue +│ │ │ ├── index.ts +``` + +Add your page component inside the `index.vue` file: + +```html title="src/app/routes/index.vue" + + + +``` + +## What to do with `layouts`? + +You can place layouts inside the `app` layer, to do this you need to modify the config as follows: + +```ts title="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' + } +}) +``` + + +## See also + +- [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) + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-react-query.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-react-query.mdx new file mode 100644 index 000000000..88931cfa3 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-react-query.mdx @@ -0,0 +1,436 @@ +--- +sidebar_position: 10 +--- +# Usage with React Query + +## The problem of “where to put the keys” + +### Solution — break down by entities + +If the project already has a division into entities, and each request corresponds to a single entity, +the purest division will be by entity. In this case, we suggest using the following structure: +```sh +└── src/ # + ├── app/ # + | ... # + ├── pages/ # + | ... # + ├── entities/ # + | ├── {entity}/ # + | ... └── api/ # + | ├── `{entity}.query` # Query-factory where are the keys and functions + | ├── `get-{entity}` # Entity getter function + | ├── `create-{entity}` # Entity creation function + | ├── `update-{entity}` # Entity update function + | ├── `delete-{entity}` # Entity delete function + | ... # + | # + ├── features/ # + | ... # + ├── widgets/ # + | ... # + └── shared/ # + ... # +``` + +If there are connections between the entities (for example, the Country entity has a field-list of City entities), +then you can use the [public API for cross-imports][public-api-for-cross-imports] or consider the alternative solution below. + +### Alternative solution — keep it in shared + +In cases where entity separation is not appropriate, the following structure can be considered: + +```sh +└── src/ # + ... # + └── shared/ # + ├── api/ # + ... ├── `queries` # Query-factories + | ├── `document.ts` # + | ├── `background-jobs.ts` # + | ... # + └── index.ts # +``` + +Then in `@/shared/api/index.ts`: + +```ts title="@/shared/api/index.ts" +export { documentQueries } from "./queries/document"; +``` + +## The problem of “Where to insert mutations?” + +It is not recommended to mix mutations with queries. There are two options: + +### 1. Define a custom hook in the `api` segment near the place of use + +```tsx title="@/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. Define a mutation function somewhere else (Shared or Entities) and use `useMutation` directly in the component + +```tsx +const { mutateAsync, isPending } = useMutation({ + mutationFn: postApi.createPost, +}); +``` + +```tsx title="@/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 + + + ); +}; +``` + +## Organization of requests + +### Query factory + +A query factory is an object where the key values are functions that return a list of query keys. Here's how to use it: + +```ts +const keyFactory = { + all: () => ["entity"], + lists: () => [...postQueries.all(), "list"], +}; +``` + +:::info +`queryOptions` is a built-in utility in react-query@v5 (optional) + +```ts +queryOptions({ + queryKey, + ...options, +}); +``` + +For greater type safety, further compatibility with future versions of react-query, and easy access to functions and query keys, +you can use the built-in queryOptions function from “@tanstack/react-query” +[(More details here)](https://tkdodo.eu/blog/the-query-options-api#queryoptions). +::: + +### 1. Creating a Query Factory + +```tsx title="@/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. Using Query Factory in application code +```tsx +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}
+
+ ); +}; +``` + +### Benefits of using a Query Factory +- **Request structuring:** A factory allows you to organize all API requests in one place, making your code more readable and maintainable. +- **Convenient access to queries and keys:** The factory provides convenient methods for accessing different types of queries and their keys. +- **Query Refetching Ability:** The factory allows easy refetching without the need to change query keys in different parts of the application. + +## Pagination + +In this section, we'll look at an example of the `getPosts` function, which makes an API request to retrieve post entities using pagination. + +### 1. Creating a function `getPosts` +The getPosts function is located in the `get-posts.ts` file, which is located in the `api` segment + +```tsx title="@/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 for pagination +The `postQueries` query factory defines various query options for working with posts, +including requesting a list of posts with a specific page and limit. + +```tsx +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. Use in application code + +```tsx title="@/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" + /> + + + ); +}; +``` +:::note +The example is simplified, the full version is available on [GitHub](https://github.com/ruslan4432013/fsd-react-query-example) +::: + +## `QueryProvider` for managing queries +In this guide, we will look at how to organize a `QueryProvider`. + +### 1. Creating a `QueryProvider` +The file `query-provider.tsx` is located at the path `@/app/providers/query-provider.tsx`. + +```tsx title="@/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. Creating a `QueryClient` +`QueryClient` is an instance used to manage API requests. +The `query-client.ts` file is located at `@/shared/api/query-client.ts`. +`QueryClient` is created with certain settings for query caching. + +```tsx title="@/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, + }, + }, +}); +``` + +## Code generation + +There are tools that can generate API code for you, but they are less flexible than the manual approach described above. +If your Swagger file is well-structured, +and you're using one of these tools, it might make sense to generate all the code in the `@/shared/api` directory. + + +## Additional advice for organizing RQ +### API Client + +Using a custom API client class in the shared layer, +you can standardize the configuration and work with the API in the project. +This allows you to manage logging, +headers and data exchange format (such as JSON or XML) from one place. +This approach makes it easier to maintain and develop the project because it simplifies changes and updates to interactions with the API. + +```tsx title="@/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); +``` + +## See also {#see-also} + +- [(GitHub) Sample Project](https://github.com/ruslan4432013/fsd-react-query-example) +- [(CodeSandbox) Sample Project](https://codesandbox.io/p/github/ruslan4432013/fsd-react-query-example/main) +- [About the query factory](https://tkdodo.eu/blog/the-query-options-api) + +[public-api-for-cross-imports]: /docs/reference/public-api#public-api-for-cross-imports diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-sveltekit.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-sveltekit.mdx new file mode 100644 index 000000000..886b2381e --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/guides/tech/with-sveltekit.mdx @@ -0,0 +1,100 @@ +--- +sidebar_position: 10 +--- +# Usage with SvelteKit + +It is possible to implement FSD in a SvelteKit project, but conflicts arise due to the differences between the structure requirements of a SvelteKit project and the principles of FSD: + +- Initially, SvelteKit offers a file structure inside the `src/routes` folder, while in FSD the routing must be part of the `app` layer. +- SvelteKit suggests putting everything not related to routing in the `src/lib` folder. + + +## Let's set up the config + +```ts title="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; +``` + +## Move file routing to `src/app`. + +Let's create an app layer, move the app's entry point `index.html` into it, and create a routes folder. +Thus, your file structure should look like this: + +```sh +├── src +│ ├── app +│ │ ├── index.html +│ │ ├── routes +│ ├── pages # FSD Pages folder +``` + +Now, you can create routes for pages within `app` and connect pages from `pages` to them. + +For example, to add a home page to your project, you need to do the following steps: +- Add a page slice inside the `pages` layer +- Add the corresponding rooute to the `routes` folder from the `app` layer +- Align the page from the slice with the rooute + +To create a page slice, let's use the [CLI](https://github.com/feature-sliced/cli): + +```shell +fsd pages home +``` + +Create a ``home-page.svelte`` file inside the ui segment, access it using the Public API + +```ts title="src/pages/home/index.ts" +export { default as HomePage } from './ui/home-page.svelte'; +``` + +Create a route for this page inside the `app` layer: + +```sh + +├── src +│ ├── app +│ │ ├── routes +│ │ │ ├── +page.svelte +│ │ ├── index.html +│ ├── pages +│ │ ├── home +│ │ │ ├── ui +│ │ │ │ ├── home-page.svelte +│ │ │ ├── index.ts +``` + +Add your page component inside the `+page.svelte` file: + +```html title="src/app/routes/+page.svelte" + + + + +``` + +## See also. + +- [Documentation on changing directory config in SvelteKit](https://kit.svelte.dev/docs/configuration#files) + + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/intro.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/intro.mdx new file mode 100644 index 000000000..7f08f72a0 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/intro.mdx @@ -0,0 +1,69 @@ +--- +sidebar_position: 1 +slug: / +pagination_next: get-started/index +--- + +# 文档 + +![feature-sliced-banner](/img/banner.jpg) + +**Feature-Sliced Design**(FSD)是一种用于构建前端应用程序的架构方法论。简单来说,它是组织代码的规则和约定的汇编。该方法论的主要目的是在不断变化的业务需求面前,使项目更加易于理解和结构化。 + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { RocketOutlined, ThunderboltOutlined, FundViewOutlined } from "@ant-design/icons"; +import Link from "@docusaurus/Link"; + + + + + +
+ + + + + + + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/reference/index.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/reference/index.mdx new file mode 100644 index 000000000..dc904866c --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/reference/index.mdx @@ -0,0 +1,33 @@ +--- +sidebar_position: 0 +hide_table_of_contents: true +pagination_prev: guides/index +--- + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { ApiOutlined, GroupOutlined, AppstoreOutlined, NodeIndexOutlined } from "@ant-design/icons"; + +# 📚 参考 + +

+Feature-Sliced Design 关键概念的详细描述。 +

+ + + + diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/reference/layers.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/reference/layers.mdx new file mode 100644 index 000000000..2236f8883 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/reference/layers.mdx @@ -0,0 +1,154 @@ +--- +sidebar_position: 1 +pagination_next: reference/slices-segments +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +# 层 + +层是 Feature-Sliced Design 中组织层次结构的第一级。它们的目的是根据代码需要的责任程度以及它依赖应用程序中其他模块的程度来分离代码。每一层都承载着特殊的语义意义,帮助您确定应该为您的代码分配多少责任。 + +总共有 **7 个 layers**,按从最高责任和 依赖到最低排列: + +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. +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. + +1. App +2. Processes (deprecated) +3. Pages +4. Widgets +5. Features +6. Entities +7. Shared + +您不必在项目中使用每一层 — 只有当您认为它们为您的项目带来价值时才添加它们。通常,大多数前端项目至少会有 Shared、Pages 和 App 层。 + +在实践中,层是具有小写名称的文件夹(例如,`📁 shared`、`📁 pages`、`📁 app`)。_不建议_添加新层,因为它们的语义是标准化的。 + +## 层上的导入规则 + +层由 _slices_ 组成 — 高度内聚的模块组。slices 之间的依赖关系由**层上的导入规则**调节: + +> _slice 中的模块(文件)只能在其他 slices 位于严格较低的层时导入它们。_ + +例如,文件夹 `📁 ~/features/aaa` 是一个名为"aaa"的 slice。其中的文件 `~/features/aaa/api/request.ts` 不能从 `📁 ~/features/bbb` 中的任何文件导入代码,但可以从 `📁 ~/entities` 和 `📁 ~/shared` 导入代码,以及从 `📁 ~/features/aaa` 导入任何同级代码,例如 `~/features/aaa/lib/cache.ts`。 + +App 和 Shared 层是此规则的**例外** — 它们既是层又是 slice。Slices 按业务域划分代码,这两层是例外,因为 Shared 没有业务域,而 App 结合了所有业务域。 + +在实践中,这意味着 App 和 Shared 层由 segments 组成,segments 可以自由地相互导入。 + +## 层定义 + +本节描述每一层的语义含义,以便直观地了解什么样的代码属于那里。 + +### Shared + +这一层为应用程序的其余部分奠定了基础。这是与外部世界建立连接的地方,例如后端、第三方库、环境。这也是定义您自己的高度封装库的地方。 + +这一层,像 App 层一样,_不包含 slices_。Slices 旨在将层划分为业务域,但业务域在 Shared 中不存在。这意味着 Shared 中的所有文件都可以相互引用和导入。 + +Here are the segments that you can typically find in this layer: + +- `📁 api` — the API client and potentially also functions to make requests to specific backend endpoints. +- `📁 ui` — the application's UI kit. + Components on this layer should not contain business logic, but it's okay for them to be business-themed. For example, you can put the company logo and page layout here. Components with UI logic are also allowed (for example, autocomplete or a search bar). +- `📁 lib` — a collection of internal libraries. + This folder should not be treated as helpers or utilities ([read here why these folders often turn into a dump][ext-sova-utility-dump]). Instead, every library in this folder should have one area of focus, for example, dates, colors, text manipulation, etc. That area of focus should be documented in a README file. The developers in your team should know what can and cannot be added to these libraries. +- `📁 config` — environment variables, global feature flags and other global configuration for your app. +- `📁 routes` — route constants or patterns for matching routes. +- `📁 i18n` — setup code for translations, global translation strings. + +You are free to add more segments, but make sure that the name of these segments describes the purpose of the content, not its essence. For example, `components`, `hooks`, and `types` are bad segment names because they aren't that helpful when you're looking for code. + +### Entities + +Slices on this layer represent concepts from the real world that the project is working with. Commonly, they are the terms that the business uses to describe the product. For example, a social network might work with business entities like User, Post, and Group. + +An entity slice might contain the data storage (`📁 model`), data validation schemas (`📁 model`), entity-related API request functions (`📁 api`), as well as the visual representation of this entity in the interface (`📁 ui`). The visual representation doesn't have to produce a complete UI block — it is primarily meant to reuse the same appearance across several pages in the app, and different business logic may be attached to it through props or slots. + +#### Entity relationships + +Entities in FSD are slices, and by default, slices cannot know about each other. In real life, however, entities often interact with each other, and sometimes one entity owns or contains other entities. Because of that, the business logic of these interactions is preferably kept in higher layers, like Features or Pages. + +When one entity's data object contains other data objects, usually it's a good idea to make the connection between the entities explicit and side-step the slice isolation by making a cross-reference API with the `@x` notation. The reason is that connected entities need to be refactored together, so it's best to make the connection impossible to miss. + +For example: + +```ts title="entities/artist/model/artist.ts" +import type { Song } from "entities/song/@x/artist"; + +export interface Artist { + name: string; + songs: Array; +} +``` + +```ts title="entities/song/@x/artist.ts" +export type { Song } from "../model/song.ts"; +``` + +Learn more about the `@x` notation in the [Public API for cross-imports][public-api-for-cross-imports] section. + +### Features + +This layer is for the main interactions in your app, things that your users care to do. These interactions often involve business entities, because that's what the app is about. + +A crucial principle for using the Features layer effectively is: **not everything needs to be a feature**. A good indicator that something needs to be a feature is the fact that it is reused on several pages. + +For example, if the app has several editors, and all of them have comments, then comments are a reused feature. Remember that slices are a mechanism for finding code quickly, and if there are too many features, the important ones are drowned out. + +Ideally, when you arrive in a new project, you would discover its functionality by looking through the pages and features. When deciding on what should be a feature, optimize for the experience of a newcomer to the project to quickly discover large important areas of code. + +A feature slice might contain the UI to perform the interaction like a form (`📁 ui`), the API calls needed to make the action (`📁 api`), validation and internal state (`📁 model`), feature flags (`📁 config`). + +### Widgets + +The Widgets layer is intended for large self-sufficient blocks of UI. Widgets are most useful when they are reused across multiple pages, or when the page that they belong to has multiple large independent blocks, and this is one of them. + +If a block of UI makes up most of the interesting content on a page, and is never reused, it **should not be a widget**, and instead it should be placed directly inside that page. + +:::tip + +If you're using a nested routing system (like the router of [Remix][ext-remix]), it may be helpful to use the Widgets layer in the same way as a flat routing system would use the Pages layer — to create full router blocks, complete with related data fetching, loading states, and error boundaries. + +In the same way, you can store page layouts on this layer. + +::: + +### Pages + +Pages are what makes up websites and applications (also known as screens or activities). One page usually corresponds to one slice, however, if there are several very similar pages, they can be grouped into one slice, for example, registration and login forms. + +There's no limit to how much code you can place in a page slice as long as your team still finds it easy to navigate. If a UI block on a page is not reused, it's perfectly fine to keep it inside the page slice. + +In a page slice you can typically find the page's UI as well as loading states and error boundaries (`📁 ui`) and the data fetching and mutating requests (`📁 api`). It's not common for a page to have a dedicated data model, and tiny bits of state can be kept in the components themselves. + +### Processes + +:::caution + +This layer has been deprecated. The current version of the spec recommends avoiding it and moving its contents to `features` and `app` instead. + +::: + +Processes are escape hatches for multi-page interactions. + +This layer is deliberately left undefined. Most applications should not use this layer, and keep router-level and server-level logic on the App layer. Consider using this layer only when the App layer grows large enough to become unmaintainable and needs unloading. + +### App + +All kinds of app-wide matters, both in the technical sense (e.g., context providers) and in the business sense (e.g., analytics). + +This layer usually doesn't contain slices, as well as Shared, instead having segments directly. + +Here are the segments that you can typically find in this layer: + +- `📁 routes` — the router configuration +- `📁 store` — global store configuration +- `📁 styles` — global styles +- `📁 entrypoint` — the entrypoint to the application code, framework-specific + +[public-api-for-cross-imports]: /docs/reference/public-api#public-api-for-cross-imports +[ext-remix]: https://remix.run +[ext-sova-utility-dump]: https://dev.to/sergeysova/why-utils-helpers-is-a-dump-45fo diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/reference/public-api.md b/i18n/zh/docusaurus-plugin-content-docs/current/reference/public-api.md new file mode 100644 index 000000000..48f89cd7c --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/reference/public-api.md @@ -0,0 +1,155 @@ +--- +sidebar_position: 3 +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +# Public API + +Public API 是一组模块(如 slice)与使用它的代码之间的_契约_。它也充当网关,只允许访问某些对象,并且只能通过该 public API 访问。 + +在实践中,它通常作为具有重新导出的 index 文件实现: + +```js title="pages/auth/index.js" +export { LoginPage } from "./ui/LoginPage"; +export { RegisterPage } from "./ui/RegisterPage"; +``` + +## 什么构成了好的 public API? + +好的 public API 使得使用和集成到其他代码中的 slice 方便可靠。这可以通过设定这三个目标来实现: + +1. 应用程序的其余部分必须受到保护,免受 slice 结构变化(如重构)的影响 +1. slice 行为的重大变化(破坏了之前的期望)应该导致 public API 的变化 +1. 只应该暴露 slice 的必要部分 + +最后一个目标有一些重要的实际含义。创建所有内容的通配符重新导出可能很诱人,特别是在 slice 的早期开发中,因为您从文件中导出的任何新对象也会自动从 slice 导出: + +```js title="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 +``` + +这会损害 slice 的可发现性,因为您无法轻易地说出这个 slice 的接口是什么。不知道接口意味着您必须深入挖掘 slice 的代码才能理解如何集成它。另一个问题是您可能意外地暴露模块内部,如果有人开始依赖它们,这将使重构变得困难。 + +## 用于交叉导入的 Public API {#public-api-for-cross-imports} + +交叉导入是指同一 layer 上的一个 slice 从另一个 slice 导入的情况。通常这被 [layers 上的导入规则][import-rule-on-layers] 禁止,但经常有合理的交叉导入理由。例如,业务 entities 在现实世界中经常相互引用,最好在代码中反映这些关系而不是绕过它们。 + +为此,有一种特殊的 public API,也称为 `@x` 记号法。如果您有 entities A 和 B,并且 entity B 需要从 entity A 导入,那么 entity A 可以为 entity B 声明一个单独的 public API。 + +- `📂 entities` + - `📂 A` + - `📂 @x` + - `📄 B.ts` — 仅用于 `entities/B/` 内部代码的特殊 public API + - `📄 index.ts` — 常规 public API + +然后 `entities/B/` 内部的代码可以从 `entities/A/@x/B` 导入: + +```ts +import type { EntityA } from "entities/A/@x/B"; +``` + +记号法 `A/@x/B` 旨在读作 "A crossed with B"。 + +:::note + +尽量减少交叉导入,并且**仅在 Entities layer 上使用此记号法**,在该 layer 上消除交叉导入通常是不合理的。 + +::: + +## index 文件的问题 + +像 `index.js` 这样的 index 文件(也称为 barrel 文件)是定义 public API 的最常见方式。它们容易制作,但众所周知会在某些打包器和框架中引起问题。 + +### 循环导入 + +循环导入是指两个或多个文件在一个循环中相互导入。 + + + +
+ Three files importing each other in a circle + Three files importing each other in a circle +
+ Pictured above: three files, `fileA.js`, `fileB.js`, and `fileC.js`, importing each other in a circle. +
+
+ +这些情况对于打包器来说通常难以处理,在某些情况下,它们甚至可能导致难以调试的运行时错误。 + +循环导入可以在没有 index 文件的情况下发生,但拥有 index 文件提供了意外创建循环导入的明显机会。当您在 slice 的 public API 中有两个暴露的对象时,这经常发生,例如 `HomePage` 和 `loadUserStatistics`,并且 `HomePage` 需要访问 `loadUserStatistics`,但它像这样做: + +```jsx title="pages/home/ui/HomePage.jsx" +import { loadUserStatistics } from "../"; // importing from pages/home/index.js + +export function HomePage() { /* … */ } +``` + +```js title="pages/home/index.js" +export { HomePage } from "./ui/HomePage"; +export { loadUserStatistics } from "./api/loadUserStatistics"; +``` + +这种情况创建了循环导入,因为 `index.js` 导入 `ui/HomePage.jsx`,但 `ui/HomePage.jsx` 导入 `index.js`。 + +为了防止这个问题,考虑这两个原则。如果您有两个文件,其中一个从另一个导入: +- 当它们在同一个 slice 中时,始终使用_相对_导入并编写完整的导入路径 +- 当它们在不同的 slices 中时,始终使用_绝对_导入,例如使用别名 + +### Shared 中的大型包和损坏的 tree-shaking {#large-bundles} + +当您有一个重新导出所有内容的 index 文件时,某些打包器可能在 tree-shaking(移除未导入的代码)方面遇到困难。 + +通常这对于 public APIs 来说不是问题,因为模块的内容通常关系非常密切,所以您很少需要导入一个东西并 tree-shake 掉另一个。然而,当 FSD 中的正常 public API 规则可能导致问题时,有两个非常常见的情况 — `shared/ui` 和 `shared/lib`。 + +这两个文件夹都是不相关事物的集合,通常不是在一个地方都需要的。例如,`shared/ui` 可能为 UI 库中的每个组件都有模块: + +- `📂 shared/ui/` + - `📁 button` + - `📁 text-field` + - `📁 carousel` + - `📁 accordion` + +当其中一个模块有重度依赖时,这个问题会变得更加严重,比如语法突出显示器或拖放库。您不希望将这些引入到使用 `shared/ui` 中某些内容的每个页面中,例如按钮。 + +如果您的包由于 `shared/ui` 或 `shared/lib` 中的单个 public API 而不必要地增长,建议改为为每个组件或库单独有一个 index 文件: + +- `📂 shared/ui/` + - `📂 button` + - `📄 index.js` + - `📂 text-field` + - `📄 index.js` + +然后这些组件的使用者可以像这样直接导入它们: + +```js title="pages/sign-in/ui/SignInPage.jsx" +import { Button } from '@/shared/ui/button'; +import { TextField } from '@/shared/ui/text-field'; +``` + +### 对绝过 public API 没有真正的保护 + +当您为 slice 创建 index 文件时,您实际上并没有禁止任何人不使用它而直接导入。这对于自动导入来说尤其是一个问题,因为有几个位置可以导入对象,所以 IDE 必须为您做决定。有时它可能选择直接导入,破坏 slices 上的 public API 规则。 + +为了自动捕获这些问题,我们建议使用 [Steiger][ext-steiger],一个具有 Feature-Sliced Design 规则集的架构 linter。 + +### 大型项目中打包器的较差性能 + +在项目中具有大量 index 文件可能会减慢开发服务器,正如 TkDodo 在[他的文章“请停止使用 Barrel 文件”][ext-please-stop-using-barrel-files]中所指出的。 + +您可以做几件事来解决这个问题: +1. 与[“Shared 中的大型包和损坏的 tree-shaking”问题](#large-bundles)相同的建议 — 在 `shared/ui` 和 `shared/lib` 中为每个组件/库单独有 index 文件,而不是一个大的 +2. 避免在有 slices 的 layers 上的 segments 中有 index 文件。 + 例如,如果您有一个用于 feature “comments” 的 index,`📄 features/comments/index.js`,则没有理由为该 feature 的 `ui` segment 有另一个 index,`📄 features/comments/ui/index.js`。 +3. 如果您有一个非常大的项目,很可能您的应用程序可以分割成几个大块。 + 例如,Google Docs 在文档编辑器和文件浏览器方面有非常不同的责任。您可以创建一个 monorepo 设置,其中每个包都是一个单独的 FSD 根,具有自己的 layers 集。某些包可能只有 Shared 和 Entities layers,其他包可能只有 Pages 和 App,还有一些包可能包含它们自己的小 Shared,但仍然使用另一个包中的大 Shared。 + + + + + +[import-rule-on-layers]: /docs/reference/layers#import-rule-on-layers +[ext-steiger]: https://github.com/feature-sliced/steiger +[ext-please-stop-using-barrel-files]: https://tkdodo.eu/blog/please-stop-using-barrel-files diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/reference/slices-segments.mdx b/i18n/zh/docusaurus-plugin-content-docs/current/reference/slices-segments.mdx new file mode 100644 index 000000000..2121a3203 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current/reference/slices-segments.mdx @@ -0,0 +1,73 @@ +--- +title: Slices and segments +sidebar_position: 2 +pagination_next: reference/public-api +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +# Slices 和 segments + +## Slices + +Slices 是 Feature-Sliced Design 组织层次结构中的第二级。它们的主要目的是按其对产品、业务或应用程序的意义对代码进行分组。 + +Slices 的名称没有标准化,因为它们直接由您应用程序的业务领域决定。例如,照片库可能有 slices `photo`、`effects`、`gallery-page`。社交网络将需要不同的 slices,例如 `post`、`comments`、`news-feed`。 + +Layers Shared 和 App 不包含 slices。这是因为 Shared 应该不包含任何业务逻辑,因此对产品没有意义,而 App 应该只包含涉及整个应用程序的代码,所以不需要分割。 + +### 零耦合和高聚合 {#zero-coupling-high-cohesion} + +Slices 旨在成为独立且高度聚合的代码文件组。下面的图形可能有助于可视化_聚合性_和_耦合性_这些复杂的概念: + +
+ + +
+ Image inspired by https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/ +
+
+ +理想的 slice 独立于其 layer 上的其他 slices(零耦合)并包含与其主要目标相关的大部分代码(高聚合)。 + +The independence of slices is enforced by the [import rule on layers][layers--import-rule]: + +> _A module (file) in a slice can only import other slices when they are located on layers strictly below._ + +### Public API rule on slices + +Inside a slice, the code could be organized in any way that you want. That doesn't pose any issues as long as the slice provides a good public API for other slices to use it. This is enforced with the **public API rule on slices**: + +> _Every slice (and segment on layers that don't have slices) must contain a public API definition._ +> +> _Modules outside of this slice/segment can only reference the public API, not the internal file structure of the slice/segment._ + +Read more about the rationale of public APIs and the best practices on creating one in the [Public API reference][ref-public-api]. + +### Slice groups + +Closely related slices can be structurally grouped in a folder, but they should exercise the same isolation rules as other slices — there should be **no code sharing** in that 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.](/img/graphic-nested-slices.svg) + +## Segments + +Segments are the third and final level in the organizational hierarchy, and their purpose is to group code by its technical nature. + +There a few standardized segment names: + +- `ui` — everything related to UI display: UI components, date formatters, styles, etc. +- `api` — backend interactions: request functions, data types, mappers, etc. +- `model` — the data model: schemas, interfaces, stores, and business logic. +- `lib` — library code that other modules on this slice need. +- `config` — configuration files and feature flags. + +See the [Layers page][layers--layer-definitions] for examples of what each of these segments might be used for on different layers. + +You can also create custom segments. The most common places for custom segments are the App layer and the Shared layer, where slices don't make sense. + +Make sure that the name of these segments describes the purpose of the content, not its essence. For example, `components`, `hooks`, and `types` are bad segment names because they aren't that helpful when you're looking for code. + +[layers--layer-definitions]: /docs/reference/layers#layer-definitions +[layers--import-rule]: /docs/reference/layers#import-rule-on-layers +[ref-public-api]: /docs/reference/public-api diff --git a/i18n/zh/docusaurus-theme-classic/footer.json b/i18n/zh/docusaurus-theme-classic/footer.json new file mode 100644 index 000000000..87da56740 --- /dev/null +++ b/i18n/zh/docusaurus-theme-classic/footer.json @@ -0,0 +1,66 @@ +{ + "link.title.Specs": { + "message": "规范", + "description": "The title of the footer links column with title=Specs in the footer" + }, + "link.title.Community": { + "message": "社区", + "description": "The title of the footer links column with title=Community in the footer" + }, + "link.title.More": { + "message": "更多", + "description": "The title of the footer links column with title=More in the footer" + }, + "link.item.label.Documentation": { + "message": "文档", + "description": "The label of footer link with label=Documentation linking to /docs" + }, + "link.item.label.Discussions": { + "message": "讨论", + "description": "The label of footer link with label=Discussions linking to https://github.com/feature-sliced/documentation/discussions" + }, + "link.item.label.Community": { + "message": "社区", + "description": "The label of the footer link with label=Community linking to /community" + }, + "link.item.label.Help": { + "message": "帮助", + "description": "The label of the footer link with label=Help linking to /nav" + }, + "link.item.label.License": { + "message": "许可证", + "description": "The label of footer link with label=License linking to LICENSE" + }, + "link.item.label.Contribution Guide (RU)": { + "message": "贡献指南(俄文)", + "description": "The label of footer link with label=Contribution Guide (RU) linking to CONTRIBUTING.md" + }, + "link.item.label.Discord": { + "message": "Discord", + "description": "The label of footer link with label=Discord linking to https://discord.com/invite/S8MzWTUsmp" + }, + "link.item.label.Telegram": { + "message": "Telegram", + "description": "The label of footer link with label=Telegram linking to https://t.me/feature_sliced" + }, + "link.item.label.Twitter": { + "message": "Twitter", + "description": "The label of footer link with label=Twitter linking to https://twitter.com/feature_sliced" + }, + "link.item.label.Open Collective": { + "message": "Open Collective", + "description": "The label of footer link with label=Open Collective linking to https://opencollective.com/feature-sliced" + }, + "link.item.label.YouTube": { + "message": "YouTube", + "description": "The label of footer link with label=YouTube linking to https://www.youtube.com/c/FeatureSlicedDesign" + }, + "link.item.label.GitHub": { + "message": "GitHub", + "description": "The label of footer link with label=GitHub linking to https://github.com/feature-sliced" + }, + "copyright": { + "message": "版权所有 © 2018-2025 Feature-Sliced Design", + "description": "The footer copyright" + } +} diff --git a/i18n/zh/docusaurus-theme-classic/navbar.json b/i18n/zh/docusaurus-theme-classic/navbar.json new file mode 100644 index 000000000..1beacfd8a --- /dev/null +++ b/i18n/zh/docusaurus-theme-classic/navbar.json @@ -0,0 +1,50 @@ +{ + "title": { + "message": "", + "description": "The title in the navbar" + }, + "item.label.🛠 Examples": { + "message": "🛠 示例", + "description": "Navbar item with label Examples" + }, + "item.label.📖 Docs": { + "message": "📖 文档", + "description": "Navbar item with label Docs" + }, + "item.label.🔎 Intro": { + "message": "🔎 介绍", + "description": "Navbar item with label Intro" + }, + "item.label.🚀 Get Started": { + "message": "🚀 开始使用", + "description": "Navbar item with label Get Started" + }, + "item.label.🧩 Concepts": { + "message": "🧩 概念", + "description": "Navbar item with label Concepts" + }, + "item.label.🎯 Guides": { + "message": "🎯 指南", + "description": "Navbar item with label Guides" + }, + "item.label.📚 Reference": { + "message": "📚 参考", + "description": "Navbar item with label Reference" + }, + "item.label.🍰 About": { + "message": "🍰 关于", + "description": "Navbar item with label About" + }, + "item.label.💫 Community": { + "message": "💫 社区", + "description": "Navbar item with label Community" + }, + "item.label.❔ Help": { + "message": "❔ 帮助", + "description": "Navbar item with label Help" + }, + "item.label.📝 Blog": { + "message": "📝 博客", + "description": "Navbar item with label Blog" + } +} diff --git a/package.json b/package.json index d810e32f1..fa054de79 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "start:uz": "docusaurus start --locale uz", "start:kr": "docusaurus start --locale kr", "start:ja": "docusaurus start --locale ja", + "start:zh": "docusaurus start --locale zh", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "test": "pnpm run test:lint && pnpm run build",