WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

TypeScript最佳实践 编写可读, 可维护且性能卓越的代码 TypeScript Best Practices #195

Open WangShuXian6 opened 1 month ago

WangShuXian6 commented 1 month ago

TypeScript最佳实践 编写可读, 可维护且性能卓越的代码 TypeScript Best Practices

1. Course Overview 课程概述


2. Project-level Best Practices 项目级最佳实践

  1. Structural Best Practices 结构最佳实践
  2. Architectural Best Practices 架构最佳实践
  3. Configuration Best Practices 配置最佳实践
  4. Tooling Best Practices 工具最佳实践

    3. Language Best Practices 语言最佳实践

  5. Avoid the any Type 避免使用 any 类型
  6. Handle null & undefined Safely 安全处理 nullundefined
  7. Avoid Excessive Annotation 避免过度注释
  8. Utilize Intersection Types 利用交叉类型
  9. Use Enums 使用枚举
  10. Using Tuples for Fixed-length Arrays 使用元组表示固定长度数组
  11. Use ReadOnly and ReadOnlyArray Types 使用只读(ReadOnly)和只读数组(ReadOnlyArray)类型
  12. Use the Polymorphic This 使用多态的 this
  13. Favour Type-guards over Type Assertion 偏好类型守卫而非类型断言
  14. Make Switch Statements Exhaustive 确保 switch 语句穷尽所有可能性
  15. Use Utility Types Where Applicable 在适用的地方使用工具类型
  16. Use Generics 使用泛型
  17. Use Conditional Types 使用条件类型
  18. Use Decorators 使用装饰器

    4. Asynchronous Best Practices 异步最佳实践

  19. Use Async/Await 使用 Async/Await
  20. Always Handle Errors 始终处理错误
  21. Use Promise.all 和 Promise.race
  22. Use Loading States and Timeouts 使用加载状态和超时
  23. Always Clean up Subscriptions 始终清理订阅

    5. Error Handling Best Practices 错误处理最佳实践

  24. Error Handling in TypeScript TypeScript 中的错误处理
  25. Use TryCatch Correctly 正确使用 Try/Catch
  26. Returning vs. Throwing Errors 返回错误与抛出错误
  27. Use Error Classes 使用错误类
  28. Provide Useful Errors and Error Documentation 提供有用的错误和错误文档

    6. Performance Best Practices 性能最佳实践

  29. Implement Lazy-loading 实现懒加载
  30. Use Web-workers 使用 Web workers
  31. Break up Long-running Tasks 拆分长时间运行的任务
  32. Throttling and Debouncing 节流和去抖
  33. Memoization and Caching 记忆化和缓存

    7. Testing Best Practices 测试最佳实践

  34. Writing Readable Tests 编写可读的测试
  35. Test and Test Data Independence 测试和测试数据独立性
  36. Test-driven Development 测试驱动开发
  37. Use a Modern Test Runner 使用现代测试运行器
WangShuXian6 commented 1 month ago

1. Course Overview 课程概述

WangShuXian6 commented 1 month ago

2. Project-level Best Practices 项目级最佳实践

1. Structural Best Practices 结构最佳实践

./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 应用程序。

当然,对于 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 应用程序中可能找到的标准组件文件集:模板、样式、单元测试和主组件类文件。

2. Architectural Best Practices 架构最佳实践

模块化设/单一职责原则

一个关于 TypeScript 架构的最佳实践是偏爱模块化设计。通过将应用程序划分为较小的模块化部分,我们可以更容易地在应用程序的不同部分重用代码并避免重复。在构建组成我们应用程序的模块时,我们应该保持它们小而专注于单一任务。这被称为单一职责原则,正如著名的开发者 Robert C. Martin 所说的那样,函数应该做一件事,并做好这件事。在 TypeScript 中,我们可以将这个原则应用到函数、类和模块中。

架构方式1:关注点分离

最重要的架构最佳实践是关注点分离。这意味着应用程序的不同部分之间应该有明确且独立的界限,每个部分负责不同的任务。 图片

例如,控制 UI 的代码应该与写入数据库的代码完全分离,并且不仅仅是分离,它们甚至不应该知道彼此的存在。原因是,如果你需要更改应用程序使用的数据库,你不希望必须同时更改 UI。或者,如果我们想从 Angular 切换到 React,我们不应该需要完全重写整个应用程序,只需要更新与 UI 相关的部分即可。

分层设计

图片

为了实现这种分离,我们应该采用分层设计的架构。我们应该将应用程序分为不同的层,每一层负责不同的任务。我们还应该尽量减少每层之间的依赖关系,这样我们就可以在不影响其他层的情况下更改某一层。

对于现代的 TypeScript 应用程序,我们可以设计如下的分层架构:

-- 最外层是表示层(Presentation Layer),其中包含组成应用程序 UI 的组件和我们使用的任何框架。 -- 下一层是领域层(Domain Layer),其中包含应用程序的实体、业务逻辑和代表应用程序可以执行的不同操作的用例。 -- 最里面一层是数据层(Data Layer),它只关注获取和存储应用程序使用的数据。 图片

依赖原则

应该尽量减少应用程序不同层之间的依赖关系。一种方法是遵循依赖原则。该原则规定依赖关系应始终指向内部,并且仅限于单层深度。 图片

架构方式2:领域驱动开发(Domain Driven Development,简称 DDD)

另一个架构最佳实践是领域驱动开发(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请求,间接操作数据库或各种数据源。

3. Configuration Best Practices 配置最佳实践

4. Tooling Best Practices 工具最佳实践

eslint

npm i eslint -D

prettier

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