Mintnoii

返回

认识 Feature Sliced Design

认识 Feature Sliced Design

什么是 FSD?#

FSD Feature-Sliced Design 功能切片设计 是一种用于构建前端应用程序的架构方法。

可以将其理解为前端项目结构、代码约定和规则的规范。

它的目的是让项目在应对不断变化的业务需求时,更加易于理解和组织,便于扩展和维护。

FSD 是否适合你的项目?

  • 这是只适用于前端项目的架构方法论,不适用与前后端同构项目
  • 只适用于面向用户的应用程序,而不适用与库或者UI工具包
  • 小项目不需要 FSD,会额外提高项目的复杂性
  • 即使是特别复杂的项目,也依然可以应用 FSD 的架构模式

设计概念#

在 FSD 架构中,一个项目由层 Layers 组成,每层由切片 Slices 组成,每个切片由段 Segments 组成。

fsd

Layers 层#

Layers 是顶层目录,标准化的 FSD 一共有六层

  • app 应用程序层 使应用程序运行的一切,比如路由 routers、providers、全局样式 styles、国际化、插件 plugins、全局类型声明和存储设置 store等,是应用程序的入口。应用程序层管理影响整个应用程序的功能。 该层基本上没有切片,并且由直接段组成。

  • pages 页面层 页面层管理应用程序嵌套路由中的各个页面或页面的大部分。 将 entities(实体)、features(特征) 和 widgets(小部件) 组合起来的东西提供给 app 使用,属于组合层。

    页面被开发为具有设计规范和功能的独立单元,可能包含数据检索逻辑和错误处理。

    在 FSD 中,pages 之间不可以互相引用,一个页面不能从另外一个页面目录中导入代码

  • widgets 部件层

    小部件组合层,并非 UI Compoennts,将 entities 和 features 组合成有意义的业务模块,例如 用户信息卡片、问题列表、用户设置等没有业务交互的模块。

    当想要准备可重用组件时使用,但如果不打算重用它,我认为将其写在页面中是可以的。

  • features 特征(功能)层

    该层是与用户有交互的模块,主要都是可以为用户带来价值的行为,例如发送评论、添加购物车、用户搜索等。

    将应用程序的主要功能组织为一个独立的单元,并通过将其视为独立的功能组件来最大程度地减少与其他部分的耦合非常重要。这提高了可维护性和可扩展性。

  • entities 业务实体层

    项目使用的业务实体,例如 user 或 product ,实体层定义应用程序的业务逻辑和数据模型,并管理可重用的业务逻辑和组件。 该层的每个 slice 都包含静态 UI、数据存储和基于实体的 CRUD 操作,例如用户、产品、订单等。也是可选层。

    有时很难在不违反层导入规则的情况下管理此类更高级别的实体 比如文件夹包含文件。这使得文件夹成为一个更高级别的实体,其中嵌套了其他实体。

    这也算是比较常见的情况,以下是解决此问题的一些建议:

    • 实体的 UI 应包含用于插入较低级别实体的位置的插槽
    • 与实体交互相关的业务逻辑应该放在features中(大多数时候)
    • 数据库实体的类型可以提取到下面 API 客户端旁边的共享层
  • shared 共享层

    共享层中是与项目和业务的具体细节无关,不依赖于特定的业务逻辑,可重复使用的功能。

    例如 UI 组件、工具库、类型、API 等 。 shared 该层与其他层的不同之处在于:它没有切片并且由直接段组成。

    Segment 也不是固定不变的,可以根据项目需求增加或者减少。但需要注意:定义 Segemnt 时,其名称应该描述 为什么 而不是 是什么 ,components 、hooks 和 models 这样的名称尽量不要使用

任意一层上的模块只能与严格位于其下层的模块交互。层级由高到低的顺序如图所示: layers

另外注意:

  • App 和 Shared 与其他层不同,没有切片,而是直接由段组成。
  • 标准化的 FSD 一共有六层。但 Layers 并非是固定不变的,对于不复杂的项目或者极其复杂的项目来说,可以根据项目自身需求动态的减少或者增加 Layer。(entites、features、widgets 为可选层)

Slices 切片#

切片是 FSD 组织层次结构中的第二级。主要目的是根据产品、业务或应用程序的含义对代码进行分组。切片是有意义的分离的层次结构,其命名直接由业务领域决定。

切片层次结构取决于应用程序,并且由于 FSD 没有特别标准化,因此必须针对每个项目确定它。主要目的是按照业务领域,将逻辑相关的模块放在一起,让代码更容易导航。

**同一层上的切片不能相互依赖或引用,这有助于高内聚和低耦合。**所以图中的情况是不允许的,如果你有想要共享的资源,则需要检查结构或使用 shared 共享。

not_allowed

其他需要注意的事项:

  • 在大多数情况下,应该避免在 Slices 中嵌套,并且只使用文件夹进行结构分组
  • 相关的切片也可以分组在同一目录中,以便于使用。例如,如果“认证”和“用户信息管理”等功能相关,则可以将它们组织到名为“User”的目录中。
  • app 应用程序层和 shared 共享层不包含切片。因为 app 应该只包含与整个应用程序相关的代码,因此无需将其拆分。 shared 不应该包含任何业务逻辑,所以它不需要有切片。 一个较为完整的 Slices 示例:

demo

Segment 片段#

段是组织层次结构中的第三层也是最后一层,其目的是根据代码的技术性质对代码进行分组。帮助切片内的代码分离。最常见的有 ui/ model/api/lib 4 个标准化段:

  • ui:UI 相关组件
  • model:业务逻辑、数据存储和操作该数据的函数
  • lib: 辅助和基础设施代码
  • api: 与外部 API 通信部分,后端 API 方法

尽管不禁止创建自己的片段,但是一定要仔细考虑并慎重创建,基本上,它用于不存在切片的 app 层或 shared 层。

  • config:配置
  • types:类型目录 segment

公共 API (统一导出索引)#

FSD 有一种 Public API 的接口设计方法,通过提供可从外部层访问的入口点并将模块封装在 Slice 下,可以仅公开必要的功能,同时隐藏内部结构。

每个 slices、 segments 都应该有一个公共 API,一般情况下为当前目录下的 index 文件。

详情查看:https://feature-sliced.design/docs/get-started/tutorial#define-a-strict-public-api ↗

公共 API 是索引文件中公共模块聚合的地方。您可以从切片和段中仅提取必要的函数,并分离不需要的函数。此外,对于外部文件,该索引文件充当入口点。

└── features/ # 
 ├── auth-form / # feature 的内部结构
 | ├── ui/ #
 | ├── model/ #
 | ├── {…}/ #
 | ├── index.ts # 入口点及其公共 API
plaintext
  • 异常模式一 外部接口直接访问模块内部的模式。 改进的好处:通过仅导入允许的内容,您可以一目了然地确定正在访问哪些模块。
// features/todo-create-form/index.ts
export { TodoCreateForm } from "./ui/TodoCreateForm"
ts
- import { TodoCreateForm } from "@/features/todo-create-form/ui/TodoCreateForm"
+ import { TodoCreateForm } from "@/features/todo-create-form"
typescript
  • 异常模式二 当模块的内部目录结构发生变化时,会影响从外部可见的公共 API 的情况。 改进好处:功能的内部结构不暴露给外部用户,移动或重命名组件不会影响外部用户。
- import { Form } from "features/todo-create-form/ui/form"
+ import { TodoCreateForm } from "features/todo-create-form"
jsx
  • 异常模式三 导致名称冲突的情况
// /features/todo-create-form/index.ts
- export { Form } from "./ui/TodoCreateForm"
+ export { Form as TodoCreateForm  } from "./ui/TodoCreateForm"
jsx
// /features/todo-edit-form/index.ts
- export { Form } from "./ui/TodoCreateForm"
+ export { Form as TodoEditForm } from "./ui/TodoCreateForm"
jsx

由于“公共 API”作为索引文件是该模块的入口点,因此应该遵循以下规则:

  • 应用程序的其他部分,只能使用在公共 API 中定义的,对外暴露的模块实体
  • 未在公共 API 中定义的 slice 或 segment 应该被视为隔离的,只能由模块本身访问

声明良好的公共接口应该方便应用程序的其余部分使用,并且可以适应模块内部的更改。

公共 API 简化了导入和导出的工作,在对项目进行修改时,可以方便的统一修改,而不需要更改代码中各处的导入。

索引文件的问题

FSD 主要解决的问题#

FSD 的主要目的在于实现高内聚和低耦合 , 它根据代码的作用以及对项目的贡献来决定它们的位置。

FSD 通过约定的方式,在文件结构层面就已经实现了 多态 、封装 、继承 和 抽象 等概念,确保了代码的隔离、可重用性和多功能性。

在 FSD 中,较低的层一般是属于比较抽象的,它们可以在较高的层中重复、多次的使用,这样就实现了 抽象 和多态 因为高层可以重复使用低层,所以也实现了 继承。

通过公共 API index,统一模块对外的入口,限制对 Slice 和 Segment 内部的访问,实现了 封装

优点#

目录管理清晰、耦合度适中

通过 FSD,已经有了分层、切片、分段的标准化管理方法。代码按影响范围(层)、领域(切片)和技术目的(段)进行组织。此外,层之间的依赖关系是单向的,并且切片始终是松散耦合的。

这创建了一个易于每个人理解的标准化架构。我个人觉得它融合了 layered architecture、package by feature 两者的思想,很容易想象,也有一种统一感

业务和用户需求导向

当应用程序被分割成业务领域时,通过浏览代码就可以发现和深入的了解项目的所有功能

功能变化和重构时的稳定性

由于层与层之间的依赖关系以及公共 API 的思想,每个组件模块都有其目的和可预测的依赖关系。该设计允许我们可以安心地进行更改

拥有支持开发的工具

拥有相关的支持工具来加速开发,例如 Lint 和 IDE 插件和扩展

可以使用 feature-sliced/eslint-config ↗ 自动检查层之间的依赖关系和公共 API 规则

缺点#

  • 与常规架构相比,需要转变思维去适应
  • 有一定的上手成本,需要团队统一规范和遵守
  • 文档中有很多空白页,文档的结构有时很难找到想要的信息
  • 不太适合 MVP 项目或者小的项目,需要付出额外的精力

写在最后,没有银弹

我认为前端架构没有正确的答案,Feature-Sliced 架构也并非适用于所有的前端项目

尤其当前大多数框架、库都提供了自己的官方标准模板,例如 NextJS 和 Sveltekit 等更是对目录结构的要求有一些限制。 同时,FSD 架构的应用还需要考虑到项目、团队等诸多因素

不过,FSD 也许并不是最适合你的项目架构,但是它的概念以及一些规范,也非常适合去学习和借鉴并应用到自己的项目中。

参考#