这篇文章将帮助你避免以后很难(或只是令人厌烦)纠正的错误。如果你打算创建一个新项目,并想让它变得令人惊叹——继续阅读!
纯函数
这是最重要的:保持你的函数的纯粹性,尽可能多地使用它们。
维基百科就是这样定义纯函数的:
- 对于相同的参数,函数返回值是相同的(局部静态变量、非局部变量、可变引用参数或输入流没有变化),并且
- 该函数没有副作用(没有局部静态变量、非局部变量、可变引用参数或输入/输出流的突变)。
使用关键字this的函数并不纯粹——它们使用的信息超出了它们的作用域,因此它们可以为相同的参数返回不同的结果。
尽管如此,我们的一些函数显然必须使用这一点——尽管如此,还是要尽可能多地将代码从不纯函数转移到纯函数。
变异其论点的函数是邪恶的最大来源——不惜一切代价避免它们。
不可变动性
数据的意外突变通常会导致危险的错误,这就是JS社区创建工具为数据结构提供不变性的原因。如果你愿意的话,你可以找到它们并阅读它们的文档(至少阅读它们是个好主意)。
我将向您展示一个在许多情况下“足够好”的技巧-它不会像不变性工具那样将您从每一个错误中拯救出来,但它将涵盖绝大多数情况,并且不会花费您任何费用:
export type UserModel = { readonly name: string; readonly email?: string; readonly age?: string; }
通过在数据结构的字段中添加readonly关键字,每次尝试对作为参数接收的数据进行变异时,都会出现TS错误。所有这些都只会在编译时产生成本——它将在编译后被删除,并且不会在运行时进行检查(0性能成本)。
只需创建一个浅拷贝就可以消除该错误,并创建一个具有修改字段的新对象:
updatedUser = { ...user, age: 25 }
此外,它不像可变性工具那样具有限制性——如果你知道自己在做什么,并且想在特定情况下忽略这种限制,你可以应用-readonly修饰符或fest或ts本质类型中的Writeable类型。
在这些库中,您还可以找到使数据结构递归(深层)只读的类型,但在实践中,它并不像预期的那样完美和可预测-如果您愿意,可以尝试它们,但我发现最好手动将字段声明为只读(并对嵌套结构递归),或者只使用不变性库。
不变性是一个大话题,对于一篇文章的一部分来说太大了,你可以找到更多的信息和观点。
如果你还没有使用它,我鼓励你尝试一下:一开始,这将是一条有点崎岖的道路,但最终,你会开始应用规则和编程模式,并考虑到不变性,它将变得和常规编程一样容易。此外,它特别适用于纯函数;)
可见性和可变性修饰符
有一条众所周知的规则:使用const而不是let来声明变量。
我建议您也使用这个:将只读修饰符添加到您不打算修改的每个字段(在类、类型、接口中)。它将带来0成本——如果你不打算修改它,那么编译器会捕捉到任何意外的尝试。如果您以后决定将此字段设为可写字段,则可以删除只读修饰符。
export class HealthyComponent { // do not modify this! private readonly stream$: Observable<UserModel>; // it's ok to modify this protected isInitialized: boolean = false; constructor() { // you can initialize readonly fields in the constructor this.stream$ = of({name: example}); } }
对于类的字段和方法(包括组件、管道、服务和指令),请使用可见性修饰符:private、protected和public。
稍后您将为此感谢自己:当某个字段的方法是私有的,并且在某个时刻您看到您的类可以删除它(或者您需要修改它)时,您可以确保没有其他代码在使用它,所以可以重构它。
受保护的字段和方法不仅对继承的类可见,而且在Angular模板中也是可见的,因此,对模板应该访问的字段和方式使用Protected修饰符是一个很好的理由,而对模板不需要的字段和方法使用private修饰符则是一个非常好的理由。
我不添加公共修饰符——默认情况下,字段和方法是公共的,没有修饰符——但这是您个人的选择。由于输入和输出应该是公共的,所以我不会为它们添加修饰符,以免重载语法。
与私有修饰符一样,protected会让你知道你可以安全地删除或修改一些字段或方法,而不用担心模板。
任何代码都会随着时间的推移而增加复杂性,人类无法记住所有内容:这就是为什么这些小的修饰符在重构过程中会产生很大的影响。
类型别名
我希望有人早点告诉我:使用类型别名,而不是模型和其他数据结构的接口。
export type Model = { readonly field: string; readonly isExample?: boolean; }
在另一个文件中:
import type { Model } from '@example/models';
通过这样做,当您只需要几个模型时,可以避免加载整个库,因为在TypeScript编译:documentation链接期间,像这样的导入将被完全删除。
此外,您将避免隐式接口声明合并,并将使用显式交集类型(如果您愿意的话)。没有其他显著差异,因此类型别名是最佳选择。
品牌类型
另一个技巧最好在项目开始时就开始使用。
“品牌类型”类似于常规类型,但有一些附加信息。代码可以忽略此添加,编译器将使用此添加来帮助您。
export type UUID = string & { __type: 'UUID' };
在上面的例子中,UUID仍然像字符串一样工作,但它有一条附加信息(“品牌”),这将有助于将其与普通字符串区分开来。
当您将用户的密码而不是电子邮件传递给某个功能时,品牌类型将使您免受这种情况的影响。当你发送错误的ID时,它会捕捉到错误——如果没有品牌类型,这是最难捕捉的情况,因为ID通常存储在具有类似名称和类型的变量和字段中。
我们可以使用一个独特的符号,也可以不使用:
// Method with additional fields: export type UUID = string & { readonly __type: 'UUID' }; export type DomainUUID = UUID & { readonly __model: 'Domain' } export type Domain = { readonly uuId: DomainUUID; readonly isActive: boolean; readonly name: string; } export type UserUUID = UUID & { readonly __model: 'User' } export type User = { readonly uuId: UserUUID; readonly name: string; } // Method with a unique symbol: declare const brand: unique symbol; export type Brand<T, TBrand extends string> = T & { readonly [brand]: TBrand; } export type UUID = Brand<string, 'UUID'>; // you can extend not only primitive types export type DomainUUID = Brand<UUID, 'Domain'>; export type Domain = { readonly uuId: DomainUUID; readonly isActive: boolean; readonly name: string; } export type UserUUID = Brand<UUID, 'User'>; export type User = { readonly uuId: UserUUID; readonly name: string; }
你可以从代码中看到,这里唯一的符号优雅地取代了我们的人工字段__model。
关于品牌类型及其实现方法的更多信息,您可以在这个Twitter帖子中阅读。
类型化函数
请键入您的函数:它们的参数和返回的结果。
当你创建它们时,它们应该得到什么以及应该返回什么对你来说是显而易见的。但添加类型有两个原因:
- 对于代码的其余部分,它们应该向该函数发送什么以及它保证返回什么并不明显;
- 对你来说,几个月后,这也不会很明显。
如果我们应该声明返回类型,有不同的意见:我建议您声明它们,排除(如果您愿意的话)那些不返回任何内容的类型。
最初的主要好处并不明显,但在重构过程中会非常有帮助:
- 如果您更改了函数的代码,并且不小心更改了它的返回类型,它将被编译器捕获;
- 如果您有意更改函数的返回类型,而使用此函数的某些代码还没有准备好,那么它将被编译器捕获。
在推断类型的情况下,一些代码很可能会接受返回的结果,但会改变行为:
// before refactoring function isAuthenticated(user: User) { //... return true; // inferred type: boolean } if (isAuthenticated(user)) { markItemAsPaid(); } else { redirectToLogin(); } // after refactoring function isAuthenticated(user: User) { //... return someApiRequest(user); // inferred type: Observable<boolean> }
在这个例子中,编译器不会引发任何错误:“Observable”是一个对象,if(isAuthenticated(user))可以工作,但它总是返回true。
这是一个简单的例子,但对于更复杂的代码,发生这种情况的几率更高。
此外,它还显著提高了代码的可读性,这是一个非常重要的指标,比保存几个符号进行键入更重要。
继承
使用组合而非继承原则。
上面链接的文本解释了Angular上下文之外的内容,我将解释为什么在Angular中使用抽象类和继承组件和指令不是一个好主意。
除了继承带来的常见问题外,父类中声明的输入和输出也将被继承,因此您必须在子类中支持它们,即使您在特定的子类中不需要它们。
此外,在组件的情况下,每个Angular组件都应该有一个模板。这里有两种方法:
- 链接到父母的模板:你不能覆盖任何东西,所以父母的模板会有很多分支来处理每个孩子的需求和特殊情况;
- 创建一个子模板:您将不得不复制整个模板,这会降低代码的可重用性——这也是继承的最初原因。
与本文中的其他建议一样,这一建议带来了一些额外的动作和思考,但没有什么好的东西是免费的。这篇文章是为了帮助你,而不是争论或批评:尽可能多地使用本文中的建议✌️
阅读“掌握角度”:
- Promises vs Observables
- Hot and Cold Observables
- Mapping the Observables
- Dangers and Treasures of RxJS
- RxJS Pipelines
- Repository and File Structure
- Essential Code Organization Principles
- 登录 发表评论