Open WangShuXian6 opened 3 months ago
./data
./domain
./domain/entities
./domain/repositories
./domain/use-cases
./domain/use-cases/app
./domain/use-cases/todo
./domain/use-cases/todo-list
./domain/utilities
./domain/utilities/logger
./presentation
./presentation/app
./presentation/app/error
./presentation/app/list
./presentation/app/new-list-popup
./presentation/app/new-todo-popup
./presentation/app/todo
./data
./data/todo-list-local-storage.ts
./domain
./domain/entities
./domain/entities/index.ts
./domain/entities/todo-list.ts
./domain/entities/todo.ts
./domain/repositories
./domain/repositories/todo-list.ts
./domain/use-cases
./domain/use-cases/app
./domain/use-cases/app/get-todo-lists.spec.ts
./domain/use-cases/app/get-todo-lists.ts
./domain/use-cases/app/index.ts
./domain/use-cases/app/save-todo-lists.ts
./domain/use-cases/index.ts
./domain/use-cases/todo
./domain/use-cases/todo/create-todo.ts
./domain/use-cases/todo/index.ts
./domain/use-cases/todo/update-completed-status.ts
./domain/use-cases/todo-list
./domain/use-cases/todo-list/add-todo.ts
./domain/use-cases/todo-list/create-todo-list.ts
./domain/use-cases/todo-list/delete-todo-list.ts
./domain/use-cases/todo-list/delete-todo.ts
./domain/use-cases/todo-list/index.ts
./domain/use-cases/use-case.interface.ts
./domain/utilities
./domain/utilities/get-data-store.ts
./domain/utilities/instrument.decorator.ts
./domain/utilities/is-todo.guard.ts
./domain/utilities/logger
./domain/utilities/logger/index.ts
./domain/utilities/logger/log-types.ts
./domain/utilities/logger/logger.ts
./domain/utilities/storage-error.ts
./presentation
./presentation/app
./presentation/app/app.component.html
./presentation/app/app.component.scss
./presentation/app/app.component.spec.ts
./presentation/app/app.component.ts
./presentation/app/app.config.ts
./presentation/app/app.routes.ts
./presentation/app/error
./presentation/app/error/error.component.html
./presentation/app/error/error.component.scss
./presentation/app/error/error.component.spec.ts
./presentation/app/error/error.component.ts
./presentation/app/list
./presentation/app/list/list.component.html
./presentation/app/list/list.component.scss
./presentation/app/list/list.component.spec.ts
./presentation/app/list/list.component.ts
./presentation/app/new-list-popup
./presentation/app/new-list-popup/new-list-popup.component.html
./presentation/app/new-list-popup/new-list-popup.component.scss
./presentation/app/new-list-popup/new-list-popup.component.spec.ts
./presentation/app/new-list-popup/new-list-popup.component.ts
./presentation/app/new-list-popup/new-list-popup.service.ts
./presentation/app/new-todo-popup
./presentation/app/new-todo-popup/new-todo-popup.component.html
./presentation/app/new-todo-popup/new-todo-popup.component.scss
./presentation/app/new-todo-popup/new-todo-popup.component.spec.ts
./presentation/app/new-todo-popup/new-todo-popup.component.ts
./presentation/app/themes.enum.ts
./presentation/app/todo
./presentation/app/todo/todo.component.html
./presentation/app/todo/todo.component.scss
./presentation/app/todo/todo.component.spec.ts
./presentation/app/todo/todo.component.ts
./presentation/favicon.ico
./presentation/index.html
./presentation/main.ts
./presentation/styles.scss
当然,对于 TypeScript 应用程序没有一种放之四海而皆准的文件夹结构。你使用的结构取决于项目的性质、使用的框架以及个人偏好等各种因素。但有一个原则可以指导我们创建新的项目,那就是 LIFT 原则。
LIFT 是一个首字母缩略词,代表:可定位 (Locatable)、可识别 (Identifiable)、扁平化 (Flat)、尽量保持 DRY (Don’t Repeat Yourself)。可定位意味着你的代码应该易于查找。显然,“易于”是一个相对术语,任何代码库都可能足够大,以至于需要一些时间来熟悉,但总体而言,你的文件和文件夹应该命名和组织得有助于查找。与此相关的是,可识别性。应该容易识别哪些文件包含哪些代码,哪些文件夹包含哪些文件。扁平化意味着你应该尽量保持文件夹结构的扁平化,只包含严格必要的子层级。最后,DRY 本身也是一个缩略词,代表不要重复自己。因此,在最后添加“尽量保持 DRY”,我们提醒自己尽量避免重复。就项目结构而言,这实际上意味着你应该避免在多个位置有相同或相似的文件副本。这不仅适用于代码,也适用于项目结构,但即使在项目开始时也是需要牢记的事情。
在项目根目录,我们有顶级配置文件。这是一个 Angular 应用程序,所以我们有 Angular 的配置文件。应用程序使用 NPM 进行依赖管理,因此我们有 node_modules 文件夹和一些与 node 相关的配置文件。它也是一个 TypeScript 应用程序,所以我们在这里也有基础的 TypeScript 配置文件。按照惯例,我们有一个 SRC 文件夹,其中包含应用程序的源代码。这个文件夹包含三个子文件夹,分别是 data、domain 和 presentation。这些文件夹代表了我们应用程序架构中的不同层次。数据层关注于获取和保存应用程序使用的数据。领域层关注于驱动应用程序的业务规则。表示层关注于应用程序的用户界面。
数据文件夹中,有一个名为 todo-list-local-storage 的文件。所以即使不打开文件阅读其中的代码,我们也可以高度自信地推断出这个文件包含与在本地存储中获取和存储数据相关的功能。你可以看到这如何与我们的指导原则 LIFT 的扁平化和可识别性方面相关。
领域文件夹进一步细分为四个区域:entities、repositories、use cases 和 utilities。这些名称对应于应用程序架构中的不同区域,我们将在下一段详细讨论这些架构。此处我们更关注布局本身。entities、repositories 和 utilities 文件夹仅包含一些零散的文件,但 use cases 文件夹包含更多文件,因此这些文件被进一步组织到不同的区域。用例表示应用程序的功能,即应用程序能够执行的所有操作。我们这里有一个名为 app 的文件夹,用于存放一般的应用程序级文件,我们还在这里有一些与获取和保存待办事项列表相关的文件。todo 文件夹包含与创建待办事项和更新待办事项完成状态相关的文件。待办事项列表文件夹包含与待办事项列表执行的操作相关的文件,例如添加和删除待办事项以及创建或删除列表。同样,所有这些文件都被专门命名,以便我们可以轻松识别哪些文件与应用程序的哪些操作相关。
表示层文件夹的布局受到我们应用程序使用的框架的强烈影响,在本例中是 Angular。在这里,我们有一些高层次的通用文件,如 index.html shell 文件、全局样式和 Angular 应用程序的主入口点。其余的 UI 包含在 app 文件夹中。这个 app 文件夹中包含组成应用程序 UI 的各种组件。你可以看到我们有列表组件、待办事项组件和几个不同的弹出窗口组件。每个子文件夹都包含任何 Angular 应用程序中可能找到的标准组件文件集:模板、样式、单元测试和主组件类文件。
一个关于 TypeScript 架构的最佳实践是偏爱模块化设计。通过将应用程序划分为较小的模块化部分,我们可以更容易地在应用程序的不同部分重用代码并避免重复。在构建组成我们应用程序的模块时,我们应该保持它们小而专注于单一任务。这被称为单一职责原则,正如著名的开发者 Robert C. Martin 所说的那样,函数应该做一件事,并做好这件事。在 TypeScript 中,我们可以将这个原则应用到函数、类和模块中。
最重要的架构最佳实践是关注点分离。这意味着应用程序的不同部分之间应该有明确且独立的界限,每个部分负责不同的任务。
例如,控制 UI 的代码应该与写入数据库的代码完全分离,并且不仅仅是分离,它们甚至不应该知道彼此的存在。原因是,如果你需要更改应用程序使用的数据库,你不希望必须同时更改 UI。或者,如果我们想从 Angular 切换到 React,我们不应该需要完全重写整个应用程序,只需要更新与 UI 相关的部分即可。
为了实现这种分离,我们应该采用分层设计的架构。我们应该将应用程序分为不同的层,每一层负责不同的任务。我们还应该尽量减少每层之间的依赖关系,这样我们就可以在不影响其他层的情况下更改某一层。
对于现代的 TypeScript 应用程序,我们可以设计如下的分层架构:
-- 最外层是表示层(Presentation Layer),其中包含组成应用程序 UI 的组件和我们使用的任何框架。 -- 下一层是领域层(Domain Layer),其中包含应用程序的实体、业务逻辑和代表应用程序可以执行的不同操作的用例。 -- 最里面一层是数据层(Data Layer),它只关注获取和存储应用程序使用的数据。
应该尽量减少应用程序不同层之间的依赖关系。一种方法是遵循依赖原则。该原则规定依赖关系应始终指向内部,并且仅限于单层深度。
另一个架构最佳实践是领域驱动开发(Domain Driven Development,简称 DDD)。它侧重于通过从核心对象及其行为和交互开始开发应用程序,并围绕这些对象构建软件。这种方法迫使我们首先考虑应用程序最基本方面的结构和行为,并根据应用程序的需求进行构建。
在本应用程序中,我们首先从基础实体开始,即待办事项列表(to-do list)和待办事项(to-do)。实际上,在我构建此应用程序时,这两个文件是我创建的第一个文件。让我们快速看一下每个文件。
待办事项实体是一个简单的 TypeScript 类,具有标题和完成状态属性,并有一个更新完成状态的方法。它不导入任何内容,也不了解其外部的任何内容。
待办事项列表类稍微大一点。它有列表标题和待办事项集合的属性,并有添加待办事项、删除待办事项和返回所有待办事项的方法。该文件导入了待办事项类,我们用它来提供类型信息,但除此之外,它不导入任何其他内容,也不了解更广泛的应用程序。
在 DDD 方面,我们接下来应该考虑应用程序可以执行的所有不同操作,并为它们创建用例。每个用例都应遵循一个标准接口,以便以一致的方式实现应用程序中的任何操作。这是 use-case.interface 文件。我们可以看到,用例是一个泛型类型,并规定了一个方法 execute。这个方法可以接受任何参数,但应始终返回一个包含泛型类型的可观察对象(Observable)。
让我们看看一个实现此接口的用例。例如,创建待办事项的用例(create to-do use case)。这个简单的类暴露了 execute 方法,它接收一个待办事项,创建并返回一个包含该待办事项的可观察对象。方法内部,我们简单地创建一个新的待办事项,传入标题和完成状态属性,并使用 RXJS 的 of 函数返回一个包含我们创建的待办事项实例的新可观察对象。这个文件非常简单,主要导入了待办事项实体和用例接口。
应用程序中的其余用例大多相同,是小而集中的类,执行单一任务,不导入应用程序外层的任何内容。它们都直接操作构成应用程序内核的实体。
应用程序的用例略有不同,因为这些用例专注于获取和保存应用程序的数据。让我们看看其中一个用例。获取待办事项列表的文件(get-to-do-lists)负责获取待办事项列表的集合。你可以看到,除了导入两个实体之外,这个文件还导入了一个存储库。从架构上讲,存储库是一种与数据源交互的方式,而无需了解太多关于该数据源的细节。这个类非常小,存储库通过构造函数注入,仍然有一个 execute 方法,该方法本身只是调用存储库的方法并返回包含结果的可观察对象。
此应用程序只有一个存储库,它由存储库文件夹中的一个抽象类表示,该类指定了 get-to-do-lists 和 save-to-do-lists 方法并定义了这些方法的签名。save-to-do-lists 方法接收一个待办事项列表数组,并返回一个字符串,表示错误消息。get-to-do-lists 方法接收实际的待办事项类和待办事项列表类。这样做是为了在从数据层读取数据时能够创建这些对象的实例。
这个存储库可以由任何类型的数据源扩展,例如 API、实际数据库(如 MySQL)或其他数据源。这个应用程序使用本地存储,让我们看看数据文件夹中的具体实现。你可以看到这个类扩展了存储库并添加了两个必需的方法。get-to-do-lists 方法接收待办事项和待办事项列表类,并在方法内部从本地存储读取数据,然后使用构造函数创建保存的列表和待办事项,最后返回所有列表。save-to-do-lists 方法接收一个列表数组并尝试将其保存到本地存储中。如果保存成功,则返回 null;如果保存失败,则返回错误消息。
这个文件包含比我们看过的其他文件更多的功能代码,但它仍然相对较小,并且每个方法都专注于执行单一任务。有一点需要注意,这个文件确实从设计中的层之外导入了一些内容。数据层是内核,不应该了解其自身之外的任何内容。因此,导入存储库和实体似乎违背了依赖原则。
存储库在概念上像是一个桥梁,连接了领域层和数据层,这可以被认为是两个层之间的边界,因此数据层了解应用程序的这一部分是可以接受的。实体纯粹是为了类型目的而导入的,是 TypeScript 语法的内在部分。你可以看到我们只在文件内部的类型位置使用了它们,这些将在将应用程序转换为 JavaScript 时被编译器完全擦除。我们并没有将这些标记用作应用程序功能的基本部分,因此我认为这是架构上可以接受的违规行为。
我认为这很好地突显了我们所看到的任何原则以及我们在课程中涵盖的任何原则都只是指导方针。当可能时,我们应该使用它们,但不应拘泥于它们,并且能够在有限且具体的方式中打破它们。
领域驱动开发是一个巨大的话题,我在这里不可能做更多的介绍。我强烈建议你跟进 Pluralsight 的一些关于该主题的优秀课程,例如 Matthew Renzi 的《Clean Architecture, Patterns, Practices, and Principles》。
注意: 这里的存储库很简单,所以直接在内部操作数据,只是用了实体的类型。 实际项目应该使用实体的方法,因为实体大部分为api请求,间接操作数据库或各种数据源。
npm i eslint -D
npm i prettier -D
配置
.prettierrc.json
{ "singleQuote": true, "tabWidth": 2, "bracketSpacing": true, "trailingComma": "none", "printWidth": 100, "semi": false, "overrides": [ { "files": ".prettierrc", "options": { "parser": "typescript" } } ] }
`.prettierignore`
```json
**/*.md
**/*.svg
**/*.ejs
**/*.html
src/lib/*
node_modules
TypeScript最佳实践 编写可读, 可维护且性能卓越的代码 TypeScript Best Practices
1. Course Overview 课程概述
2. Project-level Best Practices 项目级最佳实践
Tooling Best Practices 工具最佳实践
3. Language Best Practices 语言最佳实践
any
Type 避免使用any
类型null
&undefined
Safely 安全处理null
和undefined
this
Use Decorators 使用装饰器
4. Asynchronous Best Practices 异步最佳实践
Always Clean up Subscriptions 始终清理订阅
5. Error Handling Best Practices 错误处理最佳实践
Provide Useful Errors and Error Documentation 提供有用的错误和错误文档
6. Performance Best Practices 性能最佳实践
Memoization and Caching 记忆化和缓存
7. Testing Best Practices 测试最佳实践