diff --git a/DEV.md b/DEV.md index c9c70bdf51..c540678a38 100644 --- a/DEV.md +++ b/DEV.md @@ -7,6 +7,7 @@ This website is built using [Docusaurus 2](https://docusaurus.io/), a modern sta - [Russian docs version](i18n/ru) - [English docs version](i18n/en) - [Uzbek docs version](i18n/uz) +- [Japanese docs version](i18n/ja) ## Installation @@ -20,6 +21,8 @@ pnpm install pnpm start # for default locale pnpm start:ru # for RU locale pnpm start:en # for EN locale +pnpm start:uz # for UZ locale +pnpm start:ja # for JA locale ``` > About [docusaurus/i18n commands](https://docusaurus.io/docs/i18n/git#translate-the-files) diff --git a/config/docusaurus/i18n.js b/config/docusaurus/i18n.js index bea96e6a50..2cef5f9050 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"], + locales: ["ru", "en", "uz", "kr", "ja"], localeConfigs: { ru: { label: "Русский", @@ -17,6 +17,9 @@ const i18n = { kr: { label: "한국어", }, + ja: { + label: "日本語", + }, }, }; diff --git a/i18n/ja/code.json b/i18n/ja/code.json new file mode 100644 index 0000000000..738036b4d7 --- /dev/null +++ b/i18n/ja/code.json @@ -0,0 +1,410 @@ +{ + "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 methodology" + }, + "pages.home.companies.add_me": { + "message": "あなたの会社でFSDが使用されていますか?", + "description": "Methodology 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": "FSDを使って作られたプロジェクト一覧", + "description": "Page subtitle" + }, + "pages.examples.add_me.title": { + "message": "実装例を追加", + "description": "Request to add example" + }, + "pages.examples.repo.title": { + "message": "リポジトリ", + "description": "Examples repository label" + }, + "pages.examples.versions": { + "message": "バージョン一覧も参照してください", + "description": "Versions reminder" + }, + "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": "ここで古いバージョンのドキュメントを見つけることができます", + "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/7p4anU2shHAzmfqc8", + "description": "Feedback share form url" + }, + "features.feedback-doc.thanks": { + "message": "フィードバックありがとうございます!", + "description": "DocFeedback block=Thanks" + }, + "features.feedback-doc.title": { + "message": "このページは役に立ちましたか?", + "description": "DocFeedback block=Title" + }, + "features.feedback-doc.subtitle": { + "message": "あなたのフィードバックはドキュメントの改善に役立ちます", + "description": "DocFeedback block=Subtitle" + }, + "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 the feedback form submission fails with 403 error" + }, + "features.feedback-doc.modal-title-error-404": { + "message": "提供されたプロジェクトIDをPushFeedbackで見つけることができませんでした。", + "description": "The title of the modal displayed when the 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 displayed when the feedback form is opened" + }, + "features.feedback-doc.modal-title-error": { + "message": "おっと!", + "description": "The title of the modal displayed when the feedback form submission fails" + }, + "features.feedback-doc.modal-title-success": { + "message": "フィードバックありがとうございます!", + "description": "The title of the modal displayed when the feedback form submission is successful" + }, + "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.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 of the screenshot tool" + }, + "features.feedback-doc.send-button-text": { + "message": "送信", + "description": "The text on a button to send feedback" + }, + "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": "(チャットURL)", + "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": "リンク元のサイトの所有者に連絡して、リンクが機能しないことを知らせてください。", + "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": "{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": "{count} 投稿|{count} 投稿|{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/ja/docusaurus-plugin-content-docs/community/index.mdx b/i18n/ja/docusaurus-plugin-content-docs/community/index.mdx new file mode 100644 index 0000000000..713ae1b412 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/community/index.mdx @@ -0,0 +1,40 @@ +--- +hide_table_of_contents: true +--- + +# 💫 コミュニティ + +

+コミュニティリソースと追加資料 +

+ +## メイン {#main} + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { StarOutlined, SearchOutlined, TeamOutlined, VerifiedOutlined } from "@ant-design/icons"; + + + + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/community/team.mdx b/i18n/ja/docusaurus-plugin-content-docs/community/team.mdx new file mode 100644 index 0000000000..718318bd8c --- /dev/null +++ b/i18n/ja/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' + +# チーム + + + +## コアチーム + +### チャンピオンズ {#champions} + +## コントリビューター {#contributors} + +## 会社 {#companies} \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current.json b/i18n/ja/docusaurus-plugin-content-docs/current.json new file mode 100644 index 0000000000..d991f18054 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,54 @@ +{ + "version.label": { + "message": "v2.0.0 🍰", + "description": "現在のバージョンのラベル" + }, + "sidebar.getstartedSidebar.category.🚀 Get Started": { + "message": "🚀 はじめに", + "description": "サイドバーの「はじめに」カテゴリのラベル" + }, + "sidebar.guidesSidebar.category.🎯 Guides": { + "message": "🎯 ガイド", + "description": "サイドバーの「ガイド」カテゴリのラベル" + }, + "sidebar.referenceSidebar.category.📚 Reference": { + "message": "📚 参考書", + "description": "サイドバーの「参考書」カテゴリのラベル" + }, + "sidebar.aboutSidebar.category.🍰 About": { + "message": "🍰 メソッドについて", + "description": "サイドバーの「メソッドについて」カテゴリのラベル" + }, + "sidebar.aboutSidebar.category.Understanding": { + "message": "理解", + "description": "サイドバーの「理解」カテゴリのラベル" + }, + "sidebar.aboutSidebar.category.Promote": { + "message": "推進", + "description": "サイドバーの「推進」カテゴリのラベル" + }, + "sidebar.guidesSidebar.category.Examples": { + "message": "実装例", + "description": "サイドバーの「例」カテゴリのラベル" + }, + "sidebar.guidesSidebar.category.Migration": { + "message": "移行", + "description": "サイドバーの「移行」カテゴリのラベル" + }, + "sidebar.guidesSidebar.category.Tech": { + "message": "技術", + "description": "サイドバーの「技術」カテゴリのラベル" + }, + "sidebar.referenceSidebar.category.Units": { + "message": "ユニット", + "description": "サイドバーの「ユニット」カテゴリのラベル" + }, + "sidebar.referenceSidebar.category.Isolation": { + "message": "分離", + "description": "サイドバーの「分離」カテゴリのラベル" + }, + "sidebar.referenceSidebar.category.Layer": { + "message": "レイヤー", + "description": "サイドバーの「レイヤー」カテゴリのラベル" + } +} diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/alternatives.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/about/alternatives.mdx new file mode 100644 index 0000000000..4074abbdf7 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/alternatives.mdx @@ -0,0 +1,103 @@ +--- +sidebar_class_name: sidebar-item--wip +sidebar_position: 3 +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 代替案 + + + +## ビッグボールオブマッド + + + +- [(記事) DDD - Big Ball of mud](https://thedomaindrivendesign.io/big-ball-of-mud/) + + +## スマート&ダムコンポーネント + + + +- [(記事) Dan Abramov - Presentational and Container Components (TLDR: 非推奨)](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) + + +## デザイン原則 + + + +## DDD + + + +## 参照 {#see-also} + +- [(記事) 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/) + +## クリーンアーキテクチャ + + + +- [(記事) 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/) + +## フレームワーク + + + +- [(記事) FSDの作成理由 (フレームワークに関する断片)](/docs/about/motivation) + + +## Atomic Design + +### これは何か? + +アトミックデザインでは、責任の範囲が標準化された層に分かれています。 + +アトミックデザインは**5つの層**に分かれます(上から下へ)。 + +1. `pages` - FSDの`pages`層と同様の目的を持つ。 +2. `templates` - コンテンツに依存しないページの構造を定義するコンポーネント。 +3. `organisms` - ビジネスロジックを持つ分子から構成されるモジュール。 +4. `molecules` - 通常、ビジネスロジックを持たないより複雑なコンポーネント。 +5. `atoms` - ビジネスロジックを持たないUIコンポーネント。 + +同じ層のモジュールは、FSDのように下の層のモジュールとだけ相互作用しています。 +つまり、分子(molecule)は原子(atom)から構築され、生命体(organism)は分子から、テンプレート(template)は生命体から、ページ(page)はテンプレートから構築されます。 +また、アトミックデザインはモジュール内での**公開API**の使用を前提としています。 + +### フロントエンドでの適用性 +アトミックデザインはプロジェクトで比較的よく見られます。アトミックデザインは、開発者の間というより、ウェブデザイナーの間で人気です。ウェブデザイナーは、スケーラブルでメンテナンスしやすいデザインを作成するためにアトミックデザインをよく使用しています。 +開発では、アトミックデザインは他のアーキテクチャ設計方法論と混合されることがよくあります。 + +しかし、アトミックデザインはUIコンポーネントとその構成に焦点を当てているため、 +アーキテクチャ内でビジネスロジックを実装する問題が発生してしまいます。 + +問題は、アトミックデザインがビジネスロジックのための明確な責任レベルを提供していないため、 +ビジネスロジックがさまざまなコンポーネントやレベルに分散され、メンテナンスやテストが複雑になることです。 +ビジネスロジックは曖昧になり、責任の明確な分離が困難になり、コードがモジュール化されず再利用可能でなくなります。 + +### FSDとの統合 +FSDの文脈では、アトミックデザインのいくつかの要素を使用して柔軟でスケーラブルなUIコンポーネントを作成することができます。 `atoms`と`molecules`の層は、FSDの`shared/ui`に実装でき、基本的なUI要素の再利用とメンテナンスを簡素化しています。 + +```sh +├── shared +│   ├── ui  +│   │   ├── atoms +│   │   ├── molecules +│   ... +``` + +FSDとアトミックデザインの比較は、両方の設計方法論がモジュール性と再利用性を目指していることを示していますが、 +異なる側面に焦点を当てています。アトミックデザインは視覚的コンポーネントとその構成に焦点を当てています。 +FSDはアプリケーションの機能を独立したモジュールに分割し、それらの相互関係に焦点を当てています。 + +- [Atomic Design](https://atomicdesign.bradfrost.com/table-of-contents/) +- [(動画) Atomic Design: What is it and why is it important?](https://youtu.be/Yi-A20x2dcA) + +## Feature Driven + + + +- [(講演) Feature Driven Architecture - Oleg Isonen](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/ja/docusaurus-plugin-content-docs/current/about/index.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/about/index.mdx new file mode 100644 index 0000000000..3b573e934b --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/index.mdx @@ -0,0 +1,44 @@ +--- +hide_table_of_contents: true +pagination_prev: reference/index +--- + +# 🍰 FSDについて + +背景指向 + +

+FSD設計方法論、チーム、コミュニティ、そして発展の歴史に関する一般情報 +

+ +## 主な内容 {#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/ja/docusaurus-plugin-content-docs/current/about/mission.md b/i18n/ja/docusaurus-plugin-content-docs/current/about/mission.md new file mode 100644 index 0000000000..b99c74375c --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/mission.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 1 +--- + +# ミッション + +ここでは、私たちがFSD方法論を開発する際に従う方法論適用の制限と目標について説明します。 + +- 私たちは、目標をイデオロギーとシンプルさのバランスとして考えている +- 私たちは、すべての人に適した銀の弾丸を作ることはできない + +**それでも、FSD方法論が広範な開発者にとって近く、アクセス可能であることを望んでいます。** + +## 目標 {#goals} + +### 幅広い開発者に対する直感的な明確さ {#intuitive-clarity-for-a-wide-range-of-developers} + +FSD方法論は、プロジェクトチームの大部分にとってアクセス可能であるべきです。 + +*なぜなら、将来のすべてのツールがあっても、FSD方法論を理解できるのは経験豊富なシニアやリーダーだけでは不十分だからである* + +### 日常的な問題の解決 {#solving-everyday-problems} + +FSD方法論には、プロジェクト開発における日常的な問題の理由と解決策が示されるべきです。 + +また、開発者が*コミュニティーの経験に基づいた*アプローチを使用できるようにし、長年のアーキテクチャや開発の問題を回避できるようにするには、**FSD方法論はこれに関連するツール(CLI、リンター)を提供することも必要です。** + + +> *@sergeysova: 想像してみてください。開発者が方法論に基づいてコードを書いているとき、開発者の直面している問題は10倍少なく発生しています。それは他の人々が多くの問題の解決策を考え出したから、可能になったのです。* + +## 制限 {#limitations} + +私たちは*自分たちの見解を押し付けたくありません*が、同時に*多くの開発者の習慣が日々の開発の妨げになっていることを理解しています。* + +すべての開発者にはシステム設計と開発経験レベルが異なるため、**次のことを理解することが重要です。** + +- FSD方法論は、すべての開発者にとって、同時に非常にシンプルで、非常に明確にするのは不可能 + > *@sergeysova: 一部の概念は、問題に直面し、解決に数年を費やさない限り、直感的に理解することはできない。* + > + > - *数学の例 — グラフ理論。* + > - *物理の例 — 量子力学。* + > - *プログラミングの例 — アプリケーションのアーキテクチャ。* + > +- シンプルさ、拡張性は、実現可能であって望ましい + +## 参照 {#see-also} + +- [アーキテクチャの問題][refs-architecture--problems] + +[refs-architecture--problems]: /docs/about/understanding/architecture#problems diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/motivation.md b/i18n/ja/docusaurus-plugin-content-docs/current/about/motivation.md new file mode 100644 index 0000000000..7394004603 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/motivation.md @@ -0,0 +1,133 @@ +--- +sidebar_position: 2 +--- + +# モチベーション + +**Feature-Sliced Design**の主なアイデアは、さまざまな開発者の経験を議論し、研究結果を統合することに基づいて、複雑で発展するプロジェクトの開発を容易にし、開発コストを削減することです。 + +明らかに、これは銀の弾丸ではなく、当然ながら、FSDには独自の[適用範囲の限界][refs-mission]があります。 + +## 既存の解決策が不足している理由 {#intuitive-clarity-for-a-wide-range-of-developers} + +> 通常、次のような議論があります。 +> +> - *「SOLID」、「KISS」、「YAGNI」、「DDD」、「GRASP」、「DRY」など、すでに確立された設計原則があるのに、なぜ新しい方法論が必要なのか?」* +> - *「プロジェクトのすべての問題は、良いドキュメント、テスト、確立されたプロセスで解決できる」* +> - *「すべての問題は、すべての開発者が上記のすべてに従えば解決される」* +> - *「すでにすべてが考案されているから、あなたはそれを利用できないだけだ」* +> - *\{FRAMEWORK_NAME\}を使えば、すべてが解決される」* + +### 原則だけでは不十分 {#principles-alone-are-not-enough} + +**良いアーキテクチャを設計するためには、原則の存在だけでは不十分です。** + +すべての人が原則を完全に理解しているわけではありません。正しく原則を理解し、適用できる人はさらに少ないです。 + +*設計原則はあまりにも一般的であり、「スケーラブルで柔軟なアプリケーションの構造とアーキテクチャをどのように設計するか?」という具体的な質問に対する答えを提供していません。* + +### プロセスは常に機能するわけではない {#processes-dont-always-work} + +*ドキュメント/テスト/プロセス*を使用するのは、確かに良いことですが、残念ながら、それに多くのコストをかけても、**アーキテクチャの問題や新しい人をプロジェクトに導入する問題を解決することは常にできるわけではありません。** + +- ドキュメントは、しばしば膨大で古くなってしまうので、各開発者のプロジェクトへの参加時間はあまり短縮されない。 +- 誰もが同じようにアーキテクチャを理解しているかを常に監視することは、膨大なリソースを必要とする。 +- bus-factorも忘れないようにしましょう。 + +### 既存のフレームワークはどこでも適用できるわけではない {#existing-frameworks-cannot-be-applied-everywhere} + +- 既存の解決策は通常、高い参入障壁があるため、新しい開発者を見つけるのが難しい。 +- ほとんどの場合、技術の選択はプロジェクトの深刻な問題が発生する前に決定されているため、**技術に依存せずに**、すでにあるもので作業をすることができなければならない。 + +> Q: *「私のプロジェクトでは`React/Vue/Redux/Effector/Mobx/{YOUR_TECH}`を使っていますが、エンティティの構造とそれらの間の関係をどのように構築すればよいでしょうか?」* + +### 結果として {#as-a-result} + +「雪の結晶」のようにユニークなプロジェクトが得られ、それぞれが従業員の長期的な関与を必要とし、他のプロジェクトではほとんど適用できない知識を必要とします。 + +> @sergeysova: *これは、現在のフロントエンド開発の状況そのものであり、各リーダーがさまざまなアーキテクチャやプロジェクトの構造を考案しているが、これらの構造が時間の試練に耐えるかどうかは不明であり、最終的にはリーダー以外の人がプロジェクトを発展させることができるのは最大で2人であり、新しい開発者を再び入れる必要がある。* + +## 開発者にとっての方法論の必要性 {#why-do-developers-need-the-methodology} + +### アーキテクチャの問題ではなくビジネス機能に集中するため {#focus-on-business-features-not-on-architecture-problems} + +FSDは、スケーラブルで柔軟なアーキテクチャの設計にかかるリソースを節約し、開発者の注意を主要な機能開発に向けることを可能にしています。同時に、プロジェクトごとにアーキテクチャの解決策も標準化されます。 + +*別の問題は、FSDがコミュニティの信頼を得る必要があることです。そうすれば、開発者は自分のプロジェクトの問題を解決する際に、与えられた時間内にFSDを理解し、信頼することができます。* + +### 経験に基づく解決策 {#an-experience-proven-solution} + +FSDは、*複雑なビジネスロジックの設計における経験に基づく解決策を目指す開発者*を対象としています。 + +*ただし、FSDは、全体としてベストプラクティスのセット、または特定の問題やケースに関する記事一覧です。したがって、開発や設計の問題に直面する他の開発者にも役立てます。* + +### プロジェクトの健康 {#project-health} + +FSDは、*プロジェクトの問題を事前に解決し、追跡することを可能にし、膨大なリソースを必要としません。* + +**技術的負債は通常、時間とともに蓄積され、その解決の責任はリーダーとチームの両方にあります。** + +FSDは、*スケーリングやプロジェクトの発展における潜在的な問題を事前に警告することを可能にしています。* + +## ビジネスにとってのFSD方法論の必要性 {#why-does-a-business-need-a-methodology} + +### 迅速なオンボーディング {#fast-onboarding} + +FSDを使用すると、**すでにこのアプローチに慣れている人をプロジェクトに雇うことができ、再教育する必要がありません。** + +*人々はより早くプロジェクトに慣れ、貢献し始め、次のプロジェクトのイテレーションで人を見つけるための追加の保証が得られます。* + +### 経験に基づく解決策 {#an-experience-proven-solution-1} + +ビジネスは、プロジェクトの発展における大部分の問題を解決するフレームワーク/解決策を得たいと考えています。FSDにより、ビジネスは*システムの開発中に発生するほとんどの問題に対する解決策を得ることができます。* + +### プロジェクトのさまざまな段階への適用性 {#applicability-for-different-stages-of-the-project} + +FSDは、*プロジェクトのサポートと発展の段階でも、MVPの段階でもプロジェクトに利益をもたらすことができます。* + +はい、MVPでは通常、機能が重要であり、将来のアーキテクチャは重要ではありません。しかし、限られた時間の中で、方法論のベストプラクティスを知っていることで、少ないコストで済むことができ、MVPバージョンのシステムを設計する際に合理的な妥協を見つけることができます(無計画に機能を追加するよりも)。 + +*テストについても同じことが言えます。* + +## 私たちの方法論が必要ない場合 {#when-is-our-methodology-not-needed} + +- プロジェクトが短期間しか存続しない場合 +- プロジェクトがサポートされるアーキテクチャを必要としない場合 +- ビジネスがコードベースと機能の提供速度の関連性を認識しない場合 +- ビジネスができるだけ早く注文を完了することを重視し、さらなるサポートを求めない場合 + +### ビジネスの規模 {#business-size} + +- **小規模ビジネス** - 通常、迅速で即効性のある解決策を必要とします。ビジネスは、成長する(少なくとも中規模に達する)と、顧客が継続的にサービスなどを利用するためには、開発される解決策の品質と安定性に時間をかける必要があることを理解し始めます。 +- **中規模ビジネス** - 通常、開発のすべての問題を理解しており、たとえ機能をできるだけ早くリリースしたい場合でも、品質の改善、リファクタリング、テスト(そしてもちろん、拡張可能なアーキテクチャ)に時間をかけます。 +- **大規模ビジネス** - 通常、すでに広範なオーディエンスを持ち、従業員の数も多く、独自のプラクティスのセットを持っているため、他のアプローチを採用するアイデアはあまり浮かびません。 + +## 目標 {#plans} + +主要な目標の大部分は[ここに記載されています][refs-mission--goals]が、今後のFSD方法論に対する私たちの期待についても話しておく必要があります。 + +### 経験の統合 {#combining-experience} + +現在、私たちは`core-team`のさまざまな経験を統合し、実践に基づいた方法論を得ることを目指しています。 + +もちろん、最終的にはAngular 3.0のようなものを得るかもしれませんが、ここで最も重要なのは、**複雑なシステムのアーキテクチャ設計の問題を探求することです。** + +*そして、現在のFSD方法論のバージョンに対して不満があることは確かですが、私たちはコミュニティの経験も考慮しながら、共通の努力で統一的かつ最適な解決策に到達したいと考えています。* + +### 仕様外の生活 {#life-outside-the-specification} + +すべてがうまくいけば、FSDは仕様やツールキットに限定されることはありません。 + +- 講演や記事があるかもしれない。 +- FSD方法論に従って書かれたプロジェクトの他の技術への移行のための`CODE_MODEs`があるかもしれない。 +- 最終的には、大規模な技術的解決策のメンテイナーに到達できるかもしれない。 + - *特にReactに関しては、他のフレームワークと比較して、これは主な問題である。なぜなら、特定の問題を解決する方法を示さないからである。* + +## 参照 {#see-also} + +- [方法論の使命について:目標と制限][refs-mission] +- [プロジェクトにおける知識の種類][refs-knowledge] + +[refs-mission]: /docs/about/mission +[refs-mission--goals]: /docs/about/mission#goals +[refs-knowledge]: /docs/about/understanding/knowledge-types \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/_category_.yaml b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/_category_.yaml new file mode 100644 index 0000000000..abce1d4fe5 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/_category_.yaml @@ -0,0 +1,3 @@ +label: プロモート +position: 11 +collapsed: true diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/for-company.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/for-company.mdx new file mode 100644 index 0000000000..b8efbc68ca --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/for-company.mdx @@ -0,0 +1,10 @@ +--- +sidebar_position: 4 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 会社での推進 + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/for-team.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/for-team.mdx new file mode 100644 index 0000000000..c484972621 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/for-team.mdx @@ -0,0 +1,10 @@ +--- +sidebar_position: 3 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# チームでの推進 + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/integration.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/integration.mdx new file mode 100644 index 0000000000..b12c4006cf --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/integration.mdx @@ -0,0 +1,16 @@ +--- +sidebar_position: 1 +--- + +# 統合の側面 + +**利点**: +- [概要](/docs/get-started/overview#advantages) +- コードレビュー +- オンボーディング + +**欠点**: +- メンタル的な複雑さ +- 高い参入障壁 +- 「レイヤー地獄」 +- 機能ベースのアプローチにおける典型的な問題 \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/partial-application.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/partial-application.mdx new file mode 100644 index 0000000000..a4fc1d8221 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/promote/partial-application.mdx @@ -0,0 +1,10 @@ +--- +sidebar_position: 2 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 部分的な適用 + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/_category_.yaml b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/_category_.yaml new file mode 100644 index 0000000000..5690b49cfd --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/_category_.yaml @@ -0,0 +1,2 @@ +label: 理解 +position: 3 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/abstractions.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/abstractions.mdx new file mode 100644 index 0000000000..5e7de6f46b --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/abstractions.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 6 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 抽象化 + + + +## 漏れのある抽象化の法則 {#the-law-of-leaky-abstractions} + +## なぜこんなに多くの抽象化があるのか {#why-are-there-so-many-abstractions} + +> 抽象化はプロジェクトの複雑さに対処するのに役立ちます。問題は、これらの抽象化がこのプロジェクトに特有のものになるのか、それともフロントエンドの特性に基づいて一般的な抽象化を導き出そうとするのかということです。 + +> アーキテクチャとアプリケーション全体は元々複雑であり、問題はその複雑さをどのように分配し、記述するかだけです。 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/architecture.md b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/architecture.md new file mode 100644 index 0000000000..2153d05788 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/architecture.md @@ -0,0 +1,94 @@ +--- +sidebar_position: 1 +--- + +# アーキテクチャ + +## 問題 {#problems} + +通常、アーキテクチャについての議論は、プロジェクトの開発が何らかの問題で停滞しているときに持ち上がります。 + +### バスファクターとオンボーディング + +プロジェクトとそのアーキテクチャを理解しているのは限られた人々だけです。 + +**例:** + +- *「新しい人を開発に加えるのが難しい」* +- *「問題があるたびに、各自が異なる回避策を持っている」* +- *「この大きなモノリスの中で何が起こっているのか理解できない」* + +### 暗黙的かつ制御されていない結果 {#implicit-and-uncontrolled-consequences} + +開発やリファクタリングにおいて多くの暗黙的な副作用が発生してしまいます(「すべてがすべてに依存している」)。 + +**例:** + +- *「フィーチャーが他のフィーチャーをインポートしている」* +- *「あるページのストアを更新したら、別のページのフィーチャーが壊れた」* +- *「ロジックがアプリ全体に散らばっていて、どこが始まりでどこが終わりかわからない」* + +### 制御されていないロジックの再利用 {#uncontrolled-reuse-of-logic} + +既存のロジックを再利用したり修正したりするのが難しいです。 + +通常、2つの極端なケースがあります。 + +- 各モジュールごとにロジックを完全にゼロから書く(既存のコードベースに重複が生じる可能性がある) +- すべてのモジュールを`shared`フォルダーに移動し、大きなモジュールの「ごみ屋敷」を作る(ほとんどが一箇所でしか使用されない) + +**例:** + +- *「プロジェクトに同じビジネスロジックの複数の実装があって、毎日その影響を受けている」* +- *「プロジェクトには6つの異なるボタン/ポップアップコンポーネントがある」* +- *「ヘルパー関数の「ごみ屋敷」」* + +## 要件 {#requirements} + +したがって、理想的なアーキテクチャに対する要求を提示するのは、理にかなっています。 + +:::note + +「簡単」と言われるところでは、「広範な開発者にとって相対的に簡単である」という意味です。なぜなら、[すべての人にとって完璧な解決策を作ることはできないからです](/docs/about/mission#limitations)。 + +::: + +### 明示性 + +- チームがプロジェクトとそのアーキテクチャを**簡単に習得し、説明できる**ようにする必要がある +- 構造はプロジェクトの**ビジネス価値**を反映するべきである +- **副作用と抽象化間の関係**が明示されるべきである +- ユニークな実装を妨げず、**ロジックの重複を簡単に発見できる**ようにする必要がある +- プロジェクト全体に**ロジックが散らばってはいけない** +- 良好なアーキテクチャのために**あまりにも多くの異なる抽象化やルールが存在してはならない** + +### 制御 + +- 良好なアーキテクチャは**課題の解決や機能の導入を加速する**べきである +- プロジェクトの開発を**制御**できる必要がある +- コードを**拡張、修正、削除するのが簡単である**べきである +- 機能の**分解と孤立性**が守られるべきである +- システムの各コンポーネントは**簡単に交換可能で削除可能**であるべきである + - *未来を予測することはできないから、[変更に最適化する必要はない][ext-kof-not-modification]* + - *既存のコンテキストに基づいて、[削除に最適化する方が良い][ext-kof-but-removing]* + +### 適応性 + +- 良好なアーキテクチャは、ほとんどのプロジェクトに適用可能であるべきである + - *既存のインフラソリューションと共に* + - *どの発展段階でも* +- フレームワークやプラットフォームに依存してはいけない +- プロジェクトとチームを簡単にスケールアップでき、開発の並行処理が可能である必要がある +- 変化する要件や状況に適応するのが簡単であるべきである + +## 関連情報 {#see-also} + +- [(React Berlin Talk) Oleg Isonen - Feature Driven Architecture][ext-kof] +- [(記事) プロジェクトのモジュール化について][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-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/ja/docusaurus-plugin-content-docs/current/about/understanding/knowledge-types.md b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/knowledge-types.md new file mode 100644 index 0000000000..74e5b2dff9 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/knowledge-types.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 3 +sidebar_label: 知識の種類 +--- + +# プロジェクトにおける知識の種類 + +どのプロジェクトにも以下の「知識の種類」が存在します。 + +* **基礎知識** + 時間とともにあまり変わらない知識。例えばアルゴリズム、コンピュータサイエンス、プログラミング言語やそのAPIの動作メカニズムなど。 + +* **技術スタック** + プロジェクトで使用される技術的解決策のセットに関する知識。プログラミング言語、フレームワーク、ライブラリを含む。 + +* **プロジェクト知識** + 現在のプロジェクトに特有であり、他のプロジェクトでは役に立たない知識。この知識は新しいチームメンバーが効果的にプロジェクトに貢献するために必要である。 + +:::note + +**Feature-Sliced Design**は「プロジェクト知識」への依存を減らし、より多くの責任を引き受け、新しいチームメンバーのオンボーディングを容易にすることを目指している。 + +::: diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/naming.md b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/naming.md new file mode 100644 index 0000000000..dcb4e36686 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/naming.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 4 +--- + +# ネーミング + +異なる開発者は異なる経験と背景を持っているため、同じエンティティが異なる名前で呼ばれることによって、チーム内で誤解が生じる可能性があります。例えば + +- 表示用のコンポーネントは「ui」、「components」、「ui-kit」、「views」などと呼ばれることがある。 +- アプリケーション全体で再利用されるコードは「core」、「shared」、「app」などと呼ばれることがある。 +- ビジネスロジックのコードは「store」、「model」、「state」などと呼ばれることがある。 + +## Feature-Sliced Designにおけるネーミング {#naming-in-fsd} + +FSD設計方法論では、以下のような特定の用語が使用されます。 + +- 「app」、「process」、「page」、「feature」、「entity」、「shared」といった層の名前、 +- 「ui」、「model」、「lib」、「api」、「config」といったセグメントの名前。 + +これらの用語を遵守することは、チームメンバーやプロジェクトに新しく参加する開発者の混乱を防ぐために非常に重要です。標準的な名称を使用することは、コミュニティに助けを求める際にも役立ちます。 + +## 名前衝突 {#when-can-naming-interfere} + +名前衝突は、FSD設計方法論で使用される用語がビジネスで使用される用語と重なっている場合に発生する可能性があります。例えば + +- `FSD#process`と、アプリケーション内でモデル化されたプロセス、 +- `FSD#page`と、マガジンのページ、 +- `FSD#model`と、自動車モデル。 + +開発者がコード内で「プロセス」という言葉を見た場合、どのプロセスが指されているのかを理解するのに余分な時間を費やすことになってしまいます。このような**衝突は開発プロセスを妨げる場合があります**。 + +プロジェクトの用語集にFSD特有の用語が含まれている場合、これらの用語をチームや技術的に関心のない関係者と議論する際には特に注意が必要です。 + +チームとの効果的なコミュニケーションのためには、用語の前に「FSD」という略語を付けることをお勧めします。例えば、プロセスについて話すときは、「このプロセスをFSDのフィーチャー層に置くことができる」と言うことができます。 + +逆に、技術的でない関係者とのコミュニケーションでは、FSDの用語の使用を制限し、コードベースの内部構造に言及しない方が良いでしょう。 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/needs-driven.md b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/needs-driven.md new file mode 100644 index 0000000000..c8f60760ed --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/needs-driven.md @@ -0,0 +1,153 @@ +--- +sidebar_position: 2 +--- + +# ニーズの理解と課題の定義について + +:::note TL;DR + +— _新しい機能が解決する目標を明確にできませんか?それとも、問題はタスク自体が明確にされていないことにありますか?**FSDは、問題の定義や目標を引き出す手助けをすることも目的にしています。**_ + +— _プロジェクトは静的に存在するわけではなく、要件や機能は常に変化しています。プロジェクトは最初の要望のスナップショットのみに基づいて設計されているため、時間が経つにつれて、コードは混沌としてしまいます。**良いアーキテクチャの課題の一つは、変化する開発条件に対応できるようにすることです。**_ + +::: + + + + +## なぜ? {#why} + +エンティティの明確な名前を選び、その構成要素を理解するためには、**コードが解決する課題を明確に理解する必要があります。** + +> _@sergeysova: 開発中、私たちは各エンティティや機能に、その意図や意味を明確に反映する名前を付けようとしている。_ + +_課題を理解しなければ、重要なケースをカバーする正しいテストを書くことも、ユーザーに適切な場所でエラーを表示することもできず、単純にユーザーのフローを中断することにもなってしまいます。_ + +## どのような課題についての話? {#what-tasks-are-we-talking-about} + +フロントエンドは、エンドユーザーのためのアプリケーションやインターフェースを開発しているため、私たち開発者はその消費者の課題を解決しています。 + +私たちのもとに誰かが来るとき、**その人は自分の悩みを解決したり、ニーズを満たしたりしてほしいのです。** + +_マネージャーとアナリストの仕事はこのニーズを定義することです。開発者の仕事はウェブ開発の特性(接続の喪失、バックエンドのエラー、タイプミス、カーソルや指の操作ミス)を考慮して、そのニーズを実現することです。_ + +**ユーザーが持ってきた目的こそが、開発者の課題です。** + +> _小さな解決された課題が、Feature-Sliced Designの設計方法論におけるfeatureではあります。プロジェクト課題のスコープを小さな目標に分割する必要があります。_ + +## これが開発にどのように影響するのか? {#how-does-this-affect-development} + +### 課題(タスク)の分解 {#task-decomposition} + +開発者がタスクを実装し始めるとき、理解の簡素化とコードメンテナンスのために、**タスクを段階に分けます**。 + +- まずは、上位レベルのエンティティに分けて、それを実装する +- 次に、これらのエンティティをより小さく分ける +- そしてさらに続ける + +_エンティティを分解する過程で、開発者はそれに明確に意図を反映した名前を付ける必要があり、エンティティの一覧表を読む際にそのコードが解決する課題を理解するのに役立ちます。_ + +この際、ユーザーの悩みを軽減したり、ニーズを実現したりするユーザーへの手助けをすることを忘れないように心がけましょう。 + +### 課題の本質を理解する {#understanding-the-essence-of-the-task} + +エンティティに明確な名前を付けるためには、**開発者はその目的について十分に理解する必要があります。** + +- エンティティをどのように使用するつもりなのか +- エンティティがユーザーの課題のどの部分を実現するのか、他にどこでこのエンティティを使用できるのか +- などなど + +結論を出すのは難しくありません。**開発者がFSD枠内でのエンティティの名前を考えているとき、コードを書く前に不十分に定義された課題を見つけることができます。** + +> どのようにエンティティに名前を付けるのか、もしそのエンティティが解決できる課題をよく理解していない場合、そもそもどうやって課題をエンティティに分解できるのか? + +## どのように定義するのか? {#how-to-formulate-it} + +機能によって解決される課題を定義するためには、その課題自体を理解する必要があります。これはプロジェクトマネージャーやアナリストの責任範囲です。 + +_FSD設計方法論は、開発者に対して、プロダクトマネージャーが注目すべき課題を示唆することしかできません。_ + +> _@sergeysova: フロントエンドは、まず情報を表示するものである。どのコンポーネントも、まず何かを表示する。したがって、「ユーザーに何かを見せる」というタスクには実用的な価値がない。_ + +基本的なニーズや悩みを見つけたら、**あなたのプロダクトやサービスがどのようにユーザーの目標をサポートすることができるのかを考えます。** + +タスクトラッカーの新しいタスクは、ビジネスの課題を解決することを目的としており、ビジネスは同時にユーザーの課題を解決し、利益を上げようとしています。したがって、説明文に明記されていなくても、各タスクには特定の目標が含まれています。 + +開発者は、特定のタスクが追求する目的をはっきりと把握しておくべきです。しかし、すべての会社がプロセスを完璧に構築できるわけではありません。 + +## その利益は何か? {#and-what-is-the-benefit} + +では、プロセス全体を最初から最後まで見てみましょう。 + +### 1. ユーザーの課題を理解する {#1-understanding-user-tasks} + +開発者は、ユーザーの悩みとビジネスがその悩みをどのように解決するかを理解すると、ウェブ開発の特性によりビジネスには提供できない解決策を提案することができます。 + +> しかしもちろん、これは開発者が自分の行動や目的に無関心でない限り機能します。さもなければ、そもそもなぜFSDやアプローチが必要なのか?という疑問になってしまいます。 + +### 2. 構造化と整理 {#2-structuring-and-ordering} + +課題を理解することで、**頭とコードの中で明確な構造が得られます。** + +### 3. 機能とその構成要素を理解する {#3-understanding-the-feature-and-its-components} + +**1つの機能は、ユーザーにとって1つの有用な機能性です。** + +- 1つの機能に複数の機能性が実装されている場合、それは**境界の侵害**である。 +- 機能は分割不可能で成長可能になる場合があるが、**それは悪くない。** +- **悪い**のは、機能が「ユーザーにとってのビジネス価値は何か?」という質問に答えられないことである。 + - 「オフィスの地図」という機能は存在できない。 + - しかし、「地図上の会議室の予約」、「従業員の検索」、「作業場所の変更」は**存在可能である。** + +> _@sergeysova: 機能には、直接的にその機能を実現するコードだけが含まれるべきであり、余計な詳細や内部の解決策は含まれないべきである(理想的には)。_ + +> *機能のコードを開くと、**そのタスクに関連するものだけが見える**。それ以上は必要ない。* + +### 4. Profit {#4-profit} + +ビジネスはその方針を極めて稀にしか根本的に変えないため、**ビジネスのタスクをフロントエンドアプリケーションのコードに反映することは非常に大きな利点になれます。** + +_そうすれば、チームの新しいメンバーにそのコードが何をするのか、なぜ追加されたのかを説明する必要がなくなります。**すべては、すでにコードに反映されているビジネスのタスクを通じて説明されているからです。**_ + +> [Domain Driven Developmentにおける「ビジネス言語」][ext-ubiq-lang] + +--- + +## 現実に戻りましょう {#back-to-reality} + +ビジネスプロセスが明確な意味を持ち、設計段階で良い名前が付けられている場合、_その理解と論理をコードに移すことはそれほど問題ではありません。_ + +しかし実際には、タスクや機能性は通常「過度に」反復的に進化し、(または)デザインを考える時間がありません。 + +**その結果、今日、機能は意味を持っていますが、1か月後にその機能を拡張する際には、プロジェクト全体を再構築する必要があるかもしれません。** + +> *開発者は未来の要望を考慮しながら2〜3ステップ先を考えようとしますが、自分の経験に行き詰まってしまいます。* + +> _経験豊富なエンジニアは通常、すぐに10ステップ先を見て、どの機能を分割するか、どの機能を他の機能と統合するかを理解しています。_ + +> _しかし、経験上遭遇したことのないタスクが来ることもあり、その場合、どのように機能を適切に分解し、将来的に悲惨な結果を最小限に抑えるかを理解する手段がありません。_ + +## FSDの役割 {#the-role-of-methodology} + +**FSDは、開発者の問題を解決する手助けをし、ユーザーの問題を解決するのを容易にしています。** + +開発者のためだけに課題を解決することはありません。 + +しかし、開発者が自分の課題を解決するためには、**ユーザーの課題を理解する必要があります**。逆は成り立ちません。 + +### FSDに対する要件 {#methodology-requirements} + +明らかになるのは、**Feature-Sliced Design**のために少なくとも2つの要件を定義する必要があるということです。 + +1. FSD方法論は**フィーチャー、プロセス、エンティティを作成する方法**を説明する必要がある。 + - つまり、それらの間でコードをどのように分割するかを明確に説明する必要がある。これによりこれらのエンティティの命名も仕様に組み込まれるべきである。 +2. FSD方法論は、アーキテクチャがプロジェクトの変わりゆく要件にスムーズに対応できるようにするべきである。 + +## 関連情報 {#see-also} + +- [(記事) "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 +[ext-ubiq-lang]: https://thedomaindrivendesign.io/developing-the-ubiquitous-language \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/signals.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/signals.mdx new file mode 100644 index 0000000000..c1757ce6be --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/about/understanding/signals.mdx @@ -0,0 +1,10 @@ +--- +sidebar_position: 5 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# アーキテクチャのシグナル + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/branding.md b/i18n/ja/docusaurus-plugin-content-docs/current/branding.md new file mode 100644 index 0000000000..4b722d43e7 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/branding.md @@ -0,0 +1,80 @@ +# ブランドガイドライン + +FSDのビジュアルアイデンティティは、そのコアコンセプトである `Layered`、`Sliced self-contained parts`、`Parts & Compose`、`Segmented` に基づいています。しかし、私たちはFSDの哲学を反映し、簡単に認識できる美しいアイデンティティを目指しています。 + +**FSDのアイデンティティを「そのまま」変更せずに、私たちのアセットを使って快適にご利用ください。** このブランドガイドは、FSDのアイデンティティを正しく使用する手助けをします。 + +:::caution 互換性 + +FSDは以前、[別のレガシーアイデンティティ](https://drive.google.com/drive/folders/11Y-3qZ_C9jOFoW2UbSp11YasOhw4yBdl?usp=sharing)を持っていました。古いデザインは、FSDの主要なコンセプトを反映していませんでした。また、これは粗いドラフトとして作成され、更新されるべきものでした。 + +ブランドの互換性と長期的な使用のために、私たちは2021年から2022年にかけて慎重にリブランディングに取り組みました。**FSDのアイデンティティを使用する際に自信を持てるように🍰** + +*古いアイデンティティではなく、最新のアイデンティティを使用してください!* + +::: + +## 名前 {#title} + +- ✅ **正しい:** `Feature-Sliced Design`、`FSD` +- ❌ **間違っている:** `Feature-Sliced`、`Feature Sliced`、`FeatureSliced`、`feature-sliced`、`feature sliced`、`FS` + +## 絵文字 {#emojii} + +ケーキのイメージ 🍰 はFSDの主要なコンセプトをよく反映しているため、私たちのブランド絵文字として選ばれました。 + +> 例: *"🍰 フロントエンド用ののアーキテクチャデザイン設計方法論"* + +## ロゴとカラーパレット {#logo--palettte} + +FSDには異なるコンテキスト用のいくつかのロゴバリエーションがありますが、**primary**の使用が推奨されます。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
テーマロゴ (Ctrl/Cmd + Clickでダウンロード)使用法
primary
(#29BEDC, #517AED)
logo-primaryほとんどの場合に推奨されます
flat
(#3193FF)
logo-flat単色コンテキスト用
monochrome
(#FFF)
logo-monocrhome白黒コンテキスト用
square
(#3193FF)
logo-square正方形サイズ用
+ +## バナーとスキーム {#banners--schemes} + +banner-primary +banner-monochrome + +## ソーシャルプレビュー + +作業中... + +## プレゼンテーションテンプレート {#presentation-template} + +作業中... + +## 参照 {#see-also} + +- [ディスカッション (github)](https://github.com/feature-sliced/documentation/discussions/399) +- [リブランディングの歴史と参考資料 (figma)](https://www.figma.com/file/RPphccpoeasVB0lMpZwPVR/FSD-Brand?node-id=0%3A1) +- [リブランディングデモ](https://rebrand-sliced.netlify.app/en/) diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/get-started/cheatsheet.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/cheatsheet.mdx new file mode 100644 index 0000000000..d5d502f9b4 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/cheatsheet.mdx @@ -0,0 +1,28 @@ +--- +# sidebar_position: 3 +unlisted: true +--- + +# 分解のチートシート + +インターフェースをレイヤーに分割する際の参考書として使用してください。以下にPDFバージョンもあり、印刷して枕の下に置いておくことができます。 + +## レイヤーの選択 {#choosing-a-layer} + +[PDFをダウンロード](/files/choosing-a-layer-en.pdf) + +![レイヤーの定義と自己チェックの質問](/img/choosing-a-layer-en.jpg) + +## 例 {#examples} + +### ツイート + +![分解されたツイート](/img/decompose-twitter.png) + +### GitHub + +![分解されたGitHub](/img/decompose-github.jpg) + +## 参照 {#see-also} + +- [(記事) ロジックの分解におけるさまざまなアプローチ](https://www.pluralsight.com/resources/blog/guides/how-to-organize-your-react--redux-codebase) diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/get-started/faq.md b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/faq.md new file mode 100644 index 0000000000..062f018fed --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/faq.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 20 +pagination_next: guides/index +--- + +# FAQ + +:::info + +質問は、[Discordコミュニティ][discord]、[GitHub Discussions][github-discussions]、および[Telegramチャット][telegram]で聞くことができます。 + +::: + +### ツールキットやリンターはありますか? {#is-there-a-toolkit-or-a-linter} + +公式のESLintコンフィグ — [@feature-sliced/eslint-config][eslint-config-official]、およびコミュニティメンバーのアレクサンドル・ベロウスによって作成されたESLintプラグイン — [@conarti/eslint-plugin-feature-sliced][eslint-plugin-conarti]があります。これらのプロジェクトへの貢献や独自のツールの作成を歓迎します! + +### ページのレイアウト/テンプレートはどこに保存すればよいですか? {#where-to-store-the-layouttemplate-of-pages} + +シンプルなレイアウトテンプレートが必要な場合は、`shared/ui`に保存できます。より上層のレイヤーを使用する必要がある場合、いくつかのオプションがあります。 + +- レイアウトが本当に必要ですか?レイアウトが数行で構成されている場合、各ページにコードを重複させる方が合理的です。 +- レイアウトが必要な場合は、個別のウィジェットやページとして保存し、App層のルーター設定にそれらを組み合わせることができます。ネストされたルーティングも一つのオプションです。 + +### フィーチャーとエンティティの違いは何ですか? {#what-is-the-difference-between-feature-and-entity} + +エンティティはアプリケーションが扱う現実世界の概念です。フィーチャーはユーザーに実際の価値を提供するインタラクションであり、ユーザーがエンティティで行いたいことです。 + +詳細および例については、参考書セクションの[スライスについてのページ][reference-entities]を参照してください。 + +### ページ/フィーチャー/エンティティを相互に埋め込むことはできますか? {#can-i-embed-pagesfeaturesentities-into-each-other} + +はい、しかし、この埋め込みはより上層のレイヤーで行う必要があります。例えば、ウィジェット内で両方のフィーチャーをインポートし、プロップス/子要素として一方のフィーチャーを他方に挿入することができます。 + +一方のフィーチャーを他方のフィーチャーからインポートすることはできません。これは[**レイヤーのインポートルール**][import-rule-layers]で禁止されています。 + +### Atomic Designはどうですか? {#what-about-atomic-design} + +現在、アトミックデザインをFeature-Sliced Designと一緒に使用することを義務付けていませんが、禁止もしていません。 + +アトミックデザインは、モジュールの`ui`セグメントにうまく適用できます。 + +### FSDに関する有用なリソース/記事などはありますか? {#are-there-any-useful-resourcesarticlesetc-about-fsd} + +はい! https://github.com/feature-sliced/awesome + +### なぜFeature-Sliced Designが必要なのですか? {#why-do-i-need-feature-sliced-design} + +FSDは、プロジェクトの主要な価値を提供するコンポーネントの観点から、あなたとあなたのチームが迅速にプロジェクトを把握するのに役立ちます。標準化されたアーキテクチャは、オンボーディングを迅速化し、コード構造に関する議論を解決するのに役立ちます。FSDが作成された理由については、[モチベーション][motivation]のページを参照してください。 + +### 初心者の開発者にFSDのアーキテクチャ/設計方法論は必要ですか? {#does-a-novice-developer-need-an-architecturemethodology} + +おそらく必要です。 + +*通常、一人でプロジェクトを設計・開発する場合、すべてが順調に進みます。しかし、開発に中断が生じたり、新しい開発者がチームに加わると問題が発生します。* + +### 認証コンテキストをどのように扱えばよいですか? {#how-do-i-work-with-the-authorization-context} + +[こちら](/docs/guides/examples/auth)で回答しています。 + +[import-rule-layers]: /docs/reference/layers#import-rule-on-layers +[reference-entities]: /docs/reference/layers#entities +[eslint-config-official]: https://github.com/feature-sliced/eslint-config +[eslint-plugin-conarti]: https://github.com/conarti/eslint-plugin-feature-sliced +[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/ja/docusaurus-plugin-content-docs/current/get-started/index.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/index.mdx new file mode 100644 index 0000000000..75b532c1b0 --- /dev/null +++ b/i18n/ja/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の適用方法とその基礎知識が簡単に紹介されます。また、FSDの主な利点とその作成理由についての内容も記載されています。 +

+ + + + +{/* */} diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/get-started/overview.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/overview.mdx new file mode 100644 index 0000000000..3f47649650 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/overview.mdx @@ -0,0 +1,137 @@ +--- +sidebar_position: 1 +--- + +# 概要 + +**Feature-Sliced Design** (FSD) とは、フロントエンドアプリケーションの設計方法論です。簡単に言えば、コードを整理するためのルールと規約の集大成です。FSDの主な目的は、ビジネス要件が絶えず変化する中で、プロジェクトをより理解しやすく、構造化されたものにすることです。 + +ルールのセットに加えて、FSDはツールチェーンでもあります。プロジェクトのアーキテクチャをチェックするための[リンター][ext-steiger]、CLIやIDEを通じた[フォルダージェネレーター][ext-tools]、および豊富な[実装例のコレクション][examples]があります。 + +## FSDは私のプロジェクトに適しているのか? {#is-it-right-for-me} + +FSDは、あらゆる規模のプロジェクトやチームに導入できます。以下の場合、あなたのプロジェクトに適しています。 + +- **フロントエンド**開発での使用(ウェブサイト、モバイル/デスクトップアプリケーションのインターフェース作成など) +- **アプリケーション**開発での使用(ライブラリ開発ではない) + +これだけです!使用するプログラミング言語、フレームワーク、状態管理ライブラリには制限がありません。尚、FSDを段階的に導入したり、モノレポで使用したり、アプリケーションをパッケージに分割し、それぞれに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} + +レイヤー、スライス、セグメントは、以下の図に示されるように階層を形成します。 + +
+ ![FSDの概念の階層、以下に説明](/img/visual_schema.jpg) + +
+

上の図には、左から右に「レイヤー」、「スライス」、「セグメント」とラベル付けされた3つの列があります。

+

「レイヤー」列には、上から下に「app」、「processes」、「pages」、「widgets」、「features」、「entities」、「shared」とラベル付けされた7つの区分があります。「processes」区分は取り消し線が引かれています。「entities」区分は2番目の列「スライス」と接続されていて、2番目の列が「entities」の内容であることを示しています。

+

「スライス」列には、上から下に「user」、「post」、「comment」とラベル付けされた3つの区分があります。「post」区分は「セグメント」列と同様に接続されていて、「post」の内容であることを示しています。

+

「セグメント」列には、上から下に「ui」、「model」、「api」とラベル付けされた3つの区分があります。

+
+
+ +### レイヤー {#layers} + +レイヤーはすべてのFSDプロジェクトで標準化されています。すべてのレイヤーを使用する必要はありませんが、ネーミングは重要です。現在、7つのレイヤーが存在しています(上から下へ)。 + +1. App*(アップ) — アプリケーションの起動に必要なすべてのもの(ルーティング、エントリーポイント、グローバルスタイル、プロバイダーなど) +2. Processes(プロセス、非推奨) — 複雑なページ間のシナリオ +3. Pages(ページ) — ページ全体、またはネストされたルーティングの場合、ページの大部分 +4. Widgets(ウィジェット) — 大きな自己完結型の機能部分、またはインターフェースの大部分。通常はユーザーシナリオ全体を実装する +5. Features(フィーチャー) — プロダクト機能の再利用可能な実装、つまりユーザーにビジネス価値をもたらすアクション +6. Entities(エンティティ) — プロジェクトが扱うビジネスエンティティ、例えば`user`や`product` +7. Shared*(シェアード) — 再利用可能なコード。特にプロジェクト/ビジネスの詳細から切り離されたもの + +_* — App層とShared層のレイヤーは他のレイヤーとは異なり、スライスを持たず、直接セグメントで構成されています。_ + +レイヤーの特徴は、レイヤーのモジュールは、下層のレイヤーモジュールのみを知ることができ、その結果、レイヤーが下層のレイヤーからのみモジュールをインポートできることです。 + +### スライス {#slices} + +次にスライスがあり、レイヤーをドメインごとに分割します。スライスの名前は自由に付けることができ、いくつでも作成できます。スライスは、意味的に関連するコードをグループ化することで、プロジェクト内のナビゲーションをしやすくします。 + +スライスは同じレイヤーの他のスライスを使用できないため、スライス内のコードの強い結合とスライス間の弱い結合が保証されます。 + +### セグメント {#segments} + +スライス、およびApp層とShared層のレイヤーはセグメントで構成され、セグメントはその目的に応じてコードをグループ化します。セグメントの名前は標準で固定されていませんが、最も一般的な目的のためにいくつかの共通の名前があります。 + +- `ui` — 表示に関連するすべて: UIコンポーネント、日付フォーマッター、スタイルなど +- `api` — バックエンドとのやり取り: リクエスト関数、データ型、マッパー +- `model` — データモデル: バリデーションスキーマ、インターフェース、ストレージ、ビジネスロジック +- `lib` — 他のモジュールが必要とするライブラリコード +- `config` — 設定ファイルとフィーチャーフラグ + +通常、これらのセグメントはほとんどのレイヤーに十分であるため、独自のセグメントはShared層やApp層でのみ作成されることが多いです。しかし、これは厳格なルールではありません。 + +## 利点 {#advantages} + +- **一貫性** + 構造が標準化されているため、プロジェクトがより一貫性を持ち、新しいメンバーのチームへの参加が容易になります。 + +- **変更とリファクタリングへの耐性** + レイヤーのモジュールは、同じレイヤーや上層レイヤーの他のモジュールを使用できないため、アプリケーションの他の部分に予期しない影響を与えることなく、分離された変更を加えることができます。 + +- **ロジックの再利用制御** + レベルに応じて、コードを非常に再利用可能にすることも、非常にローカルにすることもできます。 + これにより、**DRY**原則と実用性のバランスが保たれます。 + +- **ビジネスとユーザーのニーズに焦点を当てる** + アプリケーションはビジネスドメインに分割され、命名にはビジネス用語の使用が奨励されるため、プロジェクトの他の無関係な部分に完全に精通することなく、プロダクトで有用な作業を行うことができます。 + +## 段階的な導入 {#incremental-adoption} + +既存のコードベースをFSDに移行したい場合は、以下の戦略をお勧めします。私たち自身の移行経験から、この方法は非常に効果的であることが分かりました。 + +1. App層とShared層のレイヤーを徐々に形成し、基盤を作る。 + +2. 既存のすべてのインターフェースコードをウィジェットとページに分散させる。FSDのルールに違反する依存関係があっても良い。 + +3. インポートのルール違反を徐々に修正しながら、エンティティやフィーチャーを抽出する。 + +リファクタリング中に新しい大きななエンティティを追加することや、部分的なリファクタリングは避けることをお勧めします。 + +## 次のステップ {#next-steps} + +- **FSDの考え方を理解したい?** [チュートリアル][tutorial]を読んでください。 +- **例を見て学びたい?** [実装例セクション][examples]にたくさんあります。 +- **質問がある?** [Discordチャンネル][ext-discord]にアクセスして、コミュニティに質問してください。 + +[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-discord]: https://discord.com/invite/S8MzWTUsmp + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/get-started/tutorial.md b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/tutorial.md new file mode 100644 index 0000000000..46ef4b4e92 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/get-started/tutorial.md @@ -0,0 +1,2264 @@ +--- +sidebar_position: 2 +--- + +# チュートリアル + +## 第1章 紙の上で + +このガイドでは、Real World Appとしても知られるアプリケーションを見ていきます。Conduitは、[Medium](https://medium.com/)の簡略版であり、ブログ記事を読み書きし、他の人の記事にコメントすることができます。 + +![Conduitのホームページ](/img/tutorial/realworld-feed-anonymous.jpg) + +これはかなり小さなアプリケーションなので、過度に分解することなく開発を進めます。おそらく、アプリケーション全体は3つの層に収まります: **App層**、**Pages層**、**Shared層**。もしそうでなければ、進行に応じて追加の層を導入しましょう。準備はいいですか? + +### ページの列挙から始める + +上のスクリーンショットを見てみると、少なくとも次のページがあると推測できます。 + +- ホーム(記事のフィード) +- ログインと登録 +- 記事の閲覧 +- 記事の編集 +- ユーザープロフィールの閲覧 +- プロフィールの編集(設定) + +これらの各ページは、*Pages*層の個別*スライス*になります。概要のセクションから思い出してください。スライスは単に層内のフォルダーであり、層は事前に定義された名前のフォルダーだけです。例えば、`pages`のようです。 + +したがって、私たちのPagesフォルダーは次のようになります。 + +``` +📂 pages/ + 📁 feed/ (フィード) + 📁 sign-in/ (ログイン/登録) + 📁 article-read/ (記事の閲覧) + 📁 article-edit/ (記事の編集) + 📁 profile/ (プロフィール) + 📁 settings/ (設定) +``` + +Feature-Sliced Designの特徴は、ページが互いに依存できないことです。つまり、1つのページが他のページのコードをインポートすることはできません。これは**層のインポートルール**によって禁じられています。 + +*スライス内のモジュールは、下層にあるスライスのみをインポートできる。* + +この場合、ページはスライスであるため、そのページ内のモジュール(ファイル)は、他のページではなく、下層からのみコードをインポートできます。 + +### フィードを詳しく見てみると + +
+ ![匿名訪問者の視点](/img/tutorial/realworld-feed-anonymous.jpg) +
+ _匿名訪問者の視点_ +
+
+ +
+ ![認証されたユーザーの視点](/img/tutorial/realworld-feed-authenticated.jpg) +
+ _認証されたユーザーの視点_ +
+
+ +フィードページには3つの動的領域があります。 + +1. 認証状態を示すログインリンク +2. フィードをフィルタリングするタグ一覧 +3. 1つ、または2つのフィード記事。各記事にはいいねボタンがある + +ログインリンクは、すべてのページで共通のヘッダーの一部であるため、一旦保留にしましょう。 + +#### タグ一覧 + +タグ一覧を作成するには、すべての利用可能なタグを取得し、各タグをチップ([chip](https://m3.material.io/components/chips/overview))として表示し、選択されたタグをクライアント側のストレージに保存する必要があります。これらの操作は、「APIとのインタラクション」、「ユーザーインターフェース」、「データストレージ」のカテゴリに関連しています。Feature-Sliced Designでは、コードは目的に応じて*セグメント*に分けられます。セグメントはスライス内のフォルダーであり、目的を説明する任意の名前を持つことができます。いくつかの目的は非常に一般的であるため、いくつかの一般的な名前があります。 + +- 📂 `api/` バックエンドとのインタラクション +- 📂 `ui/` 表示と外観を担当するコード +- 📂 `model/` データとビジネスロジックのストレージ +- 📂 `config/` フィーチャーフラグ、環境変数、その他の設定形式 + +タグを取得するコードは`api`に、タグコンポーネントは`ui`に、ストレージとのインタラクションは`model`に配置します。 + +#### 記事 + +同じ論理に従って、記事のフィードを同じ3つのセグメントに分けることができます。 + +- 📂 `api/`: ページごとの記事一覧を取得したり、いいねを残したりする +- 📂 `ui/`: + - タグを選択したときに追加のタブを表示できるタブ一覧 + - 個別の記事 + - ページネーション +- 📂 `model/`: クライアントのストレージに保存された読み込まれた投稿と現在のページ(必要に応じて) + +### 共通コードの再利用 + +アプリケーションのページは通常、目的によって非常に異なりますが、全体で共通するものもあります。例えば、デザイン言語に対応するUIキットや、すべてが特定の認証メソッドを介してREST APIを通じて行われるというバックエンドにおける取り決めです。スライスは隔離されている必要があるため、コードの再利用は下層の**Shared層**を介して行われます。 + +Shared層は他の層とは異なり、スライスではなくセグメントを含むため、Shared層はレイヤーとスライスのハイブリッドです。 + +通常、Shared層内のコードは事前に作成されず、開発の過程で抽出されます。なぜなら、どの部分のコードが実際に再利用されるかが開発中に明らかになるからです。それでも、Shared層にどんなコードを保持するかを念頭に置いておくことは重要です。 + +- 📂 `ui/` — ビジネスロジックなしのUIキット。例えば、ボタン、ダイアログ、フォームフィールド。 +- 📂 `api/` — バックエンドへのリクエスト用の便利なラッパー(例えば、ウェブの場合は、`fetch()`のラッパー) +- 📂 `config/` — 環境変数の処理 +- 📂 `i18n/` — 多言語対応の設定 +- 📂 `router/` — ルーティングのプリミティブと定数 + +これらはShared層内のセグメントの例に過ぎません。これらのいずれかを省略したり、自分自身のセグメントを作成したりできます。新しいセグメントを作成する際に覚えておくべき唯一のことは、セグメントの名前は内容の本質(何)ではなく、目的(なぜ)を説明するものでなければなりません。`components`、`hooks`、`modals`のような名前は使用しない方が良いです。なぜなら、これらはファイルが本質的に何を含んでいるかを説明するものであり、コードが書かれた目的を説明するものではないからです。このようなネーミングの結果、チームは必要なものを見つけるためにフォルダーを掘り下げなければならず、さらに無関係なコードが隣接しているため、リファクタリング時にアプリケーションの大部分に影響を与え、レビューやテストが難しくなってしまいます。 + +### 公開APIを定義する + +Feature-Sliced Designの文脈において、*公開API*という用語は、スライス、またはセグメントが、プロジェクト内の他のモジュールがインポートできるものを宣言することを意味します。例えば、JavaScriptでは、他のファイルからオブジェクトを再エクスポートする`index.js`ファイルがこれに該当します。これにより、外部との契約(つまり、公開API)が変更されない限り、スライス内でのリファクタリングを自由にできます。 + +Shared層にはスライスがないため、通常、セグメントレベルで公開API(インデックス)を定義する方が便利です。そうすることで、Shared層からのインポートは自然に目的に応じて整理されます。他のレイヤーにはスライスがあるため、通常は1つのインデックスをスライスに定義し、スライス自身が内部のセグメントのセットを制御する方が実用的です。なぜなら、他のレイヤーは通常、エクスポートがはるかに少なく、リファクタリングが頻繁に行われるからです。 + +私たちのスライス/セグメントは次のようになるでしょう。 + +``` +📂 pages/ + 📂 feed/ + 📄 index + 📂 sign-in/ + 📄 index + 📂 article-read/ + 📄 index + 📁 … +📂 shared/ + 📂 ui/ + 📄 index + 📂 api/ + 📄 index + 📁 … +``` + +`pages/feed`や`shared/ui`のようなフォルダー内にあるものは、これらのフォルダーにのみ知られており、これらのフォルダーの内容に関する保証はありません。 + +### 大きな再利用可能なUIブロック + +以前、再利用可能なアプリケーションのヘッダーのところに戻りますが、各ページでヘッダーを再構築するのは非効率的なので、再利用します。再利用するコードには、すでにShared層がありますが、Shared層内の大きなUIブロックには注意が必要です。Shared層は上層のレイヤーについて何も知らないべきです。 + +Shared層とPages層の間には、Entities層、Features層、Widgets層の3つの他のレイヤーがあります。他のプロジェクトでは、これらのレイヤーに大きな再利用可能なブロックで使用したいものがあるかもしれません。その場合、そのブロックをShared層に置くことはできません。なぜなら、上層からインポートしなければならず、それは禁止されているからです。ここでWidgets層が役立ちます。これはShared層、Entities層、Features層の上に位置しているため、すべてを使用できます。 + +私たちの場合、ヘッダーは非常にシンプルです。静的なロゴと上部ナビゲーションしかありません。ナビゲーションはAPIに現在のユーザーが認証されているかどうかを尋ねる必要がありますが、これは`api`セグメントからの単純なインポートで解決できます。したがって、ヘッダーはShared層に残します。 + +### フォームページに着目 + +記事を読むだけでなく、編集することもできるページも見てみましょう。例えば、記事編集者のページです。 + +![Conduitの記事編集者](/img/tutorial/realworld-editor-authenticated.jpg) + +見た目は単純ですが、私たちがまだ調べていないアプリケーション開発のいくつかの側面を含んでいます。フォームのバリデーション、エラー状態、データの永続的な保存のようなものです。 + +このページを作成するには、Shared層からいくつかのフィールドとボタンを取り、それらをこのページの`ui`セグメントにあるフォームにまとめます。次に、`api`セグメントで、バックエンドに記事を作成するための変更リクエストを定義します。 + +リクエストを送信する前にリクエストをバリデーションするために、バリデーションスキーマが必要です。バリデーションスキーマはデータモデルであるため、`model`セグメントに入れるのがちょうど良いです。そこでエラーメッセージを生成し、`ui`セグメントの別のコンポーネントを使用してエラーメッセージを表示します。 + +UXを向上させるために、ブラウザを閉じたときに偶発的なデータ損失を防ぐために、入力データを永続的に保存することもできます。これも`model`セグメントに適しています。 + +### まとめ + +いくつかのページに着目し、アプリケーションの基本的な構造を決めることができました。 + +1. Shared層 + 1. `ui` には再利用可能なUIキットが含まれる + 2. `api` にはバックエンドとのインタラクションのためのプリミティブが含まれる + 3. 残りはコードを書く過程で整理する +2. Pages層 — 各ページに対して個別のスライスを作成 + 1. `ui` にはページ自体とその構成要素が含まれる + 2. `api` には`shared/api`を使用するデータ取得のためのより専用的な関数が含まれる + 3. `model` には表示するデータのクライアントストレージなどが含まれる + +これでこのアプリケーションを作りましょう! + +## 第2章 コードの中で + +計画ができたので、実現していきましょう。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`が作成されます。 + +### フィードページを接続する + +アプリケーションのルート(`/`)をフィードページに接続しましょう。`pages/feed/ui`に`FeedPage.tsx`コンポーネントを作成し、次の内容を入れます。 + +```tsx title="pages/feed/ui/FeedPage.tsx" +export function FeedPage() { + return ( +
+
+
+

conduit

+

知識を共有する場

+
+
+
+ ); +} +``` + +次に、このコンポーネントをフィードページの公開APIに再エクスポートします。 + +```tsx 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; +``` + +これで、devサーバーを起動し、アプリケーションを開くと、Conduitのバナーが表示されるはずです! + +![Conduitのバナー](/img/tutorial/conduit-banner.jpg) + +### APIクライアント + +RealWorldのバックエンドと通信するために、Shared層内に便利なAPIクライアントを作成しましょう。クライアント用の`api`セグメントと、バックエンドの基本URLなどの変数用の`config`セグメントを作成します。 + +```bash +npx fsd shared --segments api config +``` + +次に、`shared/config/backend.ts`を作成します。 + +```tsx title="shared/config/backend.ts" +export const backendBaseUrl = "https://api.realworld.io/api"; +``` + +```tsx title="shared/config/index.ts" +export { backendBaseUrl } from "./backend"; +``` + +RealWorldプロジェクトは[OpenAPI仕様](https://github.com/gothinkster/realworld/blob/main/api/openapi.yml)を提供しているため、APIクライアントの型を自動的に生成できます。私たちは[`openapi-fetch`パッケージ](https://openapi-ts.pages.dev/openapi-fetch/)を使用します。このパッケージにはTypeScriptの型を自動生成するツールも含まれています。 + +次のコマンドを実行して、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"; +``` + +### フィード内の実データ + +これで、バックエンドから記事を取得し、フィードに追加できます。まず、記事プレビューコンポーネントを実装しましょう。 + +`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}

+ 続きを読む... +
    + {article.tagList.map((tag) => ( +
  • + {tag} +
  • + ))} +
+ +
+ ); +} +``` + +「いいね」ボタンはまだ機能していません。それは記事の読み取りページに移動して、「いいね」機能を実装するときに修正します。 + +これで、記事を取得して、たくさんのプレビューカードを表示できます。Remixでは、データの取得は*ローダー*を使用して行われます。ローダーは、ページに必要なデータを収集するサーバー関数です。ローダーはページの代わりにAPIとやり取りをするため、`api`セグメントに配置します。 + +```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; +``` + +最後のステップは、これらのカードをフィードに表示することです。`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

+

知識を共有する場

+
+
+ +
+
+
+ {articles.articles.map((article) => ( + + ))} +
+
+
+
+ ); +} +``` + +### タグによるフィルタリング + +タグに関しては、バックエンドから取得し、ユーザーが選択したタグを記憶する必要があります。私たちはバックエンドからの取得方法はすでに知っています。これはローダー関数からの別のリクエストです。すでにインストールされている`remix-utils`パッケージの便利な`promiseHash`関数を使用します。 + +`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

+

知識を共有する場

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

人気のタグ

+ +
+ +
+ {tags.tags.map((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`セグメントは必要ありませんでした。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; +} + +/** 1ページあたりの記事の数。 */ +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

+

知識を共有する場

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

人気のタグ

+ +
+ +
+ {tags.tags.map((tag) => ( + + ))} +
+ +
+
+
+
+
+ ); +} +``` + +よし、これも実現しました。タグ一覧も同様に実装できますが、認証を実装するまで待ちましょう。ところで、認証についてですが! + +### 認証 + +認証には、ログイン用のページと登録用のページの2つがあります。これらは主に非常に似ているため、必要に応じてコードを再利用できるように、1つの`sign-in`セグメントに保持するのが理にかなっています。 + +`pages/sign-in`の`ui`セグメントに`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 ( +
+
+
+
+

登録

+

+ アカウントをお持ちですか? +

+ + {registerData?.error && ( +
    + {registerData.error.errors.body.map((error) => ( +
  • {error}
  • + ))} +
+ )} + +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ ); +} +``` + +これからは壊れたインポートを修正する必要があります。インポートが新しいセグメントにアクセスしているため、次のコマンドでそのセグメントを作成しましょう。 + +```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日間 + }), + }, + }); +} + +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=これをコピーしないでください +``` + +最後に、公開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 ( +
+
+
+
+

サインイン

+

+ アカウントが必要ですか? +

+ + {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`のコンテキストプロバイダーで包みます。これにより、ヘッダーを含むアプリ全体が現在のユーザーオブジェクトにアクセスできるようになります。また、クッキーから現在のユーザーオブジェクトを取得するためのローダーも追加します。次のコードを`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 ( + + + + + + + + + + + + + +
+ + + + + + + + ); +} +``` + +最終的に、ホームページは次のようになります。 + +
+ ![ヘッダー、フィード、タグがあるConduitのフィードページ。タブはまだありません。](/img/tutorial/realworld-feed-without-tabs.jpg) + +
ヘッダー、フィード、タグがあるConduitのフィードページ。タブはまだない。
+
+ +### タブ + +これで認証状態を判断できるようになったので、タブと「いいね」ボタンをフィードページに実装しましょう。新しいフォームを作る必要がありますが、このページファイルはすでに大きすぎるので、これらのフォームを隣接するファイルに移動しましょう。`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 ( +
+

人気のタグ

+ +
+ +
+ {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} +
  • + ) : ( +
  • + +
  • + ), + )} +
+ + ); +} +``` + +これで、フィードページを大幅に簡素化できます。 + +```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

+

知識を共有する場

+
+
+ +
+
+
+ + + {articles.articles.map((article) => ( + + ))} + + +
+ +
+ +
+
+
+
+ ); +} +``` + +ローダー関数にも新しいタブを考慮する必要があります。 + +```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>, +) { + /* そのまま */ +} + +/** 1ページあたりの記事数。 */ +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")), + }), + ); +}; +``` + +フィードページを一旦置いておく前に、投稿へのいいねを処理するコードを追加しましょう。`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}

+ もっと読む... +
    + {article.tagList.map((tag) => ( +
  • + {tag} +
  • + ))} +
+ +
+ ); +} +``` + +このコードは、`/article/:slug`にPOSTリクエストを送信し、`_action=favorite`を使用して記事をお気に入りにします。今は機能していませんが、記事リーダーの作成を始めると、これも実装します。 + +これで、フィードの作成が完了しました!やったね! + +### 記事リーダー + +まず、データが必要です。ローダーを作成しましょう。 + +```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, "スラッグパラメータが必要です"); + 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`ルートに接続できます。`article.$slug.tsx`というルートファイルを作成します。 + +```tsx title="app/routes/article.$slug.tsx" +export { loader } from "pages/article-read"; +``` + +ページ自体は、記事のタイトルとアクション、記事の本文、コメントセクションの3つの主要なブロックで構成されています。下記はページのマークアップで、特に興味深いものはありません。 + +```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`セグメントに`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, "スラッグパラメータが必要です"); + await DELETE("/articles/{slug}", { + params: { path: { slug: params.slug } }, + headers: authorization, + }); + return redirect("/"); + }, + async favorite() { + invariant(params.slug, "スラッグパラメータが必要です"); + await POST("/articles/{slug}/favorite", { + params: { path: { slug: params.slug } }, + headers: authorization, + }); + return redirectBack(request, { fallback: "/" }); + }, + async unfavorite() { + invariant(params.slug, "スラッグパラメータが必要です"); + await DELETE("/articles/{slug}/favorite", { + params: { path: { slug: params.slug } }, + headers: authorization, + }); + return redirectBack(request, { fallback: "/" }); + }, + async createComment() { + invariant(params.slug, "スラッグパラメータが必要です"); + const comment = formData.get("comment"); + invariant(typeof comment === "string", "コメントパラメータが必要です"); + 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, "スラッグパラメータが必要です"); + const commentId = formData.get("id"); + invariant(typeof commentId === "string", "idパラメータが必要です"); + const commentIdNumeric = parseInt(commentId, 10); + invariant( + !Number.isNaN(commentIdNumeric), + "数値のidパラメータが必要です", + ); + 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", + "ユーザーネームパラメータが必要です", + ); + 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", + "ユーザーネームパラメータが必要です", + ); + await DELETE("/profiles/{username}/follow", { + params: { path: { username: authorUsername } }, + headers: authorization, + }); + return redirectBack(request, { fallback: "/" }); + }, + }); +}; +``` + +これをスライスから再エクスポートし、ルートから再エクスポートします。ここにいる間に、ページ自体も接続しましょう。 + +```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; +``` + +これで、記事リーダーの「いいね」ボタンはまだ実装されていないにも関わらず、フィードの「いいね」ボタンは機能し始めます!それは、フィードの「いいね」ボタンもそのルートにリクエストを送っているからです。何かを「いいね」してみてください! + +`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 ? ( + <> + + 記事を編集 + +    + + + ) : ( + <> + + +    + + + )} +
+
+ ); +} +``` + +```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 ? ( +
+
+ +
+
+ + +
+
+ ) : ( +
+
+

+ サインイン +   または   + 登録 +   して記事にコメントを追加しましょう! +

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

{comment.body}

+
+ +
+ + + +   + + {comment.author.username} + + {comment.createdAt} + {comment.author.username === currentUser?.username && ( + +
+ + +
+
+ )} +
+
+ ))} +
+ ); +} +``` + +これで、記事リーダーが完成しました!「著者をフォローする」ボタン、「いいね」ボタン、「コメントを残す」ボタンがすべて正常に機能するはずです。 + +
+ ![記事リーダーの画像](/img/tutorial/realworld-article-reader.jpg) + +
記事リーダーの画像
+
+ +### 記事編集 + +これは、このガイドで最後に取り上げるページです。ここで最も興味深い部分は、フォームデータを検証する方法です。 + +`article-edit/ui/ArticleEditPage.tsx`ページ自体は、非常にシンプルで、追加のロジックは他の2つのコンポーネントに含まれます。 + +```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; +} +``` + +アクションが`errors`フィールドを返し、人間に理解できるエラーメッセージの配列を表示することを想定しています。アクションには後で移ります。 + +もう1つのコンポーネントはタグ入力フィールドです。これは、選択されたタグのプレビューできる通常の入力フィールドです。特に注目すべき点はありません。 + +```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部分に移ります。ローダーはURLを確認し、記事へのリンクがある場合、既存の記事を編集していることを意味し、そのデータをロードする必要があります。そうでない場合は、何も返しません。このローダーを作成しましょう。 + +```tsx 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}` }, + }), + ); +}; +``` + +アクションは新しいフィールドの値を受け取り、それらをデータスキーマに通し、すべてが正しければ、既存の記事を更新するか、新しい記事を作成することによって、バックエンドに変更を保存します。 + +```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("記事にタイトルを付けてください"); + } + + const description = data.get("description"); + if (typeof description !== "string" || description === "") { + errors.push("この記事が何についてか説明してください"); + } + + const body = data.get("body"); + if (typeof body !== "string" || body === "") { + errors.push("記事そのものを書いてください"); + } + + const tags = data.get("tags"); + if (typeof tags !== "string") { + errors.push("タグは文字列である必要があります"); + } + + if (errors.length > 0) { + throw errors; + } + + return { title, description, body, tags: data.get("tags") ?? "" } as { + title: string; + description: string; + body: string; + tags: string; + }; +} +``` + +少し長く繰り返しが多いように見えるかもしれませんが、これはエラーメッセージを人間に理解しやすくするための代償です。Zodのようなスキーマを使用することもできますが、その場合、フロントエンドでエラーメッセージを表示する必要があります。このフォームはそのような複雑さには値しません。 + +最後のステップは、ページ、ローダー、アクションをルートに接続することです。私たちは作成と編集の両方をきれいにサポートしているので、`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 (同じ内容)" +import { ArticleEditPage } from "pages/article-edit"; + +export { loader, action } from "pages/article-edit"; + +export default ArticleEditPage; +``` + +これで完成です!ログインして新しい記事を作成してみてください。あるいは、フィールドに何も記入せず進み、バリデーションがどのように機能するかを検証してみてください。 + +
+ ![記事編集者の画像](/img/tutorial/realworld-article-editor.jpg) + +
記事編集画像
+
+ +プロフィールページや設定ページは、記事の読み取りや編集ページに非常に似ていて、読者のための宿題として残されています。 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/_category_.yaml b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/_category_.yaml new file mode 100644 index 0000000000..685e78df8d --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/_category_.yaml @@ -0,0 +1,2 @@ +label: 例 +position: 1 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/auth.md b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/auth.md new file mode 100644 index 0000000000..264a970c26 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/auth.md @@ -0,0 +1,229 @@ +--- +sidebar_position: 1 +--- + +# 認証 + +一般的に、認証は以下のステップで構成されます。 + +1. ユーザーから資格情報を取得する +2. それをバックエンドに送信する +3. 認証されたリクエストを送信するためのトークンを保存する + +## ユーザーの資格情報を取得する方法 + +OAuthを通じて認証を行う場合は、OAuthプロバイダーのページへのリンクを持つログインページを作成し、[ステップ3](#how-to-store-the-token-for-authenticated-requests)に進むことができます。 + +### ログイン用の別ページ + +通常、ウェブサイトにはユーザー名とパスワードを入力するためのログイン専用ページがあります。これらのページは非常にシンプルであるため、分解する必要はありません。さらに、ログインフォームと登録フォームは外見が非常に似ているため、同じページにグループ化することもできます。ログイン/登録ページ用のスライスをPages層に作成します。 + +- 📂 pages + - 📂 login + - 📂 ui + - 📄 LoginPage.tsx + - 📄 RegisterPage.tsx + - 📄 index.ts + - その他のページ… + +ここでは、2つのコンポーネントを作成し、インデックスで両方をエクスポートしました。これらのコンポーネントは、ユーザーが資格情報を入力するためのわかりやすい要素を含むフォームを持ちます。 + +### ログイン用のダイアログボックス + +アプリケーションにどのページでも使用できるログイン用のダイアログボックスがある場合は、そのダイアログボックス用のウィジェットを作成できます。これにより、フォーム自体をあまり分解せずに、どのページでもこのダイアログボックスを再利用できます。 + +- 📂 widgets + - 📂 login-dialog + - 📂 ui + - 📄 LoginDialog.tsx + - 📄 index.ts + - その他のウィジェット… + +このガイドの残りの部分は、ログインが別ページで行われる最初のアプローチに基づいていますが、同じ原則がダイアログボックス用のウィジェットにも適用されます。 + +### クライアントバリデーション + +たまには、特に登録時に、クライアント側で検証を行い、ユーザーにエラーを迅速に通知することがあります。この場合、検証は、ログインページの`model`セグメントで行うことができます。スキーマ検証ライブラリ、例えば[Zod][ext-zod]をJS/TS用に使用し、このスキーマを`ui`セグメントに提供します。 + + +```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: "パスワードが一致しません", + path: ["confirmPassword"], +}); +``` + +次に、`ui`セグメントでこのスキーマを使用してユーザー入力を検証できます。 + +```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: ユーザーにエラーメッセージを表示 + } +} + +export function RegisterPage() { + return ( +
validate(new FormData(e.target))}> + + + + + + + + +
+ ) +} +``` + +## 資格情報をバックエンドに送信する方法 + +バックエンドのログインエンドポイントにリクエストを送信する関数を作成しましょう。この関数は、コンポーネントのコード内でミューテーションライブラリ(例えば、TanStack Query)を通じて直接呼び出すことも、状態管理ライブラリの副作用として呼び出すこともできます。 + +### リクエスト関数をどこに置くか + +この関数を置く場所は2つあります: `shared/api`またはページの`api`セグメントです。 + +#### `shared/api`に + +このアプローチは、すべてのリクエスト関数を`shared/api`に配置し、エンドポイントごとにグループ化するのに適しています。この場合、ファイル構造は次のようになります。 + +- 📂 shared + - 📂 api + - 📂 endpoints + - 📄 login.ts + - その他のリクエスト関数… + - 📄 client.ts + - 📄 index.ts + +`📄 client.ts`ファイルには、リクエストを実行するためのプリミティブのラッパーが含まれています(例えば、`fetch()`)。このラッパーは、バックエンドのベースURLを知っており、必要なヘッダーを設定し、データをシリアライズします。 + +```ts title="shared/api/endpoints/login.ts" +import { POST } from "../client"; + +export function login({ email, password }: { email: string, password: string }) { + return POST("/login", { email, password }); +} +``` + +```ts title="shared/api/index.ts" +export { login } from "./endpoints/login"; +``` + +#### ページの`api`セグメントに + +すべてのリクエストを1か所に保存していない場合は、ログインページの`api`セグメントにこのリクエスト関数を配置するのが適しているかもしれません。 + +- 📂 pages + - 📂 login + - 📂 api + - 📄 login.ts + - 📂 ui + - 📄 LoginPage.tsx + - 📄 index.ts + - その他のページ… + + +```ts title="pages/login/api/login.ts" +import { POST } from "shared/api"; + +export function login({ email, password }: { email: string, password: string }) { + return POST("/login", { email, password }); +} +``` + + +この関数は、ページのインデックスから再エクスポートする必要はありません。なぜなら、恐らくこのページ内でのみ使用されるからです。 + +### 2要素認証 + +アプリケーションが2要素認証(2FA)をサポートしている場合、ユーザーを一時的なパスワードを入力するための別のページにリダイレクトする必要があるかもしれません。通常、`POST /login`リクエストは、ユーザーに2FAが有効であることを示すフラグを持つユーザーオブジェクトを返します。このフラグが設定されている場合、ユーザーを2FAページにリダイレクトします。 + +このページはログインと非常に関連しているため、Pages層の同じ`login`スライスに配置することもできます。 + +また、上で作成した`login()`に似た別のリクエスト関数が必要になります。それらをShared層にまとめるか、ログインページの`api`セグメントに配置してください。 + +## 認証されたリクエスト用のトークンを保存する方法 {#how-to-store-the-token-for-authenticated-requests} + +使用する認証スキームに関係なく、単純なログインとパスワード、OAuth、または2要素認証であっても、最終的にはトークンを取得します。以降のリクエストで自分を識別できるように、このトークンは保存する必要があります。 + +ウェブアプリケーションにおけるトークンの理想的な保存場所は**クッキー**です。クッキーはトークンの手動保存や処理を必要としません。したがって、クッキーの保存はフロントエンドアーキテクチャにほとんど労力を必要としません。フロントエンドフレームワークにサーバーサイドがある場合(例えば、[Remix][ext-remix])、クッキーのサーバーインフラは`shared/api`に保存する必要があります。[「認証」チュートリアルセクション][tutorial-authentication]には、Remixでの実装例があります。 + +ただし、時にはトークンをクッキーに保存することができない場合もあります。この場合、トークンを自分で保存しなければなりません。その際、トークンの有効期限が切れたときに更新するロジックを書く手間がかかるかもしれません。FSDの枠組み内には、トークンを保存できるいくつかの場所と、そのトークンをアプリケーションの他の部分で利用できるようにするいくつかの方法があります。 + +### Shared層に保存する + +このアプローチは、APIクライアントが`shared/api`に定義されている場合にうまく機能します。なぜなら、APIクライアントがトークンに自由にアクセスできるからです。クライアントが状態を持つようにするには、リアクティブストアを使用するか、単にモジュールレベルの変数を使用することができます。その後、`login()`/`logout()`関数内でこの状態を更新できます。 + +トークンの自動更新は、APIクライアント内のミドルウェアとして実装できます。これは、リクエストを行うたびに実行されます。例えば、次のようにすることができます。 + +- 認証し、アクセストークンとリフレッシュトークンを保存する +- 認証を必要とするリクエストを行う +- リクエストがアクセストークンの有効期限切れを示すステータスコードで失敗した場合、ストレージにリフレッシュトークンがあれば、更新リクエストを行い、新しいアクセストークンとリフレッシュトークンを保存し、元のリクエストを再試行する + +このアプローチの欠点の1つは、トークンの保存と更新ロジックが専用の場所を持たないことです。これは、特定のアプリケーションやチームには適しているかもしれませんが、トークン管理のロジックがより複雑な場合、リクエスト送信とトークン管理の責任を分けたいと思うかもしれません。この場合、リクエストとAPIクライアントを`shared/api`に置き、トークンストレージと更新ロジックを`shared/auth`に配置します。 + +このアプローチのもう1つの欠点は、サーバーがトークンとともに現在のユーザーに関する情報を返す場合、その情報を保存する場所がなく、特別なエンドポイント(例えば`/me`や`/users/current`)から再度取得する必要があることです。 + +### Entities層に保存する + +FSDプロジェクトには、ユーザーエンティティや現在のユーザーエンティティが存在することがよくあります。これらは同じエンティティである場合もあります。 + +:::note + +**現在のユーザー**は時には「viewer」や「me」とも呼ばれます。これは、権限とプライベート情報を持つ認証されたユーザーと、公開情報を持つ他のすべてのユーザーを区別するために行われます。 + +::: + +ユーザーエンティティにトークンを保存するには、`model`セグメントにリアクティブストアを作成します。このストアには、トークンとユーザー情報のオブジェクトの両方を含めることができます。 + +APIクライアントは通常、`shared/api`に配置されるか、エンティティ間で分散されるため、このアプローチの主な問題は、他のリクエストがトークンにアクセスできるようにしつつ、[レイヤーのインポートルール][import-rule-on-layers]を破らないことです。 + +> スライス内のモジュールは、下層にあるスライスのみをインポートできる。 + +この問題にはいくつかの解決策があります。 + +1. **リクエストを行うたびにトークンを手動で渡す** + これは最も簡単な解決策ですが、すぐに不便になり、厳密な型付けがない場合は忘れやすくなります。この解決策は、Shared層のAPIクライアントのミドルウェアパターンとも互換性がありません。 + +2. **コンテキストや`localStorage`のようなグローバルストレージを介してアプリ全体にトークンへのアクセスを提供する** + トークンを取得するためのキーは`shared/api`に保存され、APIクライアントがそれを使用できるようにします。トークンのリアクティブストアはユーザーエンティティからエクスポートされ、必要に応じてコンテキストプロバイダーがApp層で設定されます。これにより、APIクライアントの設計に対する自由度が増しますが、このアプローチは暗黙の依存関係を生み出してしまいます。 + +3. **トークンが変更されるたびにAPIクライアントにトークンを挿入する** + リアクティブなストアであれば、変更を監視し、ユーザーエンティティのストアが変更されるたびにAPIクライアントのトークンを更新できます。この解決策は、前の解決策と同様に暗黙の依存関係を生み出してしまいますが、より命令的(「プッシュ」)であり、前のものはより宣言的(「プル」)です。 + +ユーザーエンティティのモデルに保存されたトークンの可用性の問題を解決したら、トークン管理に関連する追加のビジネスロジックを記述できます。例えば、`model`セグメントには、トークンを一定期間後に無効にするロジックや、期限切れのトークンを更新するロジックを含めることができます。これらのタスクを実行するために、ユーザーエンティティの`api`セグメント、または`shared/api`を使用します。 + +### Pages層/Widgets層に保存する(非推奨) + +アクセストークンのようなアプリ全体に関連する状態をページやウィジェットに保存することは推奨されません。ログインページの`model`セグメントにトークンストレージを配置しないでください。代わりに、最初の2つの解決策(Shared層配置かEntities層配置)のいずれかを選択してください。 + +## ログアウトとトークンの無効化 + +通常、アプリケーションではログアウト専用のページを作成しませんが、ログアウト機能は非常に重要です。この機能には、バックエンドへの認証リクエストとトークンストレージの更新が含まれます。 + +すべてのリクエストを`shared/api`に保存している場合は、ログイン関数の近くにログアウトリクエストの関数を配置してください。そうでない場合は、ログアウトを呼び出すボタンの近くに配置してください。例えば、すべてのページに存在し、ログアウトリンクを含むヘッダーウィジェットがある場合、そのリクエストをこのウィジェットの`api`セグメントに配置します。 + +トークンストレージの更新も、ログアウトボタンの場所からトリガーされる必要があります。リクエストとストレージの更新をこのウィジェットの`model`セグメントで統合できます。 + +### 自動ログアウト + +ログアウトリクエストやトークン更新リクエストの失敗を考慮することを忘れないでください。いずれの場合も、トークンストレージをリセットする必要があります。トークンをEntities層に保存している場合、このコードは`model`セグメントに配置できます。トークンをShared層に保存している場合、このロジックを`shared/api`に配置すると、セグメントが膨らみ、その目的が曖昧になってしまいます。`api`セグメントに無関係な2つのものが含まれていることに気づいた場合、トークン管理ロジックを別のセグメント、例えば`shared/auth`に分離することを検討してみてください。 + +[tutorial-authentication]: /docs/get-started/tutorial#authentication +[import-rule-on-layers]: /docs/reference/layers#import-rule-on-layers +[ext-remix]: https://remix.run +[ext-zod]: https://zod.dev \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/autocompleted.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/autocompleted.mdx new file mode 100644 index 0000000000..98102f4f7f --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/autocompleted.mdx @@ -0,0 +1,13 @@ +--- +sidebar_position: 5 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# 自動補完 + + + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/browser-api.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/browser-api.mdx new file mode 100644 index 0000000000..e5a3c130a8 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/browser-api.mdx @@ -0,0 +1,10 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# ブラウザAPI + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/cms.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/cms.mdx new file mode 100644 index 0000000000..e53b6cc5ff --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/cms.mdx @@ -0,0 +1,10 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# CMS + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/feedback.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/feedback.mdx new file mode 100644 index 0000000000..2f620c67cb --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/feedback.mdx @@ -0,0 +1,10 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# フィードバック + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/i18n.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/i18n.mdx new file mode 100644 index 0000000000..e736c4fe37 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/i18n.mdx @@ -0,0 +1,11 @@ +--- +sidebar_position: 6 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# i18n + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/index.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/index.mdx new file mode 100644 index 0000000000..9452069ba4 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/index.mdx @@ -0,0 +1,36 @@ +--- +hide_table_of_contents: true +--- + +# 例 + +

+小さな実践的な例を通じて、FSDの適用方法を示します。 +

+ +## 主要 {#main} + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { UserSwitchOutlined, LayoutOutlined, FontSizeOutlined } from "@ant-design/icons"; + + + + \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/metric.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/metric.mdx new file mode 100644 index 0000000000..fe2dcfcac3 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/metric.mdx @@ -0,0 +1,10 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# メトリクス + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/monorepo.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/monorepo.mdx new file mode 100644 index 0000000000..42cb3c59a9 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/monorepo.mdx @@ -0,0 +1,11 @@ +--- +sidebar_position: 9 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# モノレポ + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/page-layout.md b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/page-layout.md new file mode 100644 index 0000000000..baa8ef1a9f --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/page-layout.md @@ -0,0 +1,104 @@ +--- +sidebar_position: 3 +--- + +# ページレイアウト + +このガイドでは、複数のページが同じ構造を持ち、主な内容だけが異なる場合のページレイアウトの抽象化について説明します。 + +:::info + +あなたの質問がこのガイドにない場合は、この記事にフィードバックを残して質問を投稿してください(右側の青いボタン)、私たちはこのガイドを拡張する可能性を検討します! + +::: + +## シンプルなレイアウト + +最もシンプルなレイアウトは、このページで直接見ることができます。これは、サイトのナビゲーションを含むヘッダー、2つのサイドバー、外部リンクを含むフッターを持っています。ここには複雑なビジネスロジックはなく、唯一の動的部分はサイドバーとヘッダーの右側にあるトグルスイッチです。このレイアウトは、`shared/ui`または`app/layouts`に全体を配置でき、サイドバーのコンテンツはプロパティを通じて埋め込むことができます。 + +```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
  • +
  • X
  • +
+
+
+ ); +} +``` + +```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; +} +``` + + +サイドバーのコードは読者に課題として残されています! + +## レイアウトでのウィジェットの使用 + +時には、特定のビジネスロジックをレイアウトに組み込む必要があります。特に、[React Router][ext-react-router]のような深くネストされたルートを使用している場合、Shared層やWidgets層にレイアウトを保存することはできません。これは[レイヤーのインポートルール][import-rule-on-layers]に違反しています。 + +> スライス内のモジュールは、下層にあるスライスのみをインポートできる。 + +解決策を議論する前に、これが実際に問題かどうかを確認する必要があります。このレイアウトは本当に必要なのか?もしそうなら、ウィジェットとして実装することが最適なのかも再考する必要があるでしょう。もしビジネスロジックのブロックが2〜3ページで使用され、レイアウトがそのウィジェットの小さなラッパーに過ぎない場合、次の2つのオプションを検討してください。 + +1. **レイアウトをApp層のルーターで直接作成する** + これは、ネストをサポートするルーターに最適です。特定のルートをグループ化し、必要なレイアウトをそれらにのみ適用できます。 + +2. **単にコピーする** + コードを抽象化する欲求はしばしば過大評価されます。特にレイアウトに関しては、変更がほとんどないためです。ある時点で、これらのページの1つが変更を必要とする場合、他のページに影響を与えずに変更を加えることができます。他のページを更新することを忘れるかもしれないと心配している場合は、ページ間の関係を説明するコメントを残すことができます。 + +上記のいずれのオプションも適用できない場合、ウィジェットをレイアウトに組み込むための2つの解決策があります。 + +1. **レンダープロップまたはスロットを使用する** + ほとんどのフレームワークは、UIの一部を外部から渡すことを許可しています。Reactではこれを[レンダープロップ][ext-render-props]と呼び、Vueでは[スロット][ext-vue-slots]と呼びます。 + +2. **レイアウトをApp層に移動する** + レイアウトをApp層に保存し、必要なウィジェットを組み合わせることもできます。 + +## 追加資料 + +- ReactとRemixを使用した認証付きレイアウトの作成例は[チュートリアル][tutorial]で見つけることができます(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://jp.vuejs.org/guide/components/slots \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/platforms.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/platforms.mdx new file mode 100644 index 0000000000..79f3389bb6 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/platforms.mdx @@ -0,0 +1,10 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# デスクトップ/タッチプラットフォーム + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/ssr.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/ssr.mdx new file mode 100644 index 0000000000..51fdbba8de --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/ssr.mdx @@ -0,0 +1,10 @@ +--- +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# SSR + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/theme.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/theme.mdx new file mode 100644 index 0000000000..9de66e03b0 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/theme.mdx @@ -0,0 +1,11 @@ +--- +sidebar_position: 4 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# テーマ化 + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/types.md b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/types.md new file mode 100644 index 0000000000..d0111f68b1 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/types.md @@ -0,0 +1,436 @@ +--- +sidebar_position: 2 +--- + +# 型 + +このガイドでは、TypeScriptのような型付き言語のデータの型と、それがFSDにどのように適合するかについて説明します。 + +:::info + +あなたの質問がこのガイドにない場合は、この記事にフィードバックを残して質問を投稿してください(右側の青いボタン)、私たちはこのガイドを拡張する可能性を検討します! + +::: + +## ユーティリティ型 + +ユーティリティ型は、特に意味を持たず、通常は他の型と一緒に使用される型です。例えば + +
+ + +```ts +type ArrayValues = T[number]; +``` + + +
+ 出典: 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 (ユーティリティ型) + - 📄 getMemoryUsageMetrics.ts (このユーティリティを使用するコード) + +:::warning + +`shared/types`フォルダーを作成したり、スライスに`types`セグメントを追加する誘惑に負けないでください。「型」というカテゴリは「コンポーネント」や「フック」と同様に、内容を説明するものであり、目的を示すものではありません。セグメントはコードの目的を説明するべきであり、その本質を説明するべきではありません。 + +::: + +## ビジネスエンティティと相互参照 + +アプリケーションで最も重要な型の一つは、ビジネスエンティティの型、つまりアプリケーションが扱う実際のオブジェクトです。例えば、オンライン音楽サービスのアプリケーションでは、ビジネスエンティティとして「曲」(song)や「アルバム」(album)などがあります。 + +ビジネスエンティティは、しばしばバックエンドから提供されるため、最初のステップはバックエンドのレスポンスを型付けすることです。各エンドポイントに対してリクエスト関数を持ち、その関数の呼び出し結果を型付けするのが便利です。型の安全性を高めるために、Zodのようなスキーマ検証ライブラリを通じて結果を通過させることができます。 + +例えば、すべてのリクエストを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]によって制限しているからです。 + +> スライス内のモジュールは、下層にあるスライスのみをインポートできる。 + +この問題を解決する方法は2つあります。 + +1. **型をパラメーター化する** + 型が他のエンティティと接続するためのスロットとして型引数を受け取るようにすることができます。さらに、これらのスロットに制約を課すこともできます。例えば + + ```ts title="entities/song/model/song.ts" + interface Song { + id: number; + title: string; + artists: Array; + } + ``` + + これはいくつかの型に対してはうまく機能しますが、機能しないケースもあります。`Cart = { items: Array }`のような単純な型は、任意のプロダクト型で簡単に機能させることができます。しかし、`Country`と`City`のようなより関連性の高い型は、分離するのが難しいかもしれません。 + +2. **クロスインポートする(正しく)** + FSD内でエンティティ間のクロスインポートを行うには、各スライス専用の特別の公開APIを使用することができます。例えば、`song`(曲)、`artist`(アーティスト)、`playlist`(プレイリスト)のエンティティがあり、後者の2つが`song`を参照する必要がある場合、`@x`ノーテーションを通じて`song`エンティティ内に2つの特別な公開APIを作成できます。 + + - 📂 entities + - 📂 song + - 📂 @x + - 📄 artist.ts (公開API、`artist`エンティティをインポートする) + - 📄 playlist.ts (公開API、`playlist`エンティティをインポートする) + - 📄 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; + } + ``` + + エンティティ間の明示的な関係を持つことで、依存関係を正確に制御し、ドメインの分離を十分に保つことができます。 + +## データ転送オブジェクト(DTO)とマッパー {#data-transfer-objects-and-mappers} + +データ転送オブジェクト、またはDTO(Data Transfer Object)は、バックエンドから送信されるデータの形式を説明する用語です。時には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; + /** 曲の完全なタイトル、ディスク番号を含む。 */ + 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)); +} +``` + +リクエストとストレージがエンティティスライスに定義されている場合、このすべてのコードはそこに置くべきであり、エンティティ間のクロスインポートの制限を考慮する必要があります。 + +```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; + /** 曲の完全なタイトル、ディスク番号を含む。 */ + 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だけでなく、アーティストのデータオブジェクト全体を含む場合です。この場合、エンティティは互いに知らないわけにはいきません(データを捨てたり、バックエンドチームと真剣に話し合いたくない場合を除いて)。スライス間の暗黙的な関係の解決策を考えるのではなく、`@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でエンティティのスキーマを宣言 +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) + // データを正規化して、リデューサーが予測可能なオブジェクトをロードできるようにします。例えば + // `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) => { + // ここでバックエンドからの同じレスポンスを処理し、ユーザーを追加します + usersAdapter.upsertMany(state, action.payload.users) + }) + }, +}) + +const reducer = slice.reducer +export default reducer +``` + +これはスライスの分離の利点を少し制限しますが、私たちが制御できないこれらの2つのエンティティ間の関係を明確に示します。これらのエンティティがリファクタリングされる場合、同時にリファクタリングする必要があります。 + +## グローバルの型とRedux + +グローバルの型とは、アプリケーション全体で使用される型のことです。グローバルの型には、必要な情報に応じて2種類があります。 +1. アプリケーションに特有の情報を持たないユニバーサル型 +2. アプリケーション全体について知る必要がある型 + +最初のケースは簡単に解決できます。型をShared層の適切なセグメントに置くだけです。例えば、分析用のグローバル変数のインターフェースがある場合、それを`shared/analytics`に置くことができます。 + +:::warning + +`shared/types`フォルダーを作成することは避けてください。これは「型である」という特性に基づいて無関係なものをグループ化するだけであり、この特性はプロジェクト内でコードを検索する際には通常無意味です。 + +::: + +2番目のケースは、RTKなしでReduxを使用しているプロジェクトでよく見られます。最終的なストアの型は、すべてのリデューサーを結合した後にのみ利用可能ですが、このストアの型はアプリケーションで使用されるセレクターに必要です。例えば、以下はReduxでのストアの典型的な定義です。 + +```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`に型付けされた`useAppDispatch`と`useAppSelector`のフックを持つことは良いアイデアですが、[レイヤーのインポートルール][import-rule-on-layers]のために、App層から`RootState`と`AppDispatch`をインポートすることはできません。 + +> スライス内のモジュールは、下層にあるスライスのみをインポートできる。 + +この場合の推奨解決策は、Shared層とApp層の間に暗黙の依存関係を作成することです。これらの2つの型、`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; +``` + +## 型のバリデーションスキーマとZod + +データが特定の形式や制約に従っていることを確認したい場合、バリデーションスキーマを作成できます。TypeScriptでこの目的に人気のあるライブラリは[Zod][ext-zod]です。バリデーションスキーマは、可能な限りそれを使用するコードの近くに配置する必要があります。 + +バリデーションスキーマは、データ転送オブジェクト(DTO)と似ており([DTOとマッパー](#data-transfer-objects-and-mappers)のセクションで説明)、DTOを受け取り、それを解析し、解析に失敗した場合はエラーを返します。 + +バリデーションの最も一般的なケースの一つは、バックエンドからのデータです。通常、データがスキーマに従わない場合、リクエストを失敗としてマークしたいので、リクエスト関数と同じ場所にスキーマを置くのが良いでしょう。通常、これは`api`セグメントになります。 + +ユーザー入力を介してデータが送信される場合、例えばフォームを通じて、バリデーションはデータ入力時に行わなければなりません。この場合、スキーマを`ui`セグメントに配置し、フォームコンポーネントの近くに置くか、`ui`セグメントが過負荷である場合は`model`セグメントに配置できます。 + +## コンポーネントのプロップスとコンテキストの型付け + +一般的に、プロップスやコンテキストのインターフェースは、それを使用するコンポーネントやコンテキストと同じファイルに保存するのが最良です。VueやSvelteのように、単一ファイルコンポーネントを持つフレームワークの場合、インターフェースを同じファイルに定義できない場合や、複数のコンポーネント間でこのインターフェースを再利用したい場合は、通常は`ui`セグメント内の同じフォルダーに別のファイルを作成します。 + +以下は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スキーマからバックエンドの型を生成することができます。この場合、これらの型のためにコード内に特別な場所を作成します。例えば、`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 \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/white-labels.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/white-labels.mdx new file mode 100644 index 0000000000..b536ecddbc --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/examples/white-labels.mdx @@ -0,0 +1,11 @@ +--- +sidebar_position: 8 +sidebar_class_name: sidebar-item--wip +unlisted: true +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# ホワイトラベル + + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/index.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/index.mdx new file mode 100644 index 0000000000..f6b7376683 --- /dev/null +++ b/i18n/ja/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の適用に関する実践的なガイドと例です。このセクションでは、移行ガイドや悪習のハンドブックも説明されています。具体的な何かを実現しようとしているときや、FSDを「実戦」で見たいときに最も役立ちます。 +

+ +## 主な内容 {#main} + +import NavCard from "@site/src/shared/ui/nav-card/tmpl.mdx" +import { ToolOutlined, ImportOutlined, BugOutlined, FunctionOutlined } from "@ant-design/icons"; + + + + + + \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/_category_.yaml b/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/_category_.yaml new file mode 100644 index 0000000000..8d2bd9c676 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/_category_.yaml @@ -0,0 +1,2 @@ +label: コードの臭いと問題 +position: 4 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/cross-imports.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/cross-imports.mdx new file mode 100644 index 0000000000..92183efece --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/cross-imports.mdx @@ -0,0 +1,13 @@ +--- +sidebar_position: 4 +sidebar_class_name: sidebar-item--wip +pagination_next: reference/index +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# クロスインポート + + + +> クロスインポートは、レイヤーや抽象化が本来の責任以上に多くの責任を持ち始めると発生する。そのため、FSDは新しいレイヤーを設けて、これらのクロスインポートを分離することを可能にしている。 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/desegmented.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/desegmented.mdx new file mode 100644 index 0000000000..28f3bddc42 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/desegmented.mdx @@ -0,0 +1,96 @@ +--- +sidebar_position: 2 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# デセグメンテーション + + + +## 状況 {#situation} + +プロジェクトでは、特定のドメインに関連するモジュールが過度にデセグメント化され、プロジェクト全体に散らばっていることがよくあります。 + +```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/ +``` + + +## 問題 {#problem} + +問題は、**高い凝集性**の原則の違反と、**変更の軸**の過度な拡張として現れます。 + +## 無視する場合 {#if-you-ignore-it} + +- 例えば、配達に関するロジックに触れる必要がある場合、このロジックが複数の箇所に分散していることを考慮しなければならず、コード内で複数の箇所に触れる必要がある。これにより、**変更の軸**が過度に引き伸ばされる +- ユーザーに関するロジックを調べる必要がある場合、**actions、epics、constants、entities、components**の詳細を調べるためにプロジェクト全体を巡回しなければならない +- 暗黙関係と拡大するドメインの制御不能 + - このアプローチでは、視野が狭くなり、「定数のための定数」を作成し、プロジェクトの該当ディレクトリをごちゃごちゃさせてしまうことに気づかないことがよくある + +## 解決策 {#solution} + +特定のドメイン/ユースケースに関連するすべてのモジュールを近くに配置することです。 + +これは特定のモジュールを調べる際に、そのすべての構成要素がプロジェクト全体に散らばらず、近くに配置されるためです。 + +> これにより、コードベースとモジュール間の関係の発見しやすさと明確さが向上します。 + +```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} +* [(記事) Cohesion and Coupling: the difference](https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/) diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx new file mode 100644 index 0000000000..10dc329e10 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx @@ -0,0 +1,42 @@ +--- +sidebar_position: 3 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# ルーティング + + + +## 状況 {#situation} + +ページへのURLが、pages層より下の層にハードコーディングされています。 + +```tsx title="entities/post/card" + + + + ... + +``` + + +## 問題 {#problem} + +URLがページ層に集中しておらず、責任範囲において適切な場所に配置されていません。 + +## 無視する場合 {#if-you-ignore-it} + +URLを変更する際に、URL(およびURL/リダイレクトのロジック)がpages層以外のすべての層に存在する可能性があることを考慮しなければなりません。 + +また、これは単純な商品カードでさえ、ページからの一部の責任を引き受けることを意味し、プロジェクト全体にロジックが分散してしまいます。 + +## 解決策 {#solution} + +URLやリダイレクトの処理をページ層およびそれ以上の層で定義することです。 + +URLを下層の層には、コンポジション/プロパティ/ファクトリーを通じて渡すことができます。 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/migration/_category_.yaml b/i18n/ja/docusaurus-plugin-content-docs/current/guides/migration/_category_.yaml new file mode 100644 index 0000000000..32b7491cea --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/migration/_category_.yaml @@ -0,0 +1,2 @@ +label: 移行 +position: 2 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/migration/from-custom.md b/i18n/ja/docusaurus-plugin-content-docs/current/guides/migration/from-custom.md new file mode 100644 index 0000000000..94b6a2e3bb --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/migration/from-custom.md @@ -0,0 +1,306 @@ +--- +sidebar_position: 3 +sidebar_label: カスタムアーキテクチャからの移行 +--- + +# カスタムアーキテクチャからの移行 + +このガイドは、カスタムのアーキテクチャから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} + +Feature-Sliced Designへの移行を検討する際に、チームに最も重要な質問は「本当に必要か?」です。私たちはFeature-Sliced Designが好きですが、いくつかのプロジェクトはそれなしでうまくいけることを認めています。 + +移行を検討すべき理由はいくつかあります。 + +1. 新しいチームメンバーが生産的なレベルに達するのが難しいと不満を言う。 +2. コードの一部を変更すると、**しばしば**他の無関係な部分が壊れる。 +3. 巨大なコードベースのため、新しい機能を追加するのが難しい。 + +**同僚の意に反してFSDに移行することは避けてください**。たとえあなたがチームリーダーであっても、まずは同僚を説得し、移行の利点がコストを上回ることを理解させる必要があります。 + +また、アーキテクチャの変更は、瞬時には経営陣には見えないことを覚えておいてください。始める前に、経営陣が移行を支持していることを確認し、この移行がプロジェクトにどのように役立つかを説明してください。 + +:::tip + +プロジェクトマネージャーをFSDの有用性に納得させるためのアイデアをいくつか紹介します。 + +1. FSDへの移行は段階的に行えるため、新機能の開発を止めることはありません。 +2. 良いアーキテクチャは、新しい開発者が生産性を達成するのにかかる時間を大幅に短縮できます。 +3. FSDは文書化されたアーキテクチャであるため、チームは独自の文書を維持するために常に時間を費やす必要がありません。 + +::: + +--- + +もし移行を始める決断をした場合、最初に行うべきことは`📁 src`のエイリアスを設定することです。これは、後で上位フォルダを参照するのに便利です。以降のテキストでは、`@`を`./src`のエイリアスとして扱います。 + +## ステップ1。コードをページごとに分割する {#divide-code-by-pages} + +ほとんどのカスタムアーキテクチャは、ロジックのサイズに関係なく、すでにページごとに分割されています。もし`📁 pages`がすでに存在する場合は、このステップをスキップできます。 + +もし`📁 routes`しかない場合は、`📁 pages`を作成し、できるだけ多くのコンポーネントコードを`📁 routes`から移動させてみてください。理想的には、小さなルートファイルと大きなページファイルがあることです。コードを移動させる際には、各ページのためのフォルダを作成し、その中にインデックスファイルを追加します。 + +:::note + +現時点では、ページ同士が互いにインポートすることは問題ありません。後でこれらの依存関係を解消するための別のステップがあります。今はページごとの明確な分割を確立することに集中します。 + +::: + +ルートファイル + +```js title="src/routes/products.[id].js" +export { ProductPage as default } from "@/pages/product" +``` + +ページのインデックスファイル + +```js title="src/pages/product/index.js" +export { ProductPage } from "./ProductPage.jsx" +``` + +ページコンポーネントファイル + +```jsx title="src/pages/product/ProductPage.jsx" +export function ProductPage(props) { + return
; +} +``` + +## ステップ2。 ページ以外のすべてを分離する {#separate-everything-else-from-pages} + +`📁 src/shared`フォルダを作成し、`📁 pages`や`📁 routes`からインポートされないすべてをそこに移動します。`📁 src/app`フォルダを作成し、ページやルートをインポートするすべてをそこに移動します。 + +Shared層にはスライスがないことを覚えておいてください。したがって、セグメントは互いにインポートできます。 + +最終的には、次のようなファイル構造になるはずです。 + +
+ 📁 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
    • +
    +
    +
  • +
+
+ +## ステップ3。 ページ間のクロスインポートを解消する {#tackle-cross-imports-between-pages} + +あるページが他のページから何かをインポートしているすべての箇所を見つけ、次のいずれかを行います。 + +1. インポートされているコードを依存するページにコピーして、依存関係を取り除く。 +2. コードをShared層の適切なセグメントに移動する + - UIキットの一部であれば、`📁 shared/ui`に移動。 + - 設定の定数であれば、`📁 shared/config`に移動。 + - バックエンドとのやり取りであれば、`📁 shared/api`に移動。 + +:::note + +**コピー自体はアーキテクチャの問題ではありません**。実際、時には新しい再利用可能なモジュールに何かを抽象化するよりも、何かを複製する方が正しい場合もあります。ページの共通部分が異なってくることがあるため、その場合、依存関係が妨げにならないようにする必要があります。 + +ただし、DRY("don't repeat yourself" — "繰り返さない")の原則には意味があるため、ビジネスロジックをコピーしないようにしてください。そうしないと、バグを複数の箇所で修正することになることを頭に入れておく必要があります。 + +::: + +## ステップ4。 Shared層を分解する {#unpack-shared-layer} + +この段階では、Shared層に多くのものが入っているかもしれませんが、一般的にはそのような状況を避けるべきです。理由は、Shared層に依存している他の層にあるコードが存在している可能性があるため、そこに変更を加えることは予期しない結果を引き起こす可能性が高くなります。 + +特定のページでのみ使用されるすべてのオブジェクトを見つけ、それらをそのページのスライスに移動します。そして、_これにはアクション、リデューサー、セレクターも含まれます_。すべてのアクションを一緒にグループ化することには意味がありませんが、関連するアクションをその使用場所の近くに置くことには意味があります。 + +最終的には、次のようなファイル構造になるはずです。 + +
+ 📁 src +
    +
  • 📁 app (変更なし)
  • +
  • +
    + 📁 pages +
      +
    • +
      + 📁 product +
        +
      • 📁 actions
      • +
      • 📁 reducers
      • +
      • 📁 selectors
      • +
      • +
        + 📁 ui +
          +
        • 📄 Component.jsx
        • +
        • 📄 Container.jsx
        • +
        • 📄 ProductPage.jsx
        • +
        +
        +
      • +
      • 📄 index.js
      • +
      +
      +
    • +
    • 📁 catalog
    • +
    +
    +
  • +
  • +
    + 📁 shared (再利用されるオブジェクトのみ) +
      +
    • 📁 actions
    • +
    • 📁 api
    • +
    • 📁 components
    • +
    • 📁 containers
    • +
    • 📁 constants
    • +
    • 📁 i18n
    • +
    • 📁 modules
    • +
    • 📁 helpers
    • +
    • 📁 utils
    • +
    • 📁 reducers
    • +
    • 📁 selectors
    • +
    • 📁 styles
    • +
    +
    +
  • +
+
+ +## ステップ5。 コードを技術的な目的に基づいて整理する {#organize-by-technical-purpose} + +FSDでは、技術的な目的に基づく分割がセグメントによって行われます。よく見られるセグメントはいくつかあります。 + +- `ui` — インターフェースの表示に関連するすべて: UIコンポーネント、日付のフォーマット、スタイルなど。 +- `api` — バックエンドとのやり取り: リクエスト関数、データ型、マッパーなど。 +- `model` — データモデル: スキーマ、インターフェース、ストレージ、ビジネスロジック。 +- `lib` — 他のモジュールに必要なライブラリコード。 +- `config` — 設定ファイルやフィーチャーフラグ。 + +必要に応じて独自のセグメントを作成できます。ただし、コードをその性質によってグループ化するセグメント(例: `components`、`actions`、`types`、`utils`)を作成しないようにしてください。代わりに、コードの目的に基づいてグループ化してください。 + +ページのコードをセグメントに再分配します。すでに`ui`セグメントがあるはずなので、今は他のセグメント(例えば、アクション、リデューサー、セレクターのための`model`や、サンクやミューテーションのための`api`)を作成するときです。 + +また、Shared層を再分配して、次のフォルダを削除します。 + +- `📁 components`、`📁 containers` — その内容のほとんどは`📁 shared/ui`になるべきです。 +- `📁 helpers`、`📁 utils` — 再利用可能なヘルパー関数が残っている場合は、目的に基づいてグループ化し、これらのグループを`📁 shared/lib`に移動します。 +- `📁 constants` — 同様に、目的に基づいてグループ化し、`📁 shared/config`に移動します。 + +## 任意のステップ {#optional-steps} + +### ステップ6。 複数のページで使用されるReduxスライスからエンティティ/フィーチャーを形成する {#form-entities-features-from-redux} + +通常、これらの再利用可能なReduxスライスは、ビジネスに関連する何かを説明します(例えば、プロダクトやユーザーなど)。したがって、それらをエンティティ層に移動できます。1つのエンティティにつき1つのフォルダです。Reduxスライスが、ユーザーがアプリケーションで実行したいアクションに関連している場合(例えば、コメントなど)、それをフィーチャー層に移動できます。 + +エンティティとフィーチャーは互いに独立している必要があります。ビジネス領域に組み込まれたエンティティ間の関係がある場合は、[ビジネスエンティティに関するガイド][business-entities-cross-relations]を参照して、これらの関係を整理する方法を確認してください。 + +これらのスライスに関連するAPI関数は、`📁 shared/api`に残すことができます。 + +### ステップ7。 モジュールをリファクタリングする {#refactor-your-modules} + +`📁 modules`フォルダは通常、ビジネスロジックに使用されるため、すでにFSDのフィーチャー層に似た性質を持っています。一部のモジュールは、アプリケーションの大きな部分(例えば、アプリのヘッダーなど)を説明することもあります。この場合、それらをウィジェット層に移動できます。 + +### ステップ8。 `shared/ui`にUI基盤を正しく形成する {#form-clean-ui-foundation} + +理想的には、`📁 shared/ui`にはビジネスロジックが含まれていないUI要素のセットが含まれるべきです。また、非常に再利用可能である必要があります。 + +以前`📁 components`や`📁 containers`にあったUIコンポーネントをリファクタリングして、ビジネスロジックを分離します。このビジネスロジックを上位層に移動します。あまり多くの場所で使用されていない場合は、コピーを検討することもできます。 + +[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/ja/docusaurus-plugin-content-docs/current/guides/migration/from-v1.md b/i18n/ja/docusaurus-plugin-content-docs/current/guides/migration/from-v1.md new file mode 100644 index 0000000000..4387ebba47 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/migration/from-v1.md @@ -0,0 +1,154 @@ +--- +sidebar_position: 4 +--- + +# v1からの移行 + +## なぜv2なのか? {#why-v2} + +初期の**feature-slices**の概念は、2018年に提唱されました。 + +それ以来、FSD方法論は多くの変革を経てきましたが、基本的な原則は保持されています。 + +- *標準化された*フロントエンドプロジェクト構造の使用 +- アプリケーションを*ビジネスロジック*に基づいて分割 +- *孤立した機能*の使用により、暗黙の副作用や循環依存を防止 +- モジュールの「内部」にアクセスすることを禁止する*公開API*の使用 + +しかし、以前のバージョンのFSD方法論には依然として**弱点が残っていました**。 + +- ボイラープレートの発生 +- コードベースの過剰な複雑化と抽象化間の明確でないルール +- プロジェクトのメンテナンスや新しいメンバーのオンボーディングを妨げていた暗黙のアーキテクチャ的決定 + +新しいバージョンのFSD方法論([v2][ext-v2])は、**これらの欠点を解消しつつ、既存の利点を保持することを目的としています**。 + +2018年以降、[**feature-driven**][ext-fdd]という別の類似の方法論が[発展してきました][ext-fdd-issues]。それを最初に提唱したのは[Oleg Isonen][ext-kof]でした。 + +2つのアプローチの統合により、**既存のプラクティスが改善され、柔軟性、明確さ、効率が向上しました**。 + +> 結果として、方法論の名称も「feature-slice**d**」に変更されました。 + +## なぜプロジェクトをv2に移行する意味があるのか? {#why-does-it-make-sense-to-migrate-the-project-to-v2} + +> `WIP:` 現在の方法論のバージョンは開発中であり、一部の詳細は*変更される可能性があります*。 + +#### 🔍 より透明でシンプルなアーキテクチャ {#-more-transparent-and-simple-architecture} + +FSD(v2)は、**より直感的で、開発者の間で広く受け入れられている抽象化とロジックの分割方法を提供しています**。 + +これにより、新しいメンバーの参加やプロジェクトの現状理解、アプリケーションのビジネスロジック分配に非常に良い影響を与えます。 + +#### 📦 より柔軟で誠実なモジュール性 {#-more-flexible-and-honest-modularity} + +FSD(v2)は、**より柔軟な方法でロジックを分配することを可能にしています**。 + +- 孤立した部分をゼロからリファクタリングできる +- 同じ抽象化に依存しつつ、余計な依存関係の絡みを避けられる +- 新しいモジュールの配置をよりシンプルにできる *(layer → slice → segment)* + +#### 🚀 より多くの仕様、計画、コミュニティ {#-more-specifications-plans-community} + +`core-team`は最新の(v2)バージョンのFSD方法論に積極的に取り組んでいます。 + +したがって、以下のことが期待できます。 + +- より多くの記述されたケース/問題 +- より多くの適用ガイド +- より多くの実例 +- 新しいメンバーのオンボーディングや方法論概念の学習のための全体的な文書の増加 +- 方法論の概念とアーキテクチャに関するコンベンションを遵守するためのツールキットのさらなる発展 + +> もちろん、初版に対するユーザーサポートも行われますが、私たちにとっては最新のバージョンが最優先です。 + +> 将来的には、次のメジャーアップデートの際に、現在のバージョン(v2)へのアクセスが保持され、**チームやプロジェクトにリスクをもたらすことはありません**。 + +## Changelog + +### `BREAKING` Layers + +FSD方法論は上位レベルでの層の明示的な分離を前提としています。 + +- `/app` > `/processes` > **`/pages`** > **`/features`** > `/entities` > `/shared` +- *つまり、すべてがフィーチャーやページとして解釈されるわけではない* +- このアプローチにより、層のルールを明示的に設定することが可能になる + - モジュールの**層が高いほど**、より多くの**コンテキスト**を持つことができる + + *(言い換えれば、各層のモジュールは、下層のモジュールのみをインポートでき、上層のモジュールはインポートできない)* + - モジュールの**層が低いほど**、変更を加える際の**危険性と責任**が増す + + *(一般的に、再利用されるのは下層のモジュールらからである)* + +### `BREAKING` Shared層 + +以前はプロジェクトのsrcルートにあったインフラストラクチャの `/ui`, `/lib`, `/api` 抽象化は、現在 `/src/shared` という別のディレクトリに分離されています。 + +- `shared/ui` - アプリケーションの共通UIキット(オプション) + - *ここで`Atomic Design`を使用することは引き続き許可されている* +- `shared/lib` - ロジックを実装するための補助ライブラリセット + - *引き続き、ヘルパー関数の「ごみ屋敷」を作らずに* +- `shared/api` - APIへのアクセスのための共通エントリポイント + - *各フィーチャー/ページにローカルに記述することも可能だが、推奨されない* +- 以前と同様に、`shared`にはビジネスロジックへの明示的な依存関係があってはならない + - *必要に応じて、この依存関係は`entities`、またはそれ以上の層に移動する必要がある* + +### `新規` Entities層, Processes層 + +v2では、**ロジックの複雑さと強い結合の問題を解消するために、新しい抽象化が追加されました**。 + +- `/entities` - **ビジネスエンティティ**の層で、ビジネスモデルやフロントエンド専用の合成エンティティに関連するスライスを含む + - *例:`user`, `i18n`, `order`, `blog`* +- `/processes` - アプリケーション全体にわたる**ビジネスプロセス**の層 + - **この層はオプションであり、通常は*ロジックが拡大し、複数のページにまたがる場合に使用が推奨される*** + - *例:`payment`, `auth`, `quick-tour`* + +### `BREAKING` 抽象化と命名 + +具体的な抽象化とその命名に関する[明確なガイドライン][refs-adaptability]が定義されています。 + +#### Layers + +- `/app` — **アプリケーションの初期化層** + - *以前のバリエーション: `app`, `core`, `init`, `src/index`* +- `/processes` — **ビジネスプロセスの層** + - *以前のバリエーション: `processes`, `flows`, `workflows`* +- `/pages` — **アプリケーションのページ層** + - *以前のバリエーション: `pages`, `screens`, `views`, `layouts`, `components`, `containers`* +- `/features` — **機能部分の層** + - *以前のバリエーション: `features`, `components`, `containers`* +- `/entities` — **ビジネスエンティティの層** + - *以前のバリエーション: `entities`, `models`, `shared`* +- `/shared` — **再利用可能なインフラストラクチャコードの層** 🔥 + - *以前のバリエーション: `shared`, `common`, `lib`* + +#### Segments + +- `/ui` — **UIセグメント** 🔥 + - *以前のバリエーション:`ui`, `components`, `view`* +- `/model` — **ビジネスロジックのセグメント** 🔥 + - *以前のバリエーション:`model`, `store`, `state`, `services`, `controller`* +- `/lib` — **補助コードのセグメント** + - *以前のバリエーション:`lib`, `libs`, `utils`, `helpers`* +- `/api` — **APIセグメント** + - *以前のバリエーション:`api`, `service`, `requests`, `queries`* +- `/config` — **アプリケーション設定のセグメント** + - *以前のバリエーション:`config`, `env`, `get-env`* + +### `REFINED` 低結合 + +新しいレイヤーのおかげで、モジュール間の[低結合の原則][refs-low-coupling]を遵守することがはるかに簡単になりました。 + +*それでも、モジュールを「切り離す」ことが非常に難しい場合は、できるだけ避けることが推奨されます*。 + +## 参照 {#see-also} + +- [React Berlin Talk - Oleg Isonen "Feature Driven Architecture"][ext-kof-fdd] + +[refs-low-coupling]: /docs/reference/isolation/coupling-cohesion +[refs-adaptability]: /docs/about/understanding/naming + +[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 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/_category_.yaml b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/_category_.yaml new file mode 100644 index 0000000000..b4c34b26eb --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/_category_.yaml @@ -0,0 +1,2 @@ +label: 技術 +position: 3 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-nextjs.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-nextjs.mdx new file mode 100644 index 0000000000..67de70dec4 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-nextjs.mdx @@ -0,0 +1,109 @@ +--- +sidebar_position: 10 +--- +# NextJSとの併用 + +NextJSプロジェクトでFSDを実装することは可能ですが、プロジェクトの構造に関するNextJSの要件とFSDの原則の間に2つの点で対立が生じます。 + +- `pages`のファイルルーティング +- NextJSにおける`app`の対立、または欠如 + +## `pages`におけるFSDとNextJSの対立 {#pages-conflict} + +NextJSは、アプリケーションのルートを定義するために`pages`フォルダーを使用することを提案しています。`pages`フォルダー内のファイルがURLに対応することを期待しています。このルーティングメカニズムは、FSDの概念に**適合しません**。なぜなら、このようなルーティングメカニズムでは、スライスの平坦な構造を維持することができないからです。 + +### NextJSの`pages`フォルダーをプロジェクトのルートフォルダーに移動する(推奨) + +このアプローチは、NextJSの`pages`フォルダーをプロジェクトのルートフォルダーに移動し、FSDのページをNextJSの`pages`フォルダーにインポートすることにあります。これにより、`src`フォルダー内でFSDのプロジェクト構造を維持できます。 + +```sh +├── pages # NextJSのpagesフォルダー +├── src +│ ├── app +│ ├── entities +│ ├── features +│ ├── pages # FSDのpagesフォルダー +│ ├── shared +│ ├── widgets +``` + +### FSD構造における`pages`フォルダーの名前変更 + +もう一つの解決策は、FSD構造内の`pages`層の名前を変更して、NextJSの`pages`フォルダーとの名前衝突を避けることです。 +FSDの`pages`層を`views`層に変更することができます。 +このようにすることで、`src`フォルダー内のプロジェクト構造は、NextJSの要件と矛盾することなく保持されます。 + + +```sh +├── app +├── entities +├── features +├── pages # NextJSのpagesフォルダー +├── views # 名前が変更されたFSDのページフォルダー +├── shared +├── widgets +``` + +この場合、プロジェクトのREADMEや内部ドキュメントなど、目立つ場所にこの名前変更を文書化することをお勧めします。この名前変更は、[「プロジェクト知識」][project-knowledge]の一部です。 + +## NextJSにおける`app`フォルダーの欠如 {#app-absence} + +NextJSのバージョン13未満では、明示的な`app`フォルダーは存在せず、代わりにNextJSは`_app.tsx`ファイルを提供しています。このファイルは、プロジェクトのすべてのページのラッピングコンポーネントとして機能しています。 + +### `pages/_app.tsx`ファイルへの機能のインポート + +NextJSの構造における`app`フォルダーの欠如の問題を解決するために、`app`層内に`App`コンポーネントを作成し、NextJSがそれを使用できるように`pages/_app.tsx`に`App`コンポーネントをインポートすることができます。例えば + + +```tsx +// app/providers/index.tsx + +const App = ({ Component, pageProps }: AppProps) => { + return ( + + + + + + + + ); +}; + +export default App; +``` +その後、`App`コンポーネントとプロジェクトのグローバルスタイルを`pages/_app.tsx`に次のようにインポートできます。 + +```tsx +// pages/_app.tsx + +import 'app/styles/index.scss' + +export { default } from 'app/providers'; +``` + + +## App Routerの使用 {#app-router} + +App Routerは、Next.jsのバージョン13.4で安定版として登場しました。App Routerを使用すると、`pages`フォルダーの代わりに`app`フォルダーをルーティングに使用できます。 +FSDの原則に従うために、NextJSの`app`フォルダーを`pages`フォルダーとの名前衝突を解消するために推奨される方法で扱うべきです。 + +このアプローチは、NextJSの`app`フォルダーをプロジェクトのルートフォルダーに移動し、FSDのページをNextJSの`app`フォルダーにインポートすることに基づいています。これにより、`src`フォルダー内のFSDプロジェクト構造が保持されます。また、プロジェクトのルートフォルダーに`pages`フォルダーを追加することもお勧めします。なぜなら、App RouterはPages Routerと互換性があるからです。 + +``` +├── app # NextJSのappフォルダー +├── pages # 空のNextJSのpagesフォルダー +│ ├── README.md # このフォルダーの目的に関する説明 +├── src +│ ├── app # FSDのappフォルダー +│ ├── entities +│ ├── features +│ ├── pages # FSDのpagesフォルダー +│ ├── shared +│ ├── widgets +``` + +[![StackBlitzで開く](https://developer.stackblitz.com/img/open_in_stackblitz.svg)][ext-app-router-stackblitz] + +[project-knowledge]: /docs/about/understanding/knowledge-types +[ext-app-router-stackblitz]: https://stackblitz.com/edit/stackblitz-starters-aiez55?file=README.md \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-nuxtjs.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-nuxtjs.mdx new file mode 100644 index 0000000000..a0b7de7e16 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-nuxtjs.mdx @@ -0,0 +1,178 @@ +--- +sidebar_position: 10 +--- +# NuxtJSとの併用 + +NuxtJSプロジェクトでFSDを実装することは可能ですが、NuxtJSのプロジェクト構造要件とFSDの原則の違いにより、以下の2点でコンフリクトが発生してしまいます。 + +- NuxtJSは`src`フォルダーなしでプロジェクトのファイル構造を提供している。つまり、ファイル構造がプロジェクトのルートに配置される。 +- ファイルルーティングは`pages`フォルダーにあるが、FSDではこのフォルダーはフラットなスライス構造に割り当てられている。 + +## `src`ディレクトリのエイリアスを追加する + +設定ファイルに`alias`オブジェクトを追加します。 +```ts +export default defineNuxtConfig({ + devtools: { enabled: true }, // FSDには関係なく、プロジェクト起動時に有効 + alias: { + "@": '../src' + }, +}) +``` +## ルーター設定方法の選択 + +NuxtJSには、コンフィグを使用する方法とファイル構造を使用する方法の2つのルーティング設定方法があります。 +ファイルベースのルーティングの場合、`app/routes`ディレクトリ内に`index.vue`ファイルを作成します。一方、コンフィグを使用する場合は、`router.options.ts`ファイルでルートを設定します。 + +### コンフィグによるルーティング + +`app`層に`router.options.ts`ファイルを作成し、設定オブジェクトをエクスポートします。 +```ts title="app/router.options.ts" +import type { RouterConfig } from '@nuxt/schema'; + +export default { + routes: (_routes) => [], +}; + +``` + +プロジェクトにホームページを追加するには、次の手順を行います。 +- `pages`層内にページスライスを追加する +- `app/router.config.ts`のコンフィグに適切なルートを追加する + +ページスライスを作成するには、[CLI](https://github.com/feature-sliced/cli)を使用します。 + +```shell +fsd pages home +``` + +`home-page.vue`ファイルを`ui`セグメント内に作成し、公開APIを介してアクセスできるようにします。 + +```ts title="src/pages/home/index.ts" +export { default as HomePage } from './ui/home-page'; +``` + +このように、ファイル構造は次のようになります。 +```sh +|── src +│ ├── app +│ │ ├── router.config.ts +│ ├── pages +│ │ ├── home +│ │ │ ├── ui +│ │ │ │ ├── home-page.vue +│ │ │ ├── index.ts +``` +最後に、ルートをコンフィグに追加します。 + +```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) + } + ], +} +``` + +### ファイルルーティング + +まず、プロジェクトのルートに`src`ディレクトリを作成し、その中に`app`層と`pages`層のレイヤー、`app`層内に`routes`フォルダーを作成します。 +このように、ファイル構造は次のようになります。 + +```sh +├── src +│ ├── app +│ │ ├── routes +│ ├── pages # FSDに割り当てられたpagesフォルダー +``` + + +NuxtJSが`app`層内の`routes`フォルダーをファイルルーティングに使用するには、`nuxt.config.ts`を次のように変更します。 +```ts title="nuxt.config.ts" +export default defineNuxtConfig({ + devtools: { enabled: true }, // FSDには関係なく、プロジェクト起動時に有効 + alias: { + "@": '../src' + }, + dir: { + pages: './src/app/routes' + } +}) +``` + + +これで、`app`層内のページに対してルートを作成し、`pages`層からページを接続できます。 + +例えば、プロジェクトに`Home`ページを追加するには、次の手順を行います。 +- `pages`層内にページスライスを追加する +- `app`層内に適切なルートを追加する +- スライスのページとルートを統合する + +ページスライスを作成するには、[CLI](https://github.com/feature-sliced/cli)を使用します。 +```shell +fsd pages home +``` + + +`home-page.vue`ファイルを`ui`セグメント内に作成し、公開APIを介してアクセスできるようにします。  + +```ts title="src/pages/home/index.ts" +export { default as HomePage } from './ui/home-page'; +``` + +このページのルートを`app`層内に作成します。 + +```sh + +├── src +│ ├── app +│ │ ├── routes +│ │ │ ├── index.vue +│ ├── pages +│ │ ├── home +│ │ │ ├── ui +│ │ │ │ ├── home-page.vue +│ │ │ ├── index.ts +``` + +`index.vue`ファイル内にページコンポーネントを追加します。 + +```html title="src/app/routes/index.vue" + + + +``` + +## `layouts`について + +`layouts`は`app`層内に配置できます。そのためには、コンフィグを次のように変更します。 + +```ts title="nuxt.config.ts" +export default defineNuxtConfig({ + devtools: { enabled: true }, // FSDには関係なく、プロジェクト起動時に有効 + alias: { + "@": '../src' + }, + dir: { + pages: './src/app/routes', + layouts: './src/app/layouts' + } +}) +``` + + +## 参照 + +- [NuxtJSのディレクトリ設定変更に関するドキュメント](https://nuxt.com/docs/api/nuxt-config#dir) +- [NuxtJSのルーター設定変更に関するドキュメント](https://nuxt.com/docs/guide/recipes/custom-routing#router-config) +- [NuxtJSのエイリアス設定変更に関するドキュメント](https://nuxt.com/docs/api/nuxt-config#alias) + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-react-query.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-react-query.mdx new file mode 100644 index 0000000000..4eabab8afc --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-react-query.mdx @@ -0,0 +1,434 @@ +--- +sidebar_position: 10 +--- + +# React Queryとの併用 + +## キーをどこに置くか問題 + +### 解決策 - エンティティごとに分割する + +プロジェクトにすでにエンティティの分割があり、各クエリが1つのエンティティに対応している場合、エンティティごとに分割するのが最良です。この場合、次の構造を使用することをお勧めします。 + +```sh +└── src/ # + ├── app/ # + | ... # + ├── pages/ # + | ... # + ├── entities/ # + | ├── {entity}/ # + | ... └── api/ # + | ├── `{entity}.query` # クエリファクトリー、キーと関数が定義されている + | ├── `get-{entity}` # エンティティを取得する関数 + | ├── `create-{entity}` # エンティティを作成する関数 + | ├── `update-{entity}` # オブジェクトを更新する関数 + | ├── `delete-{entity}` # オブジェクトを削除する関数 + | ... # + | # + ├── features/ # + | ... # + ├── widgets/ # + | ... # + └── shared/ # + ... # +``` + +もしエンティティ間に関係がある場合(例えば、「国」のエンティティに「都市」のエンティティ一覧フィールドがある場合)、`@x` アノテーションを使用した組織的なクロスインポートの[実験的アプローチ](https://github.com/feature-sliced/documentation/discussions/390#discussioncomment-5570073)を利用するか、以下の代替案を検討できます。 + +### 代替案 — クエリを公開で保存する + +エンティティごとの分割が適さない場合、次の構造を考慮できます。 + +```sh +└── src/ # + ... # + └── shared/ # + ├── api/ # + ... ├── `queries` # クエリファクトリー + | ├── `document.ts` # + | ├── `background-jobs.ts` # + | ... # + └── index.ts # +``` + +次に、`@/shared/api/index.ts`に + +```ts title="@/shared/api/index.ts" +export { documentQueries } from "./queries/document"; +``` + +## 問題「ミューテーションはどこに?」 + +ミューテーションをクエリと混合することは推奨されません。2つの選択肢が考えられます。 + +### 1. 使用場所の近くにAPIセグメントにカスタムフックを定義する + +```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. 別の場所(Shared層やEntities層)にミューテーション関数を定義し、コンポーネント内で`useMutation`を直接使用する + +```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 + + + ); +}; +``` + +## クエリの組織化 + +### クエリファクトリー + +このガイドでは、クエリファクトリーの使い方について説明します。 + +:::note +クエリファクトリーとは、JSオブジェクトのことで、そのオブジェクトキーの値がクエリキー一覧を返す関数である。 +::: + +```ts +const keyFactory = { + all: () => ["entity"], + lists: () => [...postQueries.all(), "list"], +}; +``` + +:::info +`queryOptions` - react-query@v5に組み込まれたユーティリティ(オプション) + +```ts +queryOptions({ + queryKey, + ...options, +}); +``` + +より高い型安全性と将来のreact-queryのバージョンとの互換性を確保し、クエリの関数やキーへのアクセスを簡素化するために、`@tanstack/react-query`の`queryOptions`関数を使用することができる[(詳細はこちら)](https://tkdodo.eu/blog/the-query-options-api#queryoptions)。 + +::: + + +### 1. クエリファクトリーの作成 + +```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. アプリケーションコードでのクエリファクトリーの適用 + +```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}
+
+ ); +}; +``` + +### クエリファクトリーを使用する利点 +- **クエリの構造化:** ファクトリーはすべてのAPIクエリを1か所に整理し、コードをより読みやすく、保守しやすくしている +- **クエリとキーへの便利なアクセス:** ファクトリーはさまざまなタイプのクエリとそのキーへの便利なメソッドを提供している +- **クエリの再フェッチ機能:** ファクトリーは、アプリケーションのさまざまな部分でクエリキーを変更することなく、簡単に再フェッチを行うことを可能にしている + +## ページネーション + +このセクションでは、ページネーションを使用して投稿エンティティを取得するためのAPIクエリを行う`getPosts`関数の例を挙げます。 + +### 1. `getPosts`関数の作成 + +`getPosts`関数は、APIセグメント内の`get-posts.ts`ファイルにあります。 + +```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. ページネーション用のクエリファクトリー + +`postQueries`クエリファクトリーは、投稿に関するさまざまなクエリオプションを定義し、事前に定義されたページとリミットを使用して投稿一覧を取得するクエリを含みます。 + +```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. アプリケーションコードでの使用 + +```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 +例は簡略化されている。 +::: + +## クエリ管理用の`QueryProvider` + +このガイドでは、`QueryProvider`をどのように構成するべきかを説明します。 + +### 1. `QueryProvider`の作成 + +`query-provider.tsx`ファイルは`@/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. `QueryClient`の作成 + +`QueryClient`はAPIクエリを管理するために使用されるインスタンスです。`query-client.ts`ファイルは`@/shared/api/query-client.ts`にあります。`QueryClient`はクエリキャッシング用の特定の設定で作成されます。 + +```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, + }, + }, +}); +``` + +## コード生成 + +自動コード生成のためのツールが存在しますが、これらは上記のように設定可能なものと比較して柔軟性が低いです。Swaggerファイルが適切に構造化されている場合、これらのツールの1つを使用して`@/shared/api`ディレクトリ内のすべてのコードを生成することができます。 + +## RQの整理に関する追加のアドバイス + +### APIクライアント + +共有層であるshared層でカスタムのAPIクライアントクラスを使用することで、プロジェクト内でのAPI設定やAPI操作を標準化できます。これにより、ログ記録、ヘッダー、およびデータ交換形式(例: JSONやXML)を一元管理することができます。このアプローチにより、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} + +- [The Query Options API](https://tkdodo.eu/blog/the-query-options-api) + diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-sveltekit.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-sveltekit.mdx new file mode 100644 index 0000000000..c7b450db92 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/guides/tech/with-sveltekit.mdx @@ -0,0 +1,96 @@ +--- +sidebar_position: 10 +--- +# SvelteKitとの併用 + +SvelteKitプロジェクトでFSDを実装することは可能ですが、SvelteKitのプロジェクト構造要件とFSDの原則の違いにより、以下の2点でコンフリクトが発生してしまいます。 + +- SvelteKitは`src/routes`フォルダー内でファイル構造を作成することを提案しているが、FSDではルーティングは`app`層の一部である必要がある +- SvelteKitは、ルーティングに関係のないすべてのものを`src/lib`フォルダーに入れることを提案している + +## コンフィグファイルの設定 + +```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', // ルーティングをapp層内に移動 + lib: 'src', + appTemplate: 'src/app/index.html', // アプリケーションのエントリーポイントをapp層内に移動 + assets: 'public' + }, + alias: { + '@/*': 'src/*' // srcディレクトリのエイリアスを作成 + } + } +}; +export default config; +``` + +## `src/app`内へのファイルルーティングの移動 + +`app`層を作成し、アプリケーションのエントリーポイントである`index.html`を移動し、`routes`フォルダーを作成します。 +最終的にファイル構造は次のようになります。 + +```sh +├── src +│ ├── app +│ │ ├── index.html +│ │ ├── routes +│ ├── pages # FSDに割り当てられたpagesフォルダー +``` + +これで、`app`内にページのルートを作成したり、`pages`からのページをルートに接続したりできます。 + +例えば、プロジェクトにホームページを追加するには、次の手順を実行します。 +- `pages`層内にホームページスライスを追加する +- `app`層の`routes`フォルダーに対応するルートを追加する +- スライスのページとルートを統合する + +ホームページスライスを作成するには、[CLI](https://github.com/feature-sliced/cli)を使用します。 + +```shell +fsd pages home +``` + +`ui`セグメント内に`home-page.svelte`ファイルを作成し、公開APIを介してアクセスできるようにします。  + +```ts title="src/pages/home/index.ts" +export { default as HomePage } from './ui/home-page'; +``` + +このページのルートを`app`層内に作成します。 + +```sh + +├── src +│ ├── app +│ │ ├── routes +│ │ │ ├── +page.svelte +│ │ ├── index.html +│ ├── pages +│ │ ├── home +│ │ │ ├── ui +│ │ │ │ ├── home-page.svelte +│ │ │ ├── index.ts +``` + +最後に`index.svelte`ファイル内にページコンポーネントを追加します。 + +```html title="src/app/routes/+page.svelte" + + + + +``` + +## 参照 +- [SvelteKitのディレクトリ設定変更に関するドキュメント](https://kit.svelte.dev/docs/configuration#files) \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/intro.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/intro.mdx new file mode 100644 index 0000000000..738b12efaf --- /dev/null +++ b/i18n/ja/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) とは、フロントエンドアプリケーションの設計方法論です。簡単に言えば、コードを整理するためのルールと規約の集大成です。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/ja/docusaurus-plugin-content-docs/current/reference/index.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/reference/index.mdx new file mode 100644 index 0000000000..86d8f4b2c1 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/reference/index.mdx @@ -0,0 +1,38 @@ +--- +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"; + +# 📚 参考書 + +

+FSDの重要な概念に関するセクション +

+ + + + + \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/_category_.yaml b/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/_category_.yaml new file mode 100644 index 0000000000..b6ec783a3f --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/_category_.yaml @@ -0,0 +1 @@ +label: 分離 diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/coupling-cohesion.md b/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/coupling-cohesion.md new file mode 100644 index 0000000000..7b6be93160 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/coupling-cohesion.md @@ -0,0 +1,147 @@ +--- +sidebar_position: 1 +--- + +# 低結合と高凝集 + +アプリケーションのモジュールは、**強い結合性**(明確なタスクを解決することに焦点を当てる)と**弱い結合性**(他のモジュールからできるだけ依存しない)を持つように設計されるべきです。 + +
+ + +
+ インスパイア先: https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/ +
+
+ +FSDでは、以下の方法で達成されます。 + +* アプリケーションを層とスライスに分割する - 特定の機能を実現するモジュール。 +* 各モジュールには、[公開API][refs-public-api]を提供することが求められます。 +* モジュール間の[相互作用][refs-isolation]に特別な制限を設ける - 各モジュールは「下位」のモジュールにのみ依存でき、同じ層またはそれ以上の層のモジュールには依存できません。 + +## コンポーネントの構成(UIレベル) {#components-composition-ui-level} + +ほとんどの現代のUIフレームワークやライブラリは、各コンポーネントが独自のプロパティ、状態、子コンポーネントを持つことができるコンポーネントモデルを提供しています。 + +このモデルにより、**直接的に関連しないさまざまなコンポーネントの構成**としてインターフェースを構築し、**コンポーネントの弱い結合性**を達成することができます。 + +### 例 {#example} + +**ヘッダー付きリスト**の例を考えてみましょう。 + +#### 拡張性を考慮する {#laying-the-extensibility} + +リストコンポーネントは、ヘッダーとリストアイテムの構造を自ら定義せず、代わりにそれらをパラメータとして受け取ります。 + +```tsx +interface ListProps { + Header: React.ReactNode; + Items: React.ReactNode; +} + +const List: Component = ({ Header, Items }) => ( +
+ {Header} +
    + {Items} +
+
+) +``` + +#### 構成を使用する {#using-the-composition} + +これにより、**異なるバージョンのヘッダーやリストアイテムのコンポーネントを再利用し、独立して変更する**ことが可能になります。ヘッダーとリストアイテムのコンポーネントは、それぞれ独自のローカル状態やアプリケーションの共通状態の任意の部分へのバインディングを持つことができ、リストコンポーネントはそれについて何も知らず、したがってそれに依存しません。 + +```tsx +} Items={} /> + +} /> + +} Items={} /> +``` + +## 層の構成(アプリレベル) {#layer-composition-app-level} + +FSDは、ユーザーにとって価値のある機能を個別のモジュール - **フィーチャー(features)** - に分割し、ビジネスエンティティに関連するロジックを**エンティティ(entities)**に分けることを提案します。フィーチャーとエンティティは、**高い結合性を持つモジュール**として設計されるべきであり、すなわち**特定のタスクを解決することに焦点を当てる**か、**特定のエンティティの周りに集中する**べきです。 + +これらのモジュール間のすべての相互作用は、上記のUIコンポーネントの例と同様に、**さまざまなモジュールの構成**として整理されるべきです。 + +### 例 {#example} + +チャットアプリケーションの例を考えてみましょう。 + +* 連絡先リストを開いて友達を選択できる +* 選択した友達とのチャットを開くことができる + +FSDに基づいて、次のように表現できます。 + +エンティティ + +* ユーザー(ユーザーの状態を含む) +* 連絡先(連絡先リストの状態、特定の連絡先を操作するためのツール) +* チャット(現在のチャットの状態とその操作) + +フィーチャー + +* メッセージ送信フォーム +* チャット選択メニュー + +#### すべてを結びつける {#lets-tie-it-all-together} + +アプリケーションには、最初に1つのページがあり、インターフェースは最初の例の少し修正されたコンポーネントに基づいています。 + +```tsx title="page/main/ui.tsx" +} + Items={} + Footer={} +/> +``` + +#### データモデル {#data-model} + +ページのデータモデルは、**フィーチャーとエンティティの構成**として整理されます。この例では、フィーチャーはファクトリーとして実装され、これらのファクトリーのパラメータを介してエンティティのインターフェースにアクセスします。 + +> ただし、ファクトリーとしての実装は必須ではなく、フィーチャーは下位層に依存し、直接的にアクセスすることもできます。 + +```ts title="pages/main/model.ts" +import { userModel } from "entitites/user" +import { conversationModel } from "entities/conversation" +import { contactModel } from "entities/contact" + +import { createMessageInput } from "features/message-input" +import { createConversationSwitch } from "features/conversation-switch" + +import { beautifiy } from "shared/lib/beautify-text" + +export const { allConversations, setConversation } = createConversationSwitch({ + contacts: contactModel.allContacts, + setConversation: conversationModel.setConversation, + currentConversation: conversationModel.conversation, + currentUser: userModel.currentUser +}) + +export const { sendMessage, attachFile } = createMessageInput({ + author: userModel.currentUser, + send: conversationModel.sendMessage, + formatMessage: beautify +}) +``` + +## まとめ {#summary} + +1. モジュールは**強い結合性**を持つべきであり(1つの責任を持ち、特定のタスクを解決する)、[**公開API**][refs-public-api]を提供する必要があります。 +2. **弱い結合性**は、UIコンポーネント、フィーチャー、エンティティの要素の構成を通じて達成されます。 +3. また、結合性を低下させるために、モジュールは**公開APIを介してのみ互いに依存するべきです** - これにより、モジュールは互いの内部実装から独立します。 + +## 参照 {#see-also} + +* [(記事) 低結合と高凝集についての視覚的説明](https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/) + * *最初の図はこの論文からインスパイアを受けています* +* [(記事) 低結合と高凝集。デメトリの法則](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9) +* [(プレゼンテーション) 設計原則について(低結合と高凝集を含む)](https://www.slideshare.net/cristalngo/software-design-principles-57388843) + +[refs-public-api]: /docs/reference/public-api +[refs-isolation]: /docs/reference/isolation diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/decouple-entities.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/decouple-entities.mdx new file mode 100644 index 0000000000..e4162bdd1e --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/decouple-entities.mdx @@ -0,0 +1,21 @@ +--- +sidebar_position: 2 +sidebar_class_name: sidebar-item--wip +--- + +import WIP from '@site/src/shared/ui/wip/tmpl.mdx' + +# エンティティの分離 + + + +> タイプのクロスインポート、アダプター、エンティティ間の明示的な関係の構築について + +> また、神話的な完全に分離されたエンティティについても + +## 参照 {#see-also} + +- [(スレッド) エンティティの分解と明示的な関係の構築に関するメモ](https://t.me/feature_sliced/3633) +- [(スレッド) 「関連するエンティティ」(ユーザー/ペット/友人)の分解の例](https://t.me/feature_sliced/3316) +- [(スレッド) エンティティにおけるタイプ/アダプターのクロスインポートについて](https://t.me/feature_sliced/4276) +- [(スレッド) エンティティと機能の境界について](https://t.me/feature_sliced/4521) \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/index.md b/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/index.md new file mode 100644 index 0000000000..70de1bb20d --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/reference/isolation/index.md @@ -0,0 +1,63 @@ +# モジュールの分離 + +FSDの枠組みの中で、すべてのモジュールは責任の領域(レイヤー、スライス、セグメント)に分配されています。 + +レイヤーは縦に組織されています。 + +- "下層"には再利用可能なモジュール(ui-kit、プロジェクトの内部ライブラリ)があり、最も抽象的です。 +- "上層"に進むにつれて、より特定的なモジュールが配置されます。 + +どのスライスに属していても、各モジュールは[**公開アクセスインターフェースを提供する義務があります**][refs-public-api]。 + +## 要件 {#requirements} + +各モジュールが他のアプリケーションと相互作用する際には、いくつかの要件を考慮して設計されます。 + +1. **他のモジュールとの弱い結合** + - *1つのモジュールの変更は、他のモジュールに対して弱く予測可能な影響を与えるべきです。* +1. **高い結合性** - 各モジュールの責任は「1つのタスク」に焦点を当てています。 + - *モジュールがあまりにも多くの責任を持つ場合(「やりすぎる」場合)、それはできるだけ早く認識されるべきです。* +1. **アプリケーション全体での循環依存の不在** + - *これはしばしば予期しない望ましくない動作を引き起こすため、完全に避けるべきです。* + +## ルール {#rule} + +これらの要件を満たすために、方法論の枠組みの中で基本的なルールを守る必要があります。 + +:::info 重要 + +モジュールは「下層」のモジュールにのみ依存でき、同じ層またはそれ以上の層のモジュールには依存できません。 + +::: + +- `features/auth` **は** `features/filters` **に依存できません** **し、逆も同様です。** +- `features/auth` **は** `shared/ui/button` **に依存できますが、逆はできません。** + +このルールに従うことで、依存関係を**「一方向」に保つ**ことができ、これにより**循環インポートを自動的に排除し**、モジュール間の依存関係の追跡を大幅に**簡素化**します。 + +## 問題の特定 {#identifying-problems} + + +このルールの違反は問題の信号です。 + +1. モジュールが自層の他のモジュールから**インポートを持つ** + - モジュールが**過剰に細分化されている**か、**余分な責任を持っている**可能性があります。 + - インポートされたモジュールと**統合する**か、**部分的または完全に下層に移動する**か、上層のモジュールに依存関係のロジックを移動する必要があります。 +1. モジュールが自層の多くのモジュールから**インポートされる** + - モジュールが**余分な責任を持っている**可能性があります。 + - **部分的または完全に下層に移動する**か、上層のモジュールに依存関係のロジックを移動する必要があります。 +1. モジュールが自層の多くのモジュールから**インポートを持つ** + - モジュールが**別の責任領域に属している**可能性があります。 + - **部分的または完全に上層に移動する**必要があります。 + +## 参照 {#see-also} + +- [(ガイド) 低い結合性の達成について][refs-low-coupling] +- [(ディスカッション) 方法論におけるエンティティとその結合性](https://github.com/feature-sliced/documentation/discussions/49) +- [(ディスカッション) クロスインポートと依存関係の分析について](https://github.com/feature-sliced/documentation/discussions/65#discussioncomment-480822) +- [パターン **GRASP**](https://ru.wikipedia.org/wiki/GRASP) + +[refs-public-api]: /docs/reference/public-api +[refs-low-coupling]: /docs/reference/isolation/coupling-cohesion \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/reference/layers.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/reference/layers.mdx new file mode 100644 index 0000000000..665317d047 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/reference/layers.mdx @@ -0,0 +1,181 @@ +--- +sidebar_position: 1 +pagination_next: reference/slices-segments +--- + +# レイヤー + +レイヤー(層)は、Feature-Sliced Designにおける組織階層の最初のレベルです。その目的は、コードを必要な責任の量とアプリケーション内の他のモジュールへの依存度に基づいて分割することです。 + +:::note + +このページでのモジュールは、アプリケーション内のファイルやインデックスファイルを持つディレクトリを指している。npmパッケージと混同しないでください。 + +::: + +各レイヤーは、コード内のモジュールにどれだけの責任を割り当てるべきかを判断するのに役立つ特別な意味を持っています。レイヤー名とレイヤーの意味は、Feature-Sliced Designを使用して構築されたすべてのプロジェクトで標準化されています。 + +合計で7つのレイヤーがあり、最も責任と依存度が高いものから最も低いものへと配置されています。 + +ファイルシステムのツリーは、1つのルートフォルダsrcと7つのサブフォルダ(app、processes、pages、widgets、features、entities、shared)で構成されています。processesフォルダは少し薄く表示されています。 +ファイルシステムのツリーは、1つのルートフォルダsrcと7つのサブフォルダ(app、processes、pages、widgets、features、entities、shared)で構成されています。processesフォルダは少し薄く表示されています。 + +1. App (アップ) +2. Processes (プロセス、非推奨) +3. Pages (ページ) +4. Widgets (ウィジェット) +5. Features (フィーチャー) +6. Entities (エンティティ) +7. Shared (シェアード) + +プロジェクトで全てのレイヤーを使用する必要はないので、プロジェクトに価値をもたらすレイヤーだけを追加してください。 + +## レイヤーのインポートルール {#import-rule-on-layers} + +レイヤーは、強く結合されたモジュールのグループであるスライスから構成されています。Feature-Sliced Designは低い結合度をサポートしているため、スライス間の依存関係はレイヤーのインポートルールによって制御されます。 + +> _スライス内のモジュールは、下層のレイヤーにあるスライスのみをインポートできる。_ + +例えば、`~/features/aaa` では、スライスは `aaa` であるため、`~/features/aaa/api/request.ts` ファイルは `~/features/bbb` フォルダー内のモジュールからコードをインポートすることはできませんが、`~/entities` や `~/shared` から、また `~/features/aaa` 内の隣接モジュールからはインポートできます。 + +## レイヤーの定義 + +### Shared層 + +Shared層は、プロジェクトやビジネスの特性から切り離された、孤立したモジュール、コンポーネント、抽象化のことです。 + +このレイヤーは他のレイヤーとは異なり、スライスではなく直接セグメントで構成されています。 + +**内容の例** + +* UIライブラリ +* APIクライアント +* ブラウザAPIと連携するコード + +### Entities層 + +Entities層は、プロジェクトの本質を形成する現実世界の概念です。通常、これはビジネスがプロダクトを説明するために使用する用語です。 + +このレイヤーの各スライスは、ユーザーインターフェースの静的要素、データストレージ、およびCRUD(作成・読み取り・更新・削除)操作を含みます。 + +**スライスの例** + + + +
SNS Gitフロントエンド(例:GitHub)
    +
  • ユーザー
  • +
  • 投稿
  • +
  • グループ
  • +
    +
  • リポジトリ
  • +
  • ファイル
  • +
  • コミット
  • +
+ +:::tip + +Gitフロントエンドの例では、リポジトリファイルを含むことに気付くかもしれません。これは、リポジトリが他のエンティティを内包する高レベルのエンティティであることを示しています。このような高レベルのエンティティを作成ことは一般的が、この場合、レイヤーのインポートルールを破らないことは時に難しいです。 + +この問題を解決するためのいくつかの提案があります。 +* エンティティのUIは、下位レベルのエンティティを挿入するためのスロットを含むべき +* エンティティ間の相互作用に関連するビジネスロジックは、通常、Features層に配置されるべき +* データベースのエンティティインターフェースは、APIクライアントの近くにあるShared層に抽出できる + +::: + +### Features層 + +Features層は、ユーザーがアプリケーション内でビジネスエンティティとインタラクションするために行うアクションで、価値のある結果を達成するためのものです。ここには、ユーザーのためにアプリケーションが実行するアクションも含まれています。 + +このレイヤーのスライスは、価値を生み出すアクションを実行するためのインタラクティブなUI要素、内部状態、およびAPIリクエストを含むことができます。 + +**スライスの例** + + + +
SNS Gitフロントエンド(例:GitHub) ユーザーの代わりに行うアクション
    +
  • ログイン
  • +
  • 投稿を作成
  • +
  • グループに参加
  • +
    +
  • ファイルを変更
  • +
  • コメントを残す
  • +
  • ブランチをマージ
  • +
    +
  • ダークテーマを自動的に有効にする
  • +
  • バックグラウンドで計算を実行する
  • +
  • User-Agentに基づくアクション
  • +
+ +### Widgets層 + +Widgets層は、独立したUIブロックであり、エンティティやフィーチャーなどの低レベルユニットの組み合わせです。 + +このレイヤーは、エンティティのインターフェースに残されたスロットを、他のフィーチャーからのインタラクティブ要素やエンティティで埋めることを可能にしています。したがって、通常、このレイヤーにはビジネスロジックは配置されず、代わりにフィーチャーに保存されます。このレイヤーの各スライスは、使用可能なUIコンポーネントを含み、時にはビジネスロジック以外のロジック(例えば、ジェスチャー、キーボードとの相互作用など)を含むことがあります。 + +時には、このレイヤーにビジネスロジックを配置する方が便利な場合もあります。これは、ウィジェットがかなりのインタラクティブ性を持っている場合(例えば、インタラクティブテーブル)で、その中のビジネスロジックが再利用されない場合によく発生します。 + +**スライスの例** + + + +
SNS Gitフロントエンド(例:GitHub)
    +
  • 投稿カード
  • +
  • ユーザープロフィールのヘッダー(アクション付き)
  • +
    +
  • リポジトリ内のファイル一覧(アクション付き)
  • +
  • スレッド内のコメント
  • +
  • リポジトリカード
  • +
+ +:::tip + +ネストされたルーティングシステム(例えば、[Remix][ext--remix]ルーター)を使用している場合、Widgets層をPages層と同様に使用することが便利なケースがあります。例えば、バックエンドからのデータを取得し、読み込み状態やエラー処理を含むインターフェースブロックを作成する際に役立てます。また、Pages層のレイアウトをここに配置することもできます。 +::: + +### Pages層 + +Pages層は、ページベースのアプリケーション(例えば、ウェブサイト)のページや、画面ベースのアプリケーション(例えば、モバイルアプリ)の画面やアクティビティです。 + +このレイヤーは、その構成的な性質においてWidgets層に似ていますが、より大きな規模で構成されています。Pages層の各スライスは、ルーターに接続できるUIコンポーネントを含むほか、データ取得やエラー処理のロジックを含むこともできます。 + +**スライスの例** + + + +
SNS Gitフロントエンド(例:GitHub)
    +
  • ニュースフィード
  • +
  • コミュニティページ
  • +
  • 公開ユーザープロフィール
  • +
    +
  • リポジトリページ
  • +
  • ユーザーのリポジトリ
  • +
  • リポジトリ内のブランチ
  • +
+ +### Processes層 + +:::caution + +このレイヤーは、廃止されています。現在の仕様バージョンでは、これを避け、内容を `features`層 と `app`層 に移動することを推奨しています。 + +::: + +Processes層は、複雑なマルチページインタラクションが必要な際に使用されます。 + +このレイヤーは意図的にあまり定義されていません。ほとんどのアプリケーションにはこのレイヤーは必要ありません。ルーターやサーバーレベルのロジックはApp層に残すべきです。このレイヤーは、App層が成長しすぎて管理できなくなった場合にのみ使用を検討してください。 + +### App層 + +App層は、アプリ全体に関するすべてのことです。技術的な意味(例えば、コンテキストプロバイダー)とビジネス的な意味(例えば、分析)を含みます。 + +このレイヤーは通常、Shared層と同様にスライスを含まず、直接セグメントで構成されています。 + +**内容の例** + +* スタイル +* ルーター +* データストレージやその他のコンテキストプロバイダー +* 分析の初期化 + +[ext--remix]: https://remix.run \ No newline at end of file diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/reference/public-api.md b/i18n/ja/docusaurus-plugin-content-docs/current/reference/public-api.md new file mode 100644 index 0000000000..8b07850319 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/reference/public-api.md @@ -0,0 +1,216 @@ +--- +sidebar_position: 3 +sidebar_label: 公開API +pagination_next: about/index +--- + +# 公開API + +FSDでの各エンティティは、**使いやすく統合しやすいモジュール**として設計されています。 + +## 目的 {#goals} + +モジュールの使いやすさと統合しやすさは、*いくつかの目的*を達成することによって実現されます。 + +1. アプリケーションは、個々モジュール内部構造の**変更から保護されるべき** +1. モジュール内部構造の再設計は、**他のモジュールに影響を与えてはならない** +1. モジュールの動作の重要な変更は、**簡単に特定できるべき** + > **モジュールの動作に関する重要な変更**とは、モジュール利用者の期待を壊す変更 + +これらの目的を達成するために、公開API(公開インターフェース)が導入され、モジュール機能への単一アクセス点を提供し、モジュールと外部世界との相互作用の「契約」を定義します。 + +:::info 重要 + +エンティティの構造は、公開APIを提供する単一のエントリーポイントを持つべき + +::: + +```sh +└── features/                        #  + └── auth-form/                  # フィーチャーの内部構造 + ├── ui/                    # + ├── model/                 # + ├── {...}/                 # + └── index.ts               # フィーチャーの公開APIを持つエントリーポイント +``` + +```ts title="**/**/index.ts" +export { Form as AuthForm } from "./ui" +export * as authFormModel from "./model" +``` + +## 公開APIの要件 {#requirements-for-the-public-api} + +下記の要件を満たすことで、モジュールとの相互作用を**公開API契約の実行**に制限し、モジュールの信頼性と使いやすさを達成できます。 + +### 1. アクセス制御 {#1-access-control} + +公開APIは、モジュール内容への**アクセス制御**を行うべきです。 + +- アプリケーションの他の部分は、**公開APIで提供されるモジュールのエンティティのみを使用できる** +- 公開APIのないモジュールの内部部分は、**モジュール自身のみがアクセスできる** + +#### 例 {#examples} + +##### プライベートインポートからの排除 {#suspension-from-private-imports} + +- **悪い例**: 公開APIをバイパスしてモジュールの内部部分に直接アクセスすることは危険であり、特にモジュールのリファクタリング時に問題を引き起こす可能性がある。 + + ```diff + - import { Form } from "features/auth-form/components/view/form" + ``` + +- **良い例**: APIは事前に必要なものだけをエクスポートし、モジュールの開発者はリファクタリング時に公開APIを壊さないことだけを考えればよい。 + + ```diff + + import { AuthForm } from "features/auth-form" + ``` + +### 2. 変更への耐性 {#2-sustainability-for-changes} + +公開APIは、モジュール内部の**変更に対して耐性があるべきです**。 + +- モジュールの動作を壊す変更は、公開APIの変更として反映されるべき + +#### 例 {#examples} + +##### 実装からの抽象化 {#abstracting-from-the-implementation} + +内部構造の変更は、公開APIの変更を引き起こすべきではありません。 + +- **悪い例**: このコンポーネントをフィーチャー内で移動、または名前変更すると、すべての使用場所でインポートをリファクタリングする必要が生じる。 + + ```diff + - import { Form } from "features/auth-form/ui/form" + ``` + +- **良い例**: フィーチャーのインターフェースは内部構造を反映せず、外部の「ユーザー」はフィーチャー内のコンポーネントの移動や名前変更の影響を受けない。 + + ```diff + + import { AuthForm } from "features/auth-form" + ``` + +### 3. 統合性 {#3-integrability} + +公開APIは、**簡単で柔軟な統合を促進するべきです**。 + +- 公開APIは、アプリケーションの他の部分での使用が便利であり、特に名前衝突問題を解決する必要がある。 + +#### 例 {#examples} + +##### 名前の衝突 {#name-collision} + +- **悪い例**: 名前の衝突が発生してしまう。 + + ```ts title="features/auth-form/index.ts" + export { Form } from "./ui" + export * as model from "./model" + ``` + + ```ts title="features/post-form/index.ts" + export { Form } from "./ui" + export * as model from "./model" + ``` + + ```diff + - import { Form, model } from "features/auth-form" + - import { Form, model } from "features/post-form" + ``` + +- **良い例**: インターフェースレベルで名前の衝突が解決される。 + + ```ts title="features/auth-form/index.ts" + export { Form as AuthForm } from "./ui" + export * as authFormModel from "./model" + ``` + + ```ts title="features/post-form/index.ts" + export { Form as PostForm } from "./ui" + export * as postFormModel from "./model" + ``` + + ```diff + + import { AuthForm, authFormModel } from "features/auth-form" + + import { PostForm, postFormModel } from "features/post-form" + ``` + +##### 柔軟な使用 {#flexible-use} + +- **悪い例**: 書きにくく、読みづらく、「ユーザー」は不便を感じます。 + + ```diff + - import { storeActionUpdateUserDetails } from "features/auth-form" + - dispatch(storeActionUpdateUserDetails(...)) + ``` + +- **良い例**: 「ユーザー」は必要なものに対して反復的かつ柔軟にアクセスできます。 + + ```diff + + import { authFormModel } from "features/auth-form" + + dispatch(authFormModel.effects.updateUserDetails(...)) // redux + + authFormModel.updateUserDetailsFx(...) // effector + ``` + +##### 衝突の解決 {#resolution-of-collisions} + +名前の衝突は、実装レベルではなく公開APIのレベルで解決されるべきです。 + +- **悪い例**: 名前の衝突が実装レベルで解決される。 + + ```ts title="features/auth-form/index.ts" + export { AuthForm } from "./ui" + export { authFormActions, authFormReducer } from "model" + ``` + + ```ts title="features/post-form/index.ts" + export { PostForm } from "./ui" + export { postFormActions, postFormReducer } from "model" + ``` + +- **良い例**: 名前の衝突がインターフェースレベルで解決される。 + + ```ts title="features/auth-form/model.ts" + export { actions, reducer } + ``` + + ```ts title="features/auth-form/index.ts" + export { Form as AuthForm } from "./ui" + export * as authFormModel from "./model" + ``` + + ```ts title="features/post-form/model.ts" + export { actions, reducer } + ``` + + ```ts title="features/post-form/index.ts" + export { Form as PostForm } from "./ui" + export * as postFormModel from "./model" + ``` + +## 再エクスポートについて {#about-re-exports} + +JavaScriptでは、モジュールの公開APIは、モジュール内部のエンティティを`index`ファイルで再エクスポートすることによって作成されます。 + +```ts title="**/**/index.ts" +export { Form as AuthForm } from "./ui" +export * as authModel from "./model" +``` + +### 欠点 {#disadvantages} + +- ほとんどの人気のバンドラーでは、再エクスポートのために**コード分割の効果が低下してしまいます**。なぜなら、このアプローチでは[ツリーシェイキング](https://webpack.js.org/guides/tree-shaking/)が安全にモジュール全体しか削除することができないからです。 + > 例えば、ページモデルで`authModel`をインポートすると、たとえ使用されていなくても、`AuthForm`コンポーネントがそのページのチャンクに含まれてしまいます。 + +- 結果として、チャンクの初期化が高コストになり、ブラウザはその中のすべてのモジュールを処理する必要があります。 + +### 可能な解決策 {#possible-solutions} + +- `webpack`は、再エクスポートファイルを[**副作用なし**](https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free)としてマークすることを可能にしています。これにより、`webpack`はそのファイルを扱う際に攻撃的な最適化を使用できるようになります。 + +## 参照 {#see-also} + +- [**SOLID**原則][ext-solid] +- [**GRASP**パターン][ext-grasp] + +[ext-solid]: https://ja.wikipedia.org/wiki/SOLID +[ext-grasp]: https://ja.wikipedia.org/wiki/GRASP diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/reference/slices-segments.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/reference/slices-segments.mdx new file mode 100644 index 0000000000..a3f29279a5 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-docs/current/reference/slices-segments.mdx @@ -0,0 +1,57 @@ +--- +title: スライスとセグメント +sidebar_position: 2 +pagination_next: reference/public-api +--- + +# スライスとセグメント + +## スライス + +スライスは、Feature-Sliced Designの組織階層における第2のレベルです。その主な目的は、プロダクト、ビジネス、または単にアプリケーションに対する意味に基づいてコードをグループ化することです。 + +スライスの名前は標準化されておらず、アプリケーションのビジネス領域によって直接決定されます。例えば、フォトギャラリーには`photo`、`create-album`、`gallery-page`というスライスがあるかもしれません。SNSには、`post`、`add-user-to-friends`、`news-feed`のようなスライスが必要になるでしょう。 + +密接に関連するスライスは、同じフォルダーに構造的にグループ化できますが、他のスライスと同じ隔離ルールを遵守する必要があります。この場合、ディレクトリには、**複数のスライスによって共有されるコードは存在してはなりません**。 + +![「compose」(作成)、「like」(いいね)、「delete」(削除)の機能が「post」(投稿)フォルダーにグループ化されています。このフォルダーには「some-shared-code.ts」というファイルもあり、その名前は取り消し線が引かれており、そこに存在してはいけないことを示しています。](/img/graphic-nested-slices.svg) + +Shared層とApp層にはスライスが含まれていません。それは、Shared層がビジネスロジックを含むべきではないため、プロダクトに対して意味を持っていないからです。また、App層はアプリケーション全体に関係するコードのみを含むべきであるため、分割の必要がないからです。 + +### スライスの公開APIルール + +スライス内では、コードは自由に整理でき、スライスが質の高い公開APIを提供している限り、それに問題がありません。これが**スライスの公開APIルール**の本質です。 + +> _すべてのスライス(およびスライスを持たないレイヤーのセグメント)は、公開APIの定義を含む必要がある_ +> +> _スライス/セグメントの外部にあるモジュールは、スライス/セグメントの内部ファイル構造ではなく、公開APIのみを参照すべき_ + +公開APIの要件とその作成に関するベストプラクティスについては、[公開APIガイド][ref--public-api]を参照してください。 + +## セグメント + +セグメントは、組織階層の第3レベルおよび最終レベルであり、その目的は技術的な性質に基づいてコードをグループ化することです。 + +いくつかの標準化されたセグメント名があります。 +* `ui` - UIコンポーネント、データフォーマット関数 +* `model` - ビジネスロジックとデータストレージ、これらのデータを操作するための関数 +* `lib` - 補助的なインフラストラクチャコード +* `api` - 外部APIとの通信、バックエンドAPIメソッド + +他のセグメントも許可されていますが、必要に応じてのみ作成されるべきです。カスタムセグメントの最も一般的な場所は、スライスが意味を持たないApp層とShared層です。 + +### 例 + +| レイヤー | `ui` | `model` | `lib` | `api` | +| :----------- | :----------- | :----------- | :----------- | :----------- | +| Shared層 | UIライブラリ | 通常は使用されない | 複数の関連ファイルからのユーティリティモジュール。
個別のヘルパー関数が必要な場合は、[`lodash-es`][ext--lodash]などのユーティリティライブラリを検討してください。 | 認証やキャッシュなどの追加機能を持つシンプルななAPIクライアント。 | +| Entities層 | インタラクティブ要素のスロットを持つビジネスエンティティの骨組み | エンティティのインスタンスのデータストレージと、それらのデータを操作するための関数。
サーバーからのデータを保存するのに最適。[TanStack Query][ext--tanstack-query]や他の暗黙的なストレージメソッドを使用する場合、省略できる。 | データストレージに関連しないエンティティのインスタンスを操作するための関数 | Shared層からのAPIクライアントを使用してバックエンドとの通信を簡素化するAPIメソッド | +| Features層 | ユーザーが機能を使用できるためのインタラクティブ要素 | 必要に応じてビジネスロジックとインフラストラクチャデータストレージ(例:アプリケーションの現在のテーマ)。ユーザーに価値を提供するコードがここにある | `model`セグメントのビジネスロジックを簡潔に説明するためのインフラストラクチャコード | バックエンドで機能を表すAPIメソッド。
Entities層からのAPIメソッドを組み合わせることがある。 | +| Widgets層 | Entities層とFeatures層を自己完結型のUIブロックに構成する。
エラーバウンダリやローディング状態を含むこともできる。 | 必要に応じてインフラストラクチャデータストレージを含むことができる | ページ上でウィジェットブロックが機能するために必要な非ビジネスインタラクション(例:ジェスチャー)やその他のコード | 通常は使用されないが、ネストされたルーティングの文脈でデータローダーを含むことがある(例: [Remix][ext--remix]) | +| Pages層 | Entities層、Features層、Widgets層の構成。
エラーバウンダリやローディング状態を含むこともできる。 | 通常は使用されない | UXを提供するために必要な非ビジネスインタラクション(例:ジェスチャー)やその他のコード | SSR(サーバーサイドレンダリング)指向のフレームワーク用のデータローダー | + +[ref--public-api]: /docs/reference/public-api + +[ext--lodash]: https://www.npmjs.com/package/lodash-es +[ext--tanstack-query]: https://tanstack.com/query/latest +[ext--remix]: https://remix.run diff --git a/i18n/ja/docusaurus-theme-classic/footer.json b/i18n/ja/docusaurus-theme-classic/footer.json new file mode 100644 index 0000000000..3fe117791d --- /dev/null +++ b/i18n/ja/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=Документация linking to /docs" + }, + "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.Discussions": { + "message": "ディスカッション", + "description": "The label of footer link with label=Обсуждения linking to https://github.com/feature-sliced/documentation/discussions" + }, + "link.item.label.License": { + "message": "ライセンス", + "description": "The label of footer link with label=License linking to LICENSE" + }, + "link.item.label.Contribution Guide (RU)": { + "message": "コントリビューションガイド (RU)", + "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": "X", + "description": "The label of footer link with label=X linking to https://x.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": "Copyright © 2018-2024 Feature-Sliced Design", + "description": "The footer copyright" + } +} diff --git a/i18n/ja/docusaurus-theme-classic/navbar.json b/i18n/ja/docusaurus-theme-classic/navbar.json new file mode 100644 index 0000000000..344f9415ab --- /dev/null +++ b/i18n/ja/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": "🍰 FSD設計方法論について", + "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 76c6fe73c5..3765108237 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start:en": "docusaurus start --locale en", "start:uz": "docusaurus start --locale uz", "start:kr": "docusaurus start --locale kr", + "start:ja": "docusaurus start --locale ja", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "test": "pnpm run test:lint && pnpm run build",