Angular最佳实践

Angular 框架作为前端三大框架之一,有着其独到优点,可用于创建高效、复杂、精致的单页面应用。本文介绍了Angular开发过程中推荐的十八个最佳实践及示例,用于开发过程中参考运用。1. trackByWhat当使用*ngFor指令在html中对数组进行陈列时,添加trackBy()函数,目的是为每个item指定一个独立的idWhy一般情况下,当数组内有变更时,Angular将会对整个DOM树加以重新渲染。如果加上trackBy方法,Angular将会知道具体的变更元素,并针对性地对此特定元素进行DOM刷新,提升页面渲染性能详细内容 -> NetanelBasalExample【Before】<li *ngFor=“let item of items;”>{{ item }}复制代码【After】// in the template

// in the component
trackByFn(index, item) {
return item.id; // unique id corresponding to the item
}复制代码2. const vs letWhat声明常量时,使用const而不是letWhya. 使赋值意图更加明确b. 若常量被重赋值,编译将直接报错,避免潜在风险c. 增加代码可读性Example【Before】let car = ‘ludicrous car’;
let myCar = My ${car};
let yourCar = Your ${car}; if (iHaveMoreThanOneCar) { myCar =KaTeX parse error: Expected 'EOF', got '}' at position 12: {myCar}s`; }̲ if (youHaveMor…{youCar}s; }复制代码【After】// 变量car不会被重赋值,所以用const声明 const car = 'ludicrous car'; let myCar =My ${car}; let yourCar =Your KaTeX parse error: Expected '}', got 'EOF' at end of input: …) { myCar = `{myCar}s; } if (youHaveMoreThanOneCar) { yourCar =${youCar}s`;
}复制代码3. pipeable 操作符What使用RxJs算子时,使用pipeable操作符号 -> 拓展阅读Whya. 可被摇树优化: import的代码中,只有需要被执行的才会被引入b. 容易定位到代码中未使用的算子注: 需要RxJs版本在5.5及以上Example【Before】import ‘rxjs/add/operator/map’;
import ‘rxjs/add/operator/take’;

iAmAnObservable
.map(value => value.item)
.take(1)复制代码【After】import { map, take } from ‘rxjs/operators’;

iAmAnObservable
.pipe(
map(value => value.item),
take(1)
)复制代码4. 隔离API攻击What不是所有的API都是安全的 -> 很多情况下需要添加额外的代码逻辑去为API打补丁相较于将这些逻辑放在component中,更好的做法是封装到一个独立的地方:比如封装到service中,再在其他地方引用Whya. 隔离攻击,使得攻击更靠近于原有的请求所在地b. 减少用于处理攻击打补丁的代码c. 将这些攻击封装在同一个地方,更容易发现d. 当要解决bug的时候, 只需要到同一个文件内去搜寻,更容易定位注: 也可以打个自有标签,比如API_FIX,类似于TODO标签,用于标记API修复5. 模板的订阅What最好在html订阅变化,而不是在ts中Whya. async管道能自动取消订阅:通过减少手动订阅管理能够简化代码b. 减少在ts中忘记取消订阅,造成内存泄露的风险(这种风险也可以通过lint规则检测来避免)c. 减少由于在订阅之外数据发生变更,进而引入bug的情况Example【Before】// template

{{ textToDisplay }}

// component
iAmAnObservable
.pipe(
map(value => value.item),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item复制代码【After】// template

{{ textToDisplay$ | async }}

// component
this.textToDisplay$ = iAmAnObservable
.pipe(
map(value => value.item)
)复制代码6. 订阅清理What如果订阅了observable,记得通过take, takeUntil等操作符妥善取消订阅Whya. 如果不取消订阅,可能导致哪怕组件被销毁或者用户去到了其他页面了,但观察observable流始终保持进而造成内存泄露b. 更好的做法是:通过lint规则检测来避免Example【Before】iAmAnObservable
.pipe(
map(value => value.item)
)
.subscribe(item => this.textToDisplay = item);复制代码【After】private _destroyed$ = new Subject();

public ngOnInit (): void {
iAmAnObservable
.pipe(
map(value => value.item)
// 被销毁前希望一直监听
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
}

public ngOnDestroy (): void {
this._destroyed . n e x t ( ) ; t h i s . d e s t r o y e d .next(); this._destroyed .complete();
}复制代码如果你只想要第一个值,那么就使用一个take(1)iAmAnObservable
.pipe(
map(value => value.item),
take(1),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);复制代码注: 此处takeUntil与take在此处同时被使用,目的是防止在组件被销毁前一直没有收到值,导致内存泄露。(如果没有takeUntil,那么在获取到第一个值之前,这个订阅将持续存在,而组件在被销毁后,由于不可能接收到第一个值,就会造成内存泄露)7. 使用合适的操作符What选取合适的合并操作符switchMap: 当你想要用新接收的值替换前面的旧值mergeMap: 当你希望同时所有接收到的值进行操作concatMap: 当你希望对接收到的值轮番处理exhaustMap: 当还在处理前一个接收到的值时,取消处理后来值Whya. 相较于链式使用多个操作符,使用一个合适的操作符实现相同的目的有助于有效减少代码量b. 不恰当地使用操作符可能导致预料外的行为,因为不同的操作符所实现的效果是不同的8. 懒加载What如果条件允许的话,尝试在angular应用中懒加载模块。懒加载是指仅在需要的情况下才加载模块内容Whya. 有效减少需要加载的应用体积b. 通过避免加载不需要的模块,能够有效提升启动性能Example【Before】{ path: ‘not-lazy-loaded’, component: NotLazyLoadedComponent }【After】// app.routing.ts

{
path: ‘lazy-load’,
loadChildren: () => import(lazy-load.module).then(m => m.LazyLoadModule)
}

// lazy-load.module.ts
import { NgModule } from ‘@angular/core’;
import { CommonModule } from ‘@angular/common’;
import { RouterModule } from ‘@angular/router’;
import { LazyLoadComponent } from ‘./lazy-load.component’;

@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: ‘’,
component: LazyLoadComponent
}
])
],
declarations: [
LazyLoadComponent
]
})
export class LazyModule {}复制代码9. 避免嵌套订阅What一定情况下,可能需要从多个observable中获取数据去达到特定目的。在这种情况下,避免在订阅块内嵌套订阅。更好的方法是使用合适的链式操作符。比如: withLatestFrom, combineLatest等Why代码异味/可读性/复杂度: 没有完全使用RxJs,表明开发者对RxJs的API浅层使用不熟悉代码表现: 如果是冷observable,将会持续订阅第一个observable直到其complete,然后才是启动第二个observable的工作。假如其中有网络请求,那么表现就会为瀑布流式的Example【Before】firstObservableKaTeX parse error: Expected '}', got 'EOF' at end of input: …econdObservable.pipe( take(1) ) .subscribe(secondValue => { console.log(Combined values are: ${firstValue} &${secondValue}); });});【After】firstObservable . p i p e ( w i t h L a t e s t F r o m ( s e c o n d O b s e r v a b l e .pipe( withLatestFrom(secondObservable ), first()) .subscribe(([firstValue, secondValue]) => { console.log(Combined values are: ${firstValue} & ${secondValue});});10. 避免使用any,明确定义类型What声明变量或常量时,为其指定具体类型而不是简单使用anyWhya. 当在TS中声明未指定类型的变量或者厂里,其类型将会由赋予的值推论得出,这容易引起意料之外的问题一个经典的例子如下:Example【Before】const x = 1;const y = ‘a’;const z = x + y;console.log(Value of z is: ${z}// 输出Value of z is 1a如果原来的预期输入是y也是个数字类型,那么就会导致意料之外的问题。【After】这些问题可以通过为声明变量指定一个恰当的类型来避免:const x: number = 1;const y: number = ‘a’;const z: number = x + y;// 这个输入将会导致编译报错抛出Type ‘“a”’ is not assignable to type ‘number’.const y:number通过上述方法,可以避免由于类型缺失导致的bugb. 指定类型的另一个好处是可以使得重构更简单,更安全Example【Before】public ngOnInit (): void { let myFlashObject = { name: ‘My cool name’, age: ‘My cool age’, loc: ‘My cool location’ } this.processObject(myFlashObject);}public processObject(myObject: any): void { console.log(Name: ${myObject.name}); console.log(Age: ${myObject.age}); console.log(Location: ${myObject.loc});}// 输出Name: My cool nameAge: My cool ageLocation: My cool location假如希望重命名myFlashObject中的loc属性名为locationpublic ngOnInit (): void { let myFlashObject = { name: ‘My cool name’, age: ‘My cool age’, location: ‘My cool location’ } this.processObject(myFlashObject);}public processObject(myObject: any): void { console.log(Name: ${myObject.name}); console.log(Age: ${myObject.age}); console.log(Location: ${myObject.loc});}// 输出Name: My cool nameAge: My cool ageLocation: undefined当未对myFlashObject指定类型时,看起来方法loc属性在myFlashObject中不存在而不是属性取值错误导致的上述结果【After】当对myFlashObject增加了类型定义,我们将获取到一个更加清晰的编译报错问题如下type FlashObject = { name: string, age: string, location: string}public ngOnInit (): void { let myFlashObject: FlashObject = { name: ‘My cool name’, age: ‘My cool age’, // Compilation error Type ‘{ name: string; age: string; loc: string; }’ is not assignable to type ‘FlashObjectType’. Object literal may only specify known properties, and ‘loc’ does not exist in type ‘FlashObjectType’. loc: ‘My cool location’ } this.processObject(myFlashObject);}public processObject(myObject: FlashObject): void { console.log(Name: ${myObject.name}); console.log(Age: ${myObject.age}) // Compilation error Property ‘loc’ does not exist on type ‘FlashObjectType’. console.log(Location: ${myObject.loc});}如果你正在开启一个全新的工程,推荐在tsconfig.json文件中设定strict:true方式,将严格模式打开,开启所有的严格类型检查选项11. 使用lint规则Whatlint规则由多个预置的选项比如no-any, no-magic-numbers, no-consle 等,你可以在你的tslint.json文件中去开启特定的校验规则Why使用lint规则意味着,在某个地方有不应当产生发生的行为出现时,你将会得到较为清晰的报错这将会提高你应用代码的一致性以及可读性一些lint规则甚至有特定的fix解法用于解决此lint任务如果你希望去定义自己的lint规则,你也可以去撰写使用TSQuery去编写自己的lint规则的教程链接一个经典的例子如下:Example【Before】public ngOnInit (): void { console.log(‘I am a naughty console log message’); console.warn(‘I am a naughty console warning message’); console.error(‘I am a naughty console error message’);}// 输出并不会报错,而是在控制台中打印如下信息:I am a naughty console messageI am a naughty console warning messageI am a naughty console error message【After】// tslint.json{ “rules”: { … “no-console”: [ true, “log”, // no console.log allowed “warn” // no console.warn allowed ] }}// …component.tspublic ngOnInit (): void { console.log(‘I am a naughty console log message’); console.warn(‘I am a naughty console warning message’); console.error(‘I am a naughty console error message’);}// Outputlint在console.log及log.warn语句处报错,console.error并不会报错,因为lint规则中未配置Calls to ‘console.log’ are not allowed.Calls to ‘console.warn’ are not allowed.12. 精简,可重用的组件What将组件中可重用的代码片段抽取出来成为一个新的组件让组件尽可能地“dumb”,从而能够在更多的场景中复用编写“dumb”组件的意思是,其中没有隐含特别的逻辑,操作只是简单地依赖于提供给它的输入输出作为一个通用的规则,在组件树中的最子节点的组件将会是其中最“dumb”的一个Why可重用的组件将会降低代码重复率,进而使其更易于维护及变更dumb组件更加简单,因此存在bug的可能性也更低。dumb组件使得你去仔细思考如何抽取通用组件API,并且帮助你识别出混杂的问题13. 组件只处理展示逻辑What避免将除了展示逻辑外的业务逻辑封装进组件,确保组件只用于处理展示逻辑Whya. 组件是为控制视图及展示目的而设计的,任何业务逻辑都应封装到自己合适的方法或者service内部,业务逻辑应与组件逻辑分离b. 业务逻辑如果被抽取到一个service内部,通常更适用于使用单元测试,而且可以被其他需要相同的业务逻辑的组件重用14. 避免长方法What长方法通常说明他们已经包含了太多的任务,尝试使用单一职责原则一个方法应该作为整体去完成一件事情,如果其中有多个操作,那么我们可以抽取这些方法,形成独立的函数,使得他们独自负责各自职责,再去调用他们Whya. 长方法难以阅读、理解以及维护。他们容易产生bug,因为改变其中一部分很可能影响方法内的其他逻辑。这也使得代码重构更加难以进行b. 方法可以用圈复杂度衡量,有一些TSLint方法用于检测圈复杂度,你可以在你的项目中去使用,避免bug以及检测代码可用性15. DryWhatDry = Do not Repeat Yourself保证在代码仓库中没有重复拷贝的代码,抽取重复代码,并且在需要使用的地方引用即可Whya. 在多个地方用重复代码意味着,如果我们想要改变代码逻辑,我们需要在多个地方修改,降低了代码的可维护性 使得对代码逻辑进行变更变得很困难而且测试过程很漫长b. 抽取重复代码到一个地方,意味着只需要修改一处代码以及单次测试c. 同时更少的代码意味着更快的速度16. 增加缓存What发起API请求得到的响应通常并没有经常变化,在这类场景里,可以通过增加缓存机制并且储存获取的值当同样的API请求再发起的时候,确认cache中是否已经有值,若有,则可以直接使用,否则发起请求并缓存。如果这些值会变化但变化不频繁,那么可以引入一个缓存时间,用于决策是否使用缓存或者去重新调用Why具有缓存机制意味着可以避免不必要的API调用,通过避免重复调用有助于提高应用响应速度,不再需要等待网络返回,而且我们不需要重复地下载同样的信息17. 避免模板中的逻辑What如果在HTML中需要增加任何逻辑,哪怕只是简单的&&,最好都将其抽取到组件内Why模板中的逻辑难以单元测试,当切换模板代码的时候容易导致代码问题【Before】// template<p *ngIf=“role===‘developer’”> Status: Developer

// componentpublic ngOnInit (): void { this.role = ‘developer’;}【After】<p *ngIf=“showDeveloperStatus”> Status: Developer // componentpublic ngOnInit (): void { this.role = ‘developer’; this.showDeveloperStatus = true;}18. 安全地声明string类型What如果有一些string变量只有一些特定的值,相比于声明为string类型,更好的方式是将其声明为一个可能的值集合类型Why通过为变量提供恰当的声明有助于避免bug:当编写代码超出预期时可以在编译阶段被发现,而不是等运行了才发现【Before】private myStringValue: string;

if (itShouldHaveFirstValue) {
myStringValue = ‘First’;
} else {
myStringValue = ‘Second’
}复制代码【After】private myStringValue: ‘First’ | ‘Second’;

if (itShouldHaveFirstValue) {
myStringValue = ‘First’;
} else {
myStringValue = ‘Other’
}

// This will give the below error
Type ‘“Other”’ is not assignable to type ‘“First” | “Second”’
(property) AppComponent.myValue: “First” | “Second”

发布了28 篇原创文章 · 获赞 0 · 访问量 907

猜你喜欢

转载自blog.csdn.net/srhgsr/article/details/105498961
今日推荐