【译】Angular最佳实践

DevUI是一支兼具设计视角和工程视角的团队,服务于华为云 DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站: devui.design
Ng组件库: ng-devui(欢迎Star)

引言

Angular 框架作为前端三大框架之一,有着其独到优点,可用于创建高效、复杂、精致的单页面应用。

本文介绍了Angular开发过程中推荐的十八个最佳实践及示例,用于开发过程中参考运用。

1. trackBy

What

当使用*ngFor指令在html中对数组进行陈列时,添加trackBy()函数,目的是为每个item指定一个独立的id

Why

一般情况下,当数组内有变更时,Angular将会对整个DOM树加以重新渲染。如果加上trackBy方法,Angular将会知道具体的变更元素,并针对性地对此特定元素进行DOM刷新,提升页面渲染性能

详细内容 ->NetanelBasal

Example

【Before】

  • {{ item }}
  • 【After】

    // in the template
    
  • {{ item }}
  • // in the component trackByFn(index, item) { return item.id; // unique id corresponding to the item }

    2. const vs let

    What

    声明常量时,使用const而不是let

    Why

    a. 使赋值意图更加明确

    b. 若常量被重赋值,编译将直接报错,避免潜在风险

    c. 增加代码可读性

    Example

    【Before】

    let car = 'ludicrous car';
    let myCar = `My ${car}`;
    let yourCar = `Your ${car};
    if (iHaveMoreThanOneCar) {
      myCar = `${myCar}s`;
    }
    if (youHaveMoreThanOneCar) {
      yourCar = `${youCar}s`;
    }

    【After】

    // 变量car不会被重赋值,所以用const声明
    const car = 'ludicrous car';
    let myCar = `My ${car}`;
    let yourCar = `Your ${car}`;
    if (iHaveMoreThanOneCar) {
      myCar = `${myCar}s`;
    }
    if (youHaveMoreThanOneCar) {
      yourCar = `${youCar}s`;
    }

    3. pipeable 操作符

    What

    使用RxJs算子时,使用pipeable操作符号 ->拓展阅读

    Why

    a. 可被摇树优化: 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中,再在其他地方引用

    Why

    a. 隔离攻击,使得攻击更靠近于原有的请求所在地

    b. 减少用于处理攻击打补丁的代码

    c. 将这些攻击封装在同一个地方,更容易发现

    d. 当要解决bug的时候, 只需要到同一个文件内去搜寻,更容易定位

    注: 也可以打个自有标签,比如API_FIX,类似于TODO标签,用于标记API修复

    5. 模板的订阅

    What

    最好在html订阅变化,而不是在ts中

    Why

    a.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等操作符妥善取消订阅

    Why

    a. 如果不取消订阅,可能导致哪怕组件被销毁或者用户去到了其他页面了,但观察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$.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: 当你想要用新接收的值替换前面的旧值

    【译】Angular最佳实践_第1张图片

    mergeMap: 当你希望同时所有接收到的值进行操作

    【译】Angular最佳实践_第2张图片

    concatMap: 当你希望对接收到的值轮番处理

    【译】Angular最佳实践_第3张图片

    exhaustMap: 当还在处理前一个接收到的值时,取消处理后来值

    【译】Angular最佳实践_第4张图片

    Why

    a. 相较于链式使用多个操作符,使用一个合适的操作符实现相同的目的有助于有效减少代码量

    b. 不恰当地使用操作符可能导致预料外的行为,因为不同的操作符所实现的效果是不同的

    8. 懒加载

    What

    如果条件允许的话,尝试在angular应用中懒加载模块。

    懒加载是指仅在需要的情况下才加载模块内容

    Why

    a. 有效减少需要加载的应用体积

    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
      ]
    })

    9. 避免嵌套订阅

    What

    一定情况下,可能需要从多个observable中获取数据去达到特定目的。

    在这种情况下,避免在订阅块内嵌套订阅。

    更好的方法是使用合适的链式操作符。

    比如:withLatestFromcombineLatest

    Why

    代码异味/可读性/复杂度: 没有完全使用RxJs,表明开发者对RxJs的API浅层使用不熟悉

    代码表现: 如果是冷observable,将会持续订阅第一个observable直到其complete,然后才是启动第二个observable的工作。

    假如其中有网络请求,那么表现就会为瀑布流式的

    Example

    【Before】

    firstObservable$.pipe(take(1))
    .subscribe(firstValue => {
      secondObservable$.pipe(
        take(1)
      )
      .subscribe(secondValue => {
        console.log(Combined values are: ${firstValue} & ${secondValue});
      });
    });

    【After】

    firstObservable$.pipe(
      withLatestFrom(secondObservable$),
      first()
    )
    .subscribe(([firstValue, secondValue]) => {
      console.log(Combined values are: ${firstValue} &${secondValue});
    });

    10. 避免使用any,明确定义类型

    What

    声明变量或常量时,为其指定具体类型而不是简单使用any

    Why

    a. 当在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

    通过上述方法,可以避免由于类型缺失导致的bug

    b. 指定类型的另一个好处是可以使得重构更简单,更安全

    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 name
    Age: My cool age
    Location: My cool location

    假如希望重命名myFlashObject中的loc属性名为location

    public 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 name
    Age: My cool age
    Location: 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规则

    What

    lint规则由多个预置的选项比如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 message
    I am a naughty console warning message
    I am a naughty console error message

    【After】

    // tslint.json
    {
      "rules": {
        .......
        "no-console": [
            true,
          "log", // no console.log allowed
          "warn" // no console.warn allowed
        ]
      }
    }
    
    // ..component.ts
    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');
    }

    // Output

    lint在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

    避免将除了展示逻辑外的业务逻辑封装进组件,确保组件只用于处理展示逻辑

    Why

    a. 组件是为控制视图及展示目的而设计的,任何业务逻辑都应封装到自己合适的方法或者service内部,业务逻辑应与组件逻辑分离

    b. 业务逻辑如果被抽取到一个service内部,通常更适用于使用单元测试,而且可以被其他需要相同的业务逻辑的组件重用

    14. 避免长方法

    What

    长方法通常说明他们已经包含了太多的任务,尝试使用单一职责原则

    一个方法应该作为整体去完成一件事情,如果其中有多个操作,那么我们可以抽取这些方法,形成独立的函数,使得他们独自负责各自职责,再去调用他们

    Why

    a. 长方法难以阅读、理解以及维护。他们容易产生bug,因为改变其中一部分很可能影响方法内的其他逻辑。这也使得代码重构更加难以进行

    b. 方法可以用圈复杂度衡量,有一些TSLint方法用于检测圈复杂度,你可以在你的项目中去使用,避免bug以及检测代码可用性

    15. Dry

    What

    Dry = Do not Repeat Yourself

    保证在代码仓库中没有重复拷贝的代码,抽取重复代码,并且在需要使用的地方引用即可

    Why

    a. 在多个地方用重复代码意味着,如果我们想要改变代码逻辑,我们需要在多个地方修改,降低了代码的可维护性

    使得对代码逻辑进行变更变得很困难而且测试过程很漫长

    b. 抽取重复代码到一个地方,意味着只需要修改一处代码以及单次测试

    c. 同时更少的代码意味着更快的速度

    16. 增加缓存

    What

    发起API请求得到的响应通常并没有经常变化,在这类场景里,可以通过增加缓存机制并且储存获取的值

    当同样的API请求再发起的时候,确认cache中是否已经有值,若有,则可以直接使用,否则发起请求并缓存。

    如果这些值会变化但变化不频繁,那么可以引入一个缓存时间,用于决策是否使用缓存或者去重新调用

    Why

    具有缓存机制意味着可以避免不必要的API调用,通过避免重复调用有助于提高应用响应速度,不再需要等待网络返回,而且我们不需要重复地下载同样的信息

    17. 避免模板中的逻辑

    What

    如果在HTML中需要增加任何逻辑,哪怕只是简单的&&,最好都将其抽取到组件内

    Why

    模板中的逻辑难以单元测试,当切换模板代码的时候容易导致代码问题

    【Before】

    // template
    

    Status: Developer

    // component public ngOnInit (): void { this.role = 'developer'; }

    【After】

    Status: Developer

    // component public 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"

    加入我们

    我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:[email protected]

    本文版权归原作者所有,仅用于学习与交流;
    如需转载译文,烦请按下方注明出处信息,谢谢!
    原文链接:Best practices for a clean and performant Angular application
    作者: Vamsi Vempati
    译者: DevUI 弘一

    你可能感兴趣的:(angular,最佳实践)