Angular 开发经验总结 (2024年9月27日)

248 阅读13分钟

最近使用 Angular 完成了一个复杂的前端功能模块,设计表单和元素拖拽,现总结完成此模块的时候遇到的一些问题以及解决方案还有知识总结。

  1. 关于一个模块下面创建多个组件的问题
  2. 声明一个受控的表单组
  3. 在父组件中操作子组件
  4. 更加快捷的路由参数构建过程
  5. Angular 中实现列表拖拽
  6. 子组件向外发送事件并携带 string 类型的数据
  7. Tooltip 效果以及容易混淆的点
  8. 三大框架对于 props 值发生改变时候的处理 -- React、Angular和Vue监视输入变化的对比
  9. 一种较好的 toolbar 悬浮实现方式
  10. 自定义单选按钮的外观
  11. 正确设置单选项的示意
  12. 容易搞错的点 -- 关于双向数据绑定
  13. Angular 和 AngularJS 的区别

1. 关于一个模块下面创建多个组件的问题

【模块提供编译条件】,【组件只管运行】,组件能否被正常渲染的条件就是【声明它的模块有没有提供对应的编译条件】。因此我们可以直接在一个模块下面【创建多个组件】,然后在这个模块中 declaration 这些组件,这样这些组件就【都能编译通过】了。

  1. 模块提供编译条件:模块通过声明组件、指令、管道和服务,为Angular编译器提供必要的信息,以便在编译时知道如何处理这些类。

  2. 组件只管运行:组件负责定义视图和处理用户交互,运行时的逻辑,不涉及编译过程。

  3. 声明它的模块有没有提供对应的编译条件:组件需要在其声明的模块中被正确配置,包括导入必要的模块和声明服务,才能保证编译时无误。

  4. 创建多个组件:可以在同一个模块中声明多个组件,这样可以方便地管理这些组件的编译和依赖。

  5. declaration 这些组件:在模块的declarations数组中声明这些组件,确保它们被Angular编译器识别。

  6. 都能编译通过:只要模块提供了正确的编译条件,声明在其中的组件都能被Angular编译器正确处理,从而顺利编译通过。

2. 声明一个受控的表单组

使用FormGroupFormControl来创建一个受控表单。FormGroup是一个包含表单控件的组,FormControl是表单控件的单个实例。

contactForm: FormGroup = new FormGroup({
  description: new FormControl(''), // 初始值为空字符串
  title: new FormControl(), // 初始值为空
});

在表单标签上绑定表单组

  • 使用[formGroup]属性将表单控件绑定到模板中的<form>标签。
<form [formGroup]="contactForm">
  <!-- 表单元素 -->
</form>

在表单元素标签上绑定表单控件

  • 使用formControlName属性将表单元素绑定到对应的FormControl
<input formControlName="description">

获取当前的表单值

  • 通过访问FormGroupvalue属性来获取表单的当前值。
console.log(this.contactForm.value); // 获取表单值

手动设置表单中元素值

  • 使用FormControlsetValue方法来手动设置表单控件的值。
this.contactForm.get('description').setValue(description ?? null); // 设置'description'字段的值

这里description ?? null使用了空值合并运算符??,意味着如果descriptionundefinednull,则使用null作为默认值。

3. 在父组件中操作子组件

通过 ViewChildren 的方式获取父组件中的所有 特定子组件SectionComponent)组成的类列表:

@ViewChildren(SectionComponent) sections!: QueryList<SectionComponent>;

类列表 没有map等方法,但是其元素值就是子组件实例,可以直接调用其中的方法或者获取/改变属性值。

获取所有子组件的数据

getAllTableData() {
  let rst = [];
  this.sections.forEach(section => {
    const questions = [];
    section.questions.forEach(question => {
      questions.push(question.contactForm.value);
    })
    const sectionData = {
      ...section.contactForm.value,
      questions,
    }
    rst.push(sectionData);
  })

  return rst;
}

其中 sections 是所有的 SectionComponent 子组件构成的 QueryList 使用 forEach 来遍历 这个类列表,然后得到 每一个子组件实例 section,而 questions则是实例上的属性,我们可以 操作 它。

这里的 ViewChildren 是Angular提供的一个装饰器,用于在父组件中查询子组件。QueryList 是一个包含子组件实例的列表,它提供了一种方式来访问这些子组件。

forEach 是一个数组方法,用于遍历数组中的每个元素。在这个例子中,它被用来遍历所有的 SectionComponent 实例。

questions 是子组件实例上的一个属性,它是一个数组,包含了子组件中的问题。通过遍历这个数组,我们可以获取每个问题的数据。

最后,sectionData 是一个对象,它包含了子组件的表单数据和问题数据。这个对象被添加到结果数组 rst 中,最终返回这个数组。

4. 更加快捷的路由参数构建过程

Angular中使用NavigationExtras接口来简化路由跳转过程中的数据传递问题。

  1. 构建满足NavigationExtras接口的数据结构
    • NavigationExtras接口允许你配置路由跳转时的一些额外参数,如查询参数(queryParams)、片段标识符(fragment)等。
const navigationExtras: NavigationExtras = {
  queryParams: { previewData: JSON.stringify(previewData) },
  fragment: 'top'
};
  1. 简化路由跳转过程中的数据传递

    • 通过queryParams属性传递一个对象,对象的键值对会被转换为URL的查询参数。
    • 通过fragment属性指定跳转后页面的片段标识符。
  2. 执行路由跳转

    • 使用router.navigate方法进行路由跳转,并传入目标路由路径和NavigationExtras对象。
this.router.navigate(['/checklisttmp/' + this.id + '/preview'], navigationExtras);

详细步骤

  1. 定义导航额外参数
    • 创建一个对象,该对象满足NavigationExtras接口。
    • 使用queryParams属性传递查询参数,将previewData对象转换为JSON字符串。
    • 使用fragment属性指定页面跳转后的锚点位置。
const navigationExtras: NavigationExtras = {
  queryParams: { previewData: JSON.stringify(previewData) },
  fragment: 'top'
};
  1. 执行路由跳转
    • 使用this.router.navigate方法进行路由跳转。
    • 第一个参数是目标路由路径,这里使用字符串插值动态拼接路径。
    • 第二个参数是navigationExtras对象,包含了额外的导航参数。
this.router.navigate(['/checklisttmp/' + this.id + '/preview'], navigationExtras);

总结

通过构建满足NavigationExtras接口的数据结构,可以简化路由跳转过程中的数据传递问题。通过queryParamsfragment属性,可以方便地传递数据和指定页面跳转后的锚点位置。

这样,目标路由页面可以通过解析URL的查询参数来获取传递的数据,从而实现数据的传递和页面的跳转。

5. Angular 中实现列表拖拽

Angular CDK(Component DevKit)中的拖拽功能,它提供了一套API来简化拖拽列表的实现。

  1. 使用CDK提供的API构建拖拽组件列表
    • Angular CDK提供了一套拖拽和排序的API,使得实现可拖拽列表变得简单。
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';

drop(event: CdkDragDrop<string[]>) {
  moveItemInArray(this.items, event.previousIndex, event.currentIndex);
}
  1. 在模板中使用cdkDrag指令
    • 使用*ngFor指令遍历items数组,并为每个元素添加cdkDrag指令,使其可拖拽。
<div
  *ngFor="let item of items; let i = index"
  cdkDrag
  class="template-list-wrapper"
>
  <div cdkDragHandle class="drag-handle">
    <img src="./drag-handle.svg" />
  </div>
</div>
  1. 响应式更新列表
    • 当拖拽事件发生时,this.items数组会响应式地更新,例如使用splice方法或者对其重新赋值都会刷新可拖拽列表。
    • 拖拽之后,this.items会自动调整元素的顺序。

通过使用Angular CDK提供的API,可以方便地构建可拖拽的组件列表。cdkDrag指令和drop事件处理函数使得实现拖拽列表变得简单。this.items数组的响应式更新确保了列表元素在拖拽后能够自动调整顺序。

6. 子组件向外发送事件并携带 string 类型的数据

子组件向外发送事件确保发送的数据是string类型。

定义事件

使用@Output()装饰器定义了一个名为duplicateEvent的事件,这个事件使用EventEmitter来触发。

@Output() duplicateEvent = new EventEmitter<string>();

发送事件

在子组件中定义了一个duplicate方法,这个方法触发duplicateEvent事件,并发送一个string类型的数据。

duplicate() {
  this.duplicateEvent.emit((this.sort - 1).toString());
}

完整的组件示例

import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <button (click)="duplicate()">Duplicate</button>
  `
})
export class ChildComponent {
  @Output() duplicateEvent = new EventEmitter<string>();
  sort: number = 2; // 示例数据

  duplicate() {
    this.duplicateEvent.emit((this.sort - 1).toString());
  }
}

父组件中监听事件

在父组件中,你可以监听这个事件并处理接收到的数据:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-child (duplicateEvent)="handleDuplicate($event)"></app-child>
  `
})
export class AppComponent {
  handleDuplicate(data: string) {
    console.log('Received data:', data);
  }
}

解释

  1. 定义事件

    • 使用@Output()装饰器定义一个名为duplicateEvent的事件。
    • 使用泛型<string>确保事件发送的数据是string类型。
  2. 触发事件

    • duplicate方法中,使用(this.sort - 1).toString()将数字转换为string类型,并触发事件。
  3. 父组件监听事件

    • 在父组件的模板中,使用(duplicateEvent)语法监听子组件的事件。
    • 使用handleDuplicate方法处理接收到的数据。

通过这种方式,子组件可以向外发送事件,并确保发送的数据是string类型。父组件可以监听这个事件并处理接收到的数据。这是一种常见的父子组件通信方式。

7. Tooltip 效果以及容易混淆的点

实现提示框(tooltip)功能的是title属性,而不是tooltip属性title属性在HTML中用于提供有关元素的额外信息,当用户将鼠标悬停在元素上时,这些信息会显示为一个提示框。

以下是如何在Angular中使用title属性来实现tooltip功能的一个简单示例:

HTML模板中使用title属性

<button title="Click to duplicate" (click)="duplicate()">Duplicate</button>

在这个例子中,当用户将鼠标悬停在按钮上时,会显示"Click to duplicate"这个提示信息。

使用Angular Material的Tooltip组件

如果你想要更丰富的提示框功能,比如自定义样式、位置等,你可以使用Angular Material库中的MatTooltip组件。

首先,确保你已经安装了Angular Material:

ng add @angular/material

然后,在你的组件中导入MatTooltipModule

import { MatTooltipModule } from '@angular/material/tooltip';

@NgModule({
  imports: [
    // ...
    MatTooltipModule
  ],
  // ...
})
export class AppModule { }

在你的模板中使用matTooltipmatTooltipPosition属性:

<button mat-button [matTooltip]="tooltipText" [matTooltipPosition]="'above'">
  Duplicate
</button>

在你的组件中定义tooltipText

export class YourComponent {
  tooltipText = 'Click to duplicate';
}

解释

  1. HTML title属性:这是一个简单的HTML属性,用于提供基本的提示框功能。

  2. Angular Material MatTooltip:这是一个更强大的提示框组件,提供了丰富的API来自定义提示框的行为和样式。

  • 使用HTML的title属性可以快速实现简单的提示框功能。
  • 使用Angular Material的MatTooltip组件可以提供更丰富的提示框功能和更好的用户体验。

8. 三大框架对于 props 值发生改变时候的处理 -- React、Angular和Vue监视输入变化的对比

你提到的是Angular中的ngOnChanges生命周期钩子,它用于监视组件输入属性的变化。

  1. React
    • 在React中,通常通过比较propscomponentDidUpdate生命周期方法或使用useEffect钩子来监视props的变化。
componentDidUpdate(prevProps) {
  if (prevProps.sort !== this.props.sort) {
    console.log('Sort prop changed:', this.props.sort);
  }
}

// 或者使用hooks
useEffect(() => {
  console.log('Sort prop changed:', props.sort);
}, [props.sort]);
  1. Angular
    • 在Angular中,通过实现OnChanges接口并使用ngOnChanges生命周期钩子来监视输入属性的变化。
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  // ...
})
export class YourComponent implements OnChanges {
  @Input() sort: number;

  ngOnChanges(changes: SimpleChanges) {
    if (changes.sort) {
      const newSortValue = changes.sort.currentValue;
      this.contactForm.get('id').setValue(`${newSortValue}`);
    }
  }
}
  1. Vue
    • 在Vue中,可以在watch选项中指定监视的属性,并定义一个函数来响应变化。
<script>
export default {
  props: ['sort'],
  watch: {
    sort(newVal, oldVal) {
      console.log('Sort prop changed:', newVal);
    }
  }
}
</script>

Angular的OnChanges接口

在Angular中,OnChanges接口定义了一个ngOnChanges方法,该方法接收一个SimpleChanges对象,该对象包含了变化的输入属性信息。

import { OnChanges, SimpleChanges } from '@angular/core';

export class YourComponent implements OnChanges {
  @Input() sort: number;

  ngOnChanges(changes: SimpleChanges) {
    // ...
  }
}

SimpleChanges对象是一个包含了所有变化的输入属性的映射,每个属性都有一个SimpleChange对象,该对象包含previousValuecurrentValuefirstChange等属性。

小结

  • React通过componentDidUpdateuseEffect来监视props的变化。
  • Angular通过实现OnChanges接口并使用ngOnChanges生命周期钩子来监视输入属性的变化。
  • Vue通过watch选项来监视属性的变化。
  • 在Angular中,ngOnChanges方法用于响应输入属性的变化,它是OnChanges接口的一部分。

附加项 -- 为什么需要监控 props 的变化

在React中,当props传递给组件时,组件可以使用这些props来渲染UI或控制行为。如果props发生变化,React的渲染系统会自动重新渲染组件,因此通常不需要像在Angular或Vue中那样显式监视props的变化。

React中处理Props变化的方式:

  1. 直接使用Props:

    • 组件可以直接使用props来执行渲染逻辑。
    function MyComponent(props) {
      return <div>{props.message}</div>;
    }
    
  2. 使用Hooks响应Props变化:

    • 如果需要在组件内部基于props的变化执行副作用(如数据获取、订阅等),可以使用useEffect钩子。
    import React, { useEffect } from 'react';
    
    function MyComponent(props) {
      useEffect(() => {
        console.log('Message prop changed:', props.message);
        // 可以在这里执行基于props变化的逻辑
      }, [props.message]); // 依赖数组中的props.message变化时触发
    
      return <div>{props.message}</div>;
    }
    
  3. 使用Memoization优化性能:

    • 如果组件基于props进行昂贵的计算,可以使用React.memouseMemo来避免不必要的计算。
    import React, { useMemo } from 'react';
    
    function MyComponent(props) {
      const computedValue = useMemo(() => {
        // 昂贵的计算
        return computeExpensiveValue(props.input);
      }, [props.input]); // 当props.input变化时重新计算
    
      return <div>{computedValue}</div>;
    }
    

总结:

  • 在React中,通常不需要显式监视props的变化,因为props的变化会自动触发组件的重新渲染。
  • 使用useEffect钩子可以在props变化时执行副作用,这类似于监视props的变化。
  • React.memouseMemo可以用来优化性能,避免不必要的渲染或计算。

9. 一种较好的 toolbar 悬浮实现方式

下面的代码实现一个功能,使得当鼠标悬浮在.container上时,与之相关的.toolbars会显示出来,而且通过设置padding使得.container.toolbars部分重叠,这样即使鼠标从.container移动到.toolbars上,.toolbars也不会消失。

HTML结构

<div class="container-fluid pr template-section-container" style="width: 944px">
  <div class="template-header"></div>
  <div class="section-toolbars-wrapper">
    <div class="section-toolbars">
      <!-- 工具栏内容 -->
    </div>
  </div>
</div>

CSS样式

.template-section-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0;
  position: relative;
}

.template-header {
  /* 根据需要设置样式 */
}

.section-toolbars-wrapper {
  width: 70px;
  padding-left: 20px;
  height: 160px;
  display: none;
  position: absolute;
  right: -60px;
}

.section-toolbars {
  padding: 8px 0;
  height: 100%;
  background: #fff;
  border: 1px solid #00000042;
  border-radius: 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.container-fluid:hover .section-toolbars-wrapper {
  display: block;
}

.section-toolbars-wrapper:hover .section-toolbars {
  display: block;
}

解释

  1. HTML结构

    • .template-section-container是主容器。
    • .template-header是容器的头部。
    • .section-toolbars-wrapper包含.section-toolbars,是工具栏的外层容器。
  2. CSS样式

    • .template-section-container设置为flex布局,定位为relative,以确保.section-toolbars-wrapper可以相对于它进行定位。
    • .section-toolbars-wrapper使用absolute定位,设置display: none,默认不显示。
    • .section-toolbars设置了背景、边框和圆角,以及flex布局,使得内部项目垂直居中。
    • 使用.container-fluid:hover .section-toolbars-wrapper选择器,当.template-section-container被悬停时,显示.section-toolbars-wrapper
    • 使用.section-toolbars-wrapper:hover .section-toolbars选择器,当.section-toolbars-wrapper被悬停时,显示.section-toolbars

注意事项

  • 重叠区域.section-toolbars-wrapperpadding-left设置为20px,使得与.template-section-container有重叠区域,这样即使鼠标从.template-section-container移动到.section-toolbars-wrapper上,由于重叠,.section-toolbars-wrapper:hover状态不会消失,从而保持显示状态

通过这种方式,可以实现当鼠标悬浮在.container上时显示.toolbars,并且通过设置padding使得.container.toolbars部分重叠,这样即使鼠标从.container移动到.toolbars上,.toolbars也不会消失。

10. 自定义单选按钮的外观

自定义单选按钮(radio button)的外观,包括修改其大小以及选中时的颜色。

CSS样式

/* 自定义单选按钮的大小 */
input[type="radio"] {
  -webkit-appearance: none; /* 移除默认样式 */
  appearance: none;
  width: 20px; /* 设置宽度 */
  height: 20px; /* 设置高度 */
  border-radius: 50%; /* 设置圆形 */
  border: 2px solid #ccc; /* 设置边框 */
}

/* 自定义单选按钮选中时的颜色 */
input[type="radio"]:checked {
  accent-color: #3f51b5; /* 设置选中时的颜色 */
}

/* 自定义单选按钮的包装器 */
.choice {
  /* 根据需要设置样式 */
}

解释

  1. 自定义单选按钮的大小

    • 使用-webkit-appearance: none;appearance: none;移除原生的单选按钮样式。
    • 设置widthheight属性来定义单选按钮的大小。
    • 设置border-radius: 50%;来让单选按钮呈现圆形。
  2. 自定义单选按钮选中时的颜色

    • 使用accent-color属性来定义单选按钮选中时的圆点颜色。
  3. 自定义单选按钮的包装器

    • .choice类可以用于包装单选按钮,以便进行进一步的布局或样式设置。

注意事项

  • accent-color属性是一个较新的CSS属性,可能在某些浏览器中不受支持。如果需要更广泛的兼容性,可能需要使用其他方法(如自定义伪元素或背景图片)来实现选中时的效果。

示例HTML

<div class="choice">
  <input type="radio" id="option1" name="group" value="option1">
  <label for="option1">Option 1</label>
  
  <input type="radio" id="option2" name="group" value="option2">
  <label for="option2">Option 2</label>
</div>

通过这种方式,你可以自定义单选按钮的外观,包括修改其大小和选中时的颜色,以更好地融入你的网页设计。

11. 正确设置单选项的示意

  1. 当你想要设置一组单选按钮(radio buttons)的时候,确保它们具有相同的name属性,这样浏览器会将它们分组,一次只能选择其中一个

  2. 同时,给每个单选按钮设置一个唯一的id,以及一个value,这样在表单提交时,可以选择具有特定值的按钮。

  3. 对应的label元素通过for属性与单选按钮的id关联起来,增强可访问性。

示意代码:

HTML结构

<div class="choice" *ngFor="let option of options; let i = index">
  <input
    type="radio"
    [id]="'checklist-' + id + '.' + subId + '.' + optionId"
    [name]="'checklist-' + id + '.' + subId"
    [value]="'checklist-' + id + '.' + subId + '.' + optionId"
    [(ngModel)]="selectedOption"
  />
  <label
    [for]="'checklist-' + id + '.' + subId + '.' + optionId"
  >{{ option.value }}</label>
</div>

12. 容易搞错的点 -- 关于双向数据绑定

在使用 Angular 的时候 () 表示监听某个事件,而 [] 表示属性值为变量,非常容易混淆这两种不同的括号。相比较而言,虽然 Vue 抄袭了 Angular 但是它使用 :@ 就很不容易搞混。

13. Angular 和 AngularJS 的区别

Angular和AngularJS是两个不同的前端JavaScript框架:

  1. AngularJS(也称为Angular 1.x)是一个早期的框架,使用双向数据绑定和扩展HTML的声明式模板。

  2. Angular(也称为Angular 2+)是一个完全不同的重写版本,采用TypeScript开发,引入了组件架构、服务、依赖注入等现代Web开发特性。

简而言之,Angular 是 AngularJS 的后续版本,提供了更先进的功能和更好的性能。