用Angular写出烂代码

2019-03-12 08:26:22 织梦安装使用
  • 文章介绍
山高路远 Angular完全开发手册

Angular是一个非常棒的前端框架,它提供了很多开箱即用的功能,比如:路由,动画,HTTP,表单等,这大大加快了开发人员的上手速度和开发速度,特别是配合angular-cli,你不需要考虑如何把ts编译成js,是不是还要弄个babel转码,打包的时候用grunt还是gulp或者webpack,怎么把sass、less转换成css。。。。,只要有nodejs和angular-cli其它的一切都不需要你考虑。

但是,即使抓着一手好牌,仍然架不住4个2带俩王这样的打法。

注意:这篇文章里为了演示方便会使用内联模板,但在大多数情况下,这是一个不好的写法,但作为演示来说会让你阅读起来更舒服。

另外,为了示例更简洁,示例中会忽略import语句和一些其它的引用。

不是在真正的使用angular组件

组件是angular应用的基本组成单位,负责连接应用的逻辑和视图。但是有些时候,开发人员会过分的把一些无意义的东西划分到一个组件中,就像下面的代码中展示的:

  1. @Component({

  2.  selector: app-some-component-with-form,

  3.  template: `

  4.        <div [formGroup]="form">

  5.          <div class="form-control">

  6.            <label>First Name</label>

  7.            <input type="text" formControlName="firstName" />

  8.          </div>

  9.          <div class="form-control">

  10.            <label>Last Name</label>

  11.            <input type="text" formControlName="lastName" />

  12.          </div>

  13.          <div class="form-control">

  14.            <label>Age</label>

  15.            <input type="text" formControlName="age" />

  16.          </div>

  17.        </div>

  18.    `

  19. })

  20. export class SomeComponentWithForm {

  21.  public form: FormGroup;

  22.  constructor(private formBuilder: FormBuilder){

  23.    this.form = formBuilder.group({

  24.      firstName: [, Validators.required],

  25.      lastName: [, Validators.required],

  26.      age: [, Validators.max(120)],      

  27.    })

  28.  }

  29. }

这个示例中只有一个表单,包含三个表单控件。每一个控件模板都高度相似,div,label,input,或许你会把它划分到一个组件中:

  1. @Component({

  2.  selector: app-single-control-component,

  3.  template: `

  4.        <div class="form-control">

  5.          <label>{{ label }}</label>

  6.          <input type="text" [formControl]="control" />

  7.        </div>

  8.      `

  9. })

  10. export class SingleControlComponent{

  11.  @Input() control: AbstractControl

  12.  @Input() label: string;

  13. }

现在每一个控件都拥有自己的组件,通过input将把所需要的lable和contorl从父组件传送给它,使用这个组件后的模板代码:

  1. <div>

  2.  <app-single-control-component [control]="form.controls[firstName]" [label]="First Name">

  3.  </app-single-control-component>

  4.  <app-single-control-component [control]="form.controls[lastName]" [label]="Last Name">

  5.  </app-single-control-component>

  6.  <app-single-control-component [control]="form.controls[age]" [label]="Age">

  7.  </app-single-control-component>

  8. </div>

这个示例很简单,但类似的情况很多,如果都像这样的话,组件的数量马上会像黄河泛滥一样,一发而不可收拾。 另一种是无脑划分组件,比如一个包含文章和滚动区域的页面,滚动区域内还会展示各种各样的文章,可能划分不同的主题等,类似于Medium。

要创建这样一个页面,请先考虑一下怎么样划分组件才能让生活更美好?如果像这样:

红色区域代表一个组件,它里面有一些相同类型的文章以及头部的标题,tag等。绿色的区域也代表一个组件,它代表的是某一篇文章,包含这篇文章的标题,概述、作者等信息。但是这样的划分如何帮助我们分离逻辑?代码如何复用?如何以后某个地方发生了改变,真的利于管理么?

你可能觉得组件的划分是一个简单的事情,拿到页面后我自然知道怎么去划分,有什么可说的?但实际情况是,你可能被angular的路由骗了——路由是映射到组件上的,所以自然想到按照页面分离组件,这么想的可能不只新手,甚至包括一些有经验的开发者。But,angular组件并不是页面,它仅仅是视图片段,各种各样的片段组合在一起才拼凑出了一个页面。

使用.toPromise()

Angualr有自己的HTTP模块,使用它来和服务器进行通信,你肯定知道Angular使用Rxjs来支撑HTTP请求而不是Promise。然而并不是所有人都知道Rxjs是啥东西,尤其是对于刚使用Angular的开发人员来说,一旦发现toPromise操作符以后,就像抓住了救命稻草,不是返回Observable么,管它三七二十一,老子一率toPromise。Why?这种问题你最好别问,真正的答案只有一个,熟悉呗!4个2带俩王,说的就是这种人,只知道能快点走完,不知道捏了2个炸弹,归根结底,就是一个字,懒!

  1. 增加无谓逻辑。除非一些第三方API明确要求只接受Promise,否则Promise能做的Observable可以做的更好。


  2. 无法使用Rxjs带的好处。本来可以很方便的缓存响应,修改数据,发起重试,捕获错误等等,可偏偏有人要把它转成Promise,2个字,呵呵!


不用或不敢用Rxjs

Rxjs是非常通用的一种数据处理工具,只有不断的尝试去用,你才能发现它的惊艳之处。不管是维护数据,响应事件,甚至在管理某些状态时,Rxjs都可以做的很好。

把Directive抛到脑后

AngularJs里指令是老大,啥东西都是指令,什么ng-click,ng-src,几乎没有不是指令的。Angular中很多都被Input和Output取代了,但还是保留了一些的,比如ngIf,ngFor。

AngularJs的经验是:

不要在controller中操作DOM

Angular的经验是:

不要在component中操作DOM

有了新欢不要忘了旧爱,如果有需要,请记得指令一直都在。

从来不用interface

这个请求从服务器获取到的数据类型是什么?这个函数返回的数据类型是什么?any,any,any!any一时爽,重构火葬场。如果你无法确定响应类型,请‘厚颜无耻’的问后端小伙伴,直到有一天他(她)主动告诉你xxx接口返回的类型。人同此心,心同此理,你也不要等着你的小伙伴一直来问你这个方法到底返回啥,因为你也会烦。一旦全是any,在未来的某一天,你和代码的对话可能是这样的:

你:“这个地方返回的是东西?” 代码:“你猜?“

那你是猜还是不猜呢?还是你反问它:“你猜我猜不猜?”。如果不猜,请先把代码跑起来,一顿断点、一顿console,或者文档拿来一顿翻(如果文档更新或一不小心写错了,你还不敢确定跑起来结果是不是真的和文档一致)。如果猜,那我告诉你代码的心思你别猜,直到有一天bug出现了,你还是要重复上面的步骤。生命如些美好,何必这样折腾?

所以,不要说Typescript是负担,真正的负担是自己。

把数据操作写在组件中

这个问题有点棘手,不太建议写在service里,service中一般是用来处理请求、响应,在不同的component中共享数据,或者提供一些公共方法等。对于组件中的数据操作比较理想的是放到一个单独的class中。比如下面这段代码:

  1. nterface Movie {

  2.  id: number;

  3.  title: string;

  4. }

  5. @Component({

  6.  selector: app-some-component-with-form,

  7.  template: `...` // our form is here

  8. })

  9. export class SomeComponentWithForm {

  10.  public form: FormGroup;

  11.  public movies: Array<Movie>

  12.  constructor(private formBuilder: FormBuilder){

  13.    this.form = formBuilder.group({

  14.      firstName: [, Validators.required],

  15.      lastName: [, Validators.required],

  16.      age: [, Validators.max(120)],

  17.      favoriteMovies: [[]], /*

  18.                well have a multiselect dropdown

  19.                in our template to select favorite movies

  20.                */

  21.    });

  22.  }

  23.  public onSubmit(values){

  24.    /*

  25.      values is actually a form value, which represents a user

  26.      but imagine our API does not expect as to send a list of movie

  27.      objects, just  a list of id-s, so we have to map the values

  28.    */

  29.    values.favouriteMovies = values.favouriteMovies.map((movie: Movie) => movie.id);

  30.    // then we will send the user data to the server using some service

  31.  }

  32. }

目前看起来没啥大问题,但是想像一下,如果有很多外键,多对多的字段,大量的数据处理或者还依赖于应用的某些状态时,这个submit方法将变的一团糟,那么可以考虑这样做:

  1. nterface Movie {

  2.  id: number;

  3.  title: string;

  4. }

  5. interface User {

  6.  firstName: string;

  7.  lastName: string;

  8.  age: number;

  9.  favoriteMovies: Array<Movie | number>;

  10.  /*

  11.   notice how we supposed that this property

  12.   may be either an Array of Movie objects

  13.   or of numerical identificators

  14.  */

  15. }

  16. class UserModel implements User {

  17.  firstName: string;

  18.  lastName: string;

  19.  age: number;

  20.  favoriteMovies: Array<Movie | number>;

  21.  constructor(source: User){

  22.    this.firstName = source.firstName;

  23.    this.lastName = source.lastName;

  24.    this.age = source.age;

  25.    this.favoriteMovies = source.favoriteMovies.map((movie: Movie) => movie.id);

  26.    /*

  27.     we moved the data manipulation to this separate class,

  28.     which is also a valid representation of a User model,

  29.     so no unnecessary clutter here

  30.    */

  31.  }

  32. }

这里使用了一个Class来代表User,把数据操作移入它的constructor里。那么component的代码就可以变的更简洁:

  1. @Component({

  2.  selector: app-some-component-with-form,

  3.  template: `...` // our form is here

  4. })

  5. export class SomeComponentWithForm {

  6.  public form: FormGroup;

  7.  public movies: Array<Movie>

  8.  constructor(private formBuilder: FormBuilder){

  9.    this.form = formBuilder.group({

  10.      firstName: [, Validators.required],

  11.      lastName: [, Validators.required],

  12.      age: [, Validators.max(120)],

  13.      favoriteMovies: [[]], /*

  14.                well have a multiselect dropdown

  15.                in our template to select favorite movies

  16.                */

  17.    });

  18.  }

  19.  public onSubmit(values: User){

  20.    /*

  21.      now we will just create a new User instance from our form,

  22.      with all the data manipulations done inside the constructor

  23.    */

  24.    let user: UserModel = new UserModel(values);

  25.    // then we will send the user model data to the server using some service

  26.  }

  27. }

需要的数据操作都被移入的UserModel这个类中,每次发送数据的时候也很清楚是要new一个User。

不用或者滥用Pipe

这里使用一个示例也许能解释的清楚点。假设有2个下拉框让用用户选择重量,一个直接显示单位,另一个在显示的时候需要加一个价格,并且价格和单位之间需要一个’/‘来分割,最终以‘1 dollar/kg’这种方式显示。

  1. @Component({

  2.  selector: some-component,

  3.  template: `

  4.    <div>

  5.      <dropdown-component [options]="weightUnits"></dropdown-component>

  6.      <-- This will render a dropdown based in the options -->

  7.      <input type="text" placeholder="Price">

  8.      <dropdown-component [options]="weightUnits"></dropdown-component>

  9.      <-- We need to make this ones labels to be preceded with a slash -->

  10.    </div>

  11. `

  12. })

  13. export class SomeComponent {

  14.  public weghtUnits = [{value: 1, label: kg}, {value: 2, label: oz}];

  15. }

现在用了两个组件,数据源也一样,所以看起来也应该差不多。当然可以用一个很愚蠢的方式把它们区分出来:

  1. @Component({

  2.  selector: some-component,

  3.  template: `

  4.    <div>

  5.      <dropdown-component [options]="weightUnits"></dropdown-component>

  6.      <input type="text" placeholder="Price">

  7.      <dropdown-component [options]="slashedWeightUnits"></dropdown-component>

  8.      <-- Now this ones labels  will be preceded with a slash -->

  9.    </div>

  10. `

  11. })

  12. export class SomeComponent {

  13.  public weightUnits = [{value: 1, label: kg}, {value: 2, label: oz}];

  14.  public slashedWeightUnits = [{value: 1, label: /kg}, {value: 2, label: /oz}];

  15.  // we just add a new property

  16. }

这样问题是解决了,但是组件上的数据如果不是写死的,而是从其它地方获取过来的(比如通过http请求),怎么办?当然可以继续用恶心的办法搞定:

  1. @Component({

  2.  selector: some-component,

  3.  template: `

  4.    <div>

  5.      <dropdown-component [options]="weightUnits"></dropdown-component>

  6.      <input type="text" placeholder="Price">

  7.      <dropdown-component [options]="slashedWeightUnits"></dropdown-component>

  8.      <-- Now this ones labels  will be preceded with a slash -->

  9.    </div>

  10. `

  11. })

  12. export class SomeComponent {

  13.  public weightUnits = [{value: 1, label: kg}, {value: 2, label: oz}];

  14.  public get slashedWeightUnits() {

  15.    return this.weightUnits.map(weightUnit => {

  16.      return {

  17.        label: / + weightUnit.label,

  18.        value: weightUnit.value

  19.      };

  20.    })

  21.  }

  22.  // so now we map existing weight units to a new array

  23. }

看起来不错,实际更糟糕!当点击它,甚至都不用点击,你会发现下拉框会闪烁。Why?要解释这个问题可能需要深入了解Angular的变更检测是如何作用于Input和Output上的。

dropdown组件有一个Input属性,每当输入的值发生变化时,这个组件都会重新被渲染。在这里,它的值是通过函数调用来产生的,所以变更检测机制无法确定值是否发生了变化,在每次检测发生时都要重新调用这个函数,下拉框自然就会被刷新。所以,问题看起来被解决了,其实是制造了一个更大的问题。

较好的解决方案:

  1. @Pipe({

  2.  name: slashed

  3. })

  4. export class Slashed implements PipeTransform {

  5.  transform(value){

  6.    return value.map(item => {

  7.      return {

  8.        label: / + item.label,

  9.        value: item.value

  10.      };

  11.    })

  12.  }

  13. }

  14. @Component({

  15.  selector: some-component,

  16.  template: `

  17.    <div>

  18.      <dropdown-component [options]="weightUnits"></dropdown-component>

  19.      <input type="text" placeholder="Price">

  20.      <dropdown-component [options]="(weightUnits | slashed)"></dropdown-component>

  21.      <-- This will do the job -->

  22.    </div>

  23. `

  24. })

  25. export class SomeComponent {

  26.  public weightUnits = [{value: 1, label: kg}, {value: 2, label: oz}];

  27.  // we will delegate the data transformation to a pipe

  28. }

这里使用了熟悉的pipes,只是官方文档说pipes适合于这种场景。其实想说的不是pipes,而是,这种方式有可能也不并不完美。如果应用中有很多这种场景,只是数据不一样,那是不是说遇到一个写一个Pipe?有些Pipe可能只是为了给某个组件中的特定场景使用,这样就会创建一大堆的Pipe。

更好的解决方案:

  1. @Pipe({

  2.  name: map

  3. })

  4. export class Mapping implements PipeTransform {

  5.  /*

  6.  this will be a universal pipe for array mappings. You may add more

  7.  type checkings and runtime checkings to make sure it works correctly everywhere

  8.  */

  9.  transform(value, mappingFunction: Function){

  10.    return mappingFunction(value)

  11.  }

  12. }

  13. @Component({

  14.  selector: some-component,

  15.  template: `

  16.    <div>

  17.      <dropdown-component [options]="weightUnits"></dropdown-component>

  18.      <input type="text" placeholder="Price">

  19.      <dropdown-component [options]="(weightUnits | map : slashed)"></dropdown-component>

  20.      <-- This will do the job -->

  21.    </div>

  22. `

  23. })

  24. export class SomeComponent {

  25.  public weightUnits = [{value: 1, label: kg}, {value: 2, label: oz}];

  26.  public slashed(units){

  27.    return units.map(unit => {

  28.      return {

  29.        label: / + unit.label,

  30.        value: unit.value

  31.      };

  32.    });

  33.  }

  34.  // we will delegate a custom mapping function to a more generic pipe, which will just call it on value change

  35. }

有什么不同呢?Pipe上的transform函数只在数据变化的时候才调用,如果weightUnits没有变化,Pipe仅仅只会被调用一次,而不是每次变更检测发生时都运行。

这里要表达的不是说你只能定义1个或者2个Pipe,而是你定义的Pipe应该是去处理一些更加通用或复杂的事情。典型的例子是Angular内置处理日期的Pipe,它可没有针对每一种日期类型都定义一个Pipe。换句话说,pipe的逻辑复用是关键,而不是针对一个具体的情况就写一个Pipe。

注意:如果你要给Pipe传函数的话,请确保这个函数一定是一个纯函数,千万不要让函数受到外部状态的干扰!

关于重用

如果你要写一个组件给其它开发人员使用,一定要检查这个组件需要的所有条件和数据。如果组件需要的参数类型是一个泛型,最起码你要检查这个参数是不是传进来了,因为代码运行的时候它可能是undefined(ts编译器只能在编译时检查出错误)。在你的组件中捕获和抛出这种错误,你可以很快的定位到问题,一旦让zone.js或者angular捕获后抛出一个错误,错误信息可能会让你一脸懵逼。

总的来说,多点细心和耐心,你会发现很多麻烦的事情其实在一开始的时候就可以避免掉的,那些看起来没有意义的工作实际是为了生活更加美好!

原文地址


    发送中

    上一篇: ASP.NET Core MVC 源码学习:Routi..

    下一篇: 腾讯开源大规模 Node.js 微服务框..

    相关文档推荐

    精品模板推荐

     2020-07-29   18166  0金币下载

     2020-07-27   65338  0金币下载

     2020-07-27   65333  0金币下载

     2020-06-22   57995  0金币下载

     2020-06-13   62585  0金币下载

     2020-06-13   62587  0金币下载

    专业的织梦模板定制下载站,在线购买后即可下载!

    商业源码

    跟版网模板,累计帮助5000+客户企业成功建站,为草根创业提供助力!

    立刻开启你的建站之旅
    
    QQ在线客服

    服务热线

    织梦建站咨询