angular 指令渲染_Angular-Material-Design 图表实现(一)

本文介绍了如何使用Angular 9和Angular Material Design构建数据表格,包括依赖包配置、Material Data Table的使用、添加Material Design效果、响应式设计以及自定义CDK Data Source的实现,强调了数据表与数据源的解耦和响应式设计的重要性。
摘要由CSDN通过智能技术生成

a2302bd3f56303075ee20f1e7842e65d.png

前言

考虑再三还是给自己的专栏加一些前端展示的文章,因为数据科学最终终究是要落地的,这要求我们技术人员(非数据科学家)不仅需要懂 AI 算法,也要有着过硬的工程实践,即知识宽度。当然如果你是数据科学家,你只需要在自己的领域不断深入就可以了,但做工程除了一定的深度外,也要兼顾自己技术栈的宽度,最好能达到一个人能够从头到尾搞定一个项目。

由于数据类产品总是离不开数据展示这一重要环节,目前工业领域应用最广的前端框架无非就是 Angular,Vue 和 React。Angular 比较像前端中的 Java,尽管看似笨重冗余,但在大项目中的可复制性、延展性(Scalability)之强是后两者无法相比的。比如最近前端领域在微服务大火的年代有 Customer Element 的概念,用这种方法你可以将一个大型项目拆解成许多 Microservice 从而降低大项目在架构和扩展上的难度。后两者对于 Customer Element 的支持很有限,目前经我司前端人员亲测后,发现后者尚存大量深坑,不建议没有源码级的公司在项目中使用。

本文会使用 Angular 9 利用 Material Design 写个前端小程序用于展示表格类数据。本文会参照Angular University 中的例子做部分调整和个人解读,大家也可以直接阅读原文。

涉及到的关键技术有:

  • Angular Material Data Table Reactive Design
  • Material Paginator 与服务端分页
  • Sortable Headers
  • Custom Angular Material CDK Data Source

在叙述上本文假设读者已经具备 Angular 初级水平,比如:懂得怎么创建新项目、下载依赖包等。

* 达到这个水平大概要 4 到 8 小时左右的学习,具体可参见Tour of Heroes。

1 依赖包配置

由于本人用的是最新 Angular 9 的版本,包的引用有可能会与旧版本有不一样的地方:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { MatPaginatorModule } from "@angular/material/paginator";
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { BrowserModule } from '@angular/platform-browser';

@NgModule({
  declarations: [...省略],
  imports: [
    BrowserModule,    
    BrowserAnimationsModule,
    ReactiveFormsModule,
    FormsModule,
    MatInputModule,
    MatTableModule,
    MatPaginatorModule,
    MatSortModule,
    MatProgressSpinnerModule,
    MatAutocompleteModule
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})

export class MyModule { }
  • MatInputModule: 包含了 Material Design 输入框需要的组件(Component)和指令(Directive),我们会在晚些用这个输入框作为搜索输入
  • MatTableModule: Material-Table 的核心部分,里面包括了比如 mat-tab 等组件和指令
  • MatPaginatorModule: 是一个通用的分页模块,就是说可以被用到没有 Material Design 的前端中
  • MatSortModule: 非必须包括的模块,主要给图表提供了排序的功能
  • MatProgressSpinnerModule: 可以给图表加载的时候增加一些美化效果

2 Angular Material Data Table 简介

Material Data Table 是一个通用的表格式展示组件,使用它可以很容易的给你的表格配置成带有 Material Design 风格的外观。原文中给了一个没有 Material Design 的例子:

<mat-table class="lessons-table mat-elevation-z8" [dataSource]="dataSource">
  
    <ng-container matColumnDef="seqNo">
        <div *matHeaderCellDef>#</div>
        <div *matCellDef="let lesson">{{lesson.seqNo}}</div>
    </ng-container>
  
    <ng-container matColumnDef="description">
        <div *matHeaderCellDef>Description</div>
        <div class="description-cell"
                  *matCellDef="let lesson">{{lesson.description}}</div>
    </ng-container>
  
    <ng-container matColumnDef="duration">
        <div *matHeaderCellDef>Duration</div>
        <div class="duration-cell"
                  *matCellDef="let lesson">{{lesson.duration}}</div>
    </ng-container>
    
    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    
    <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
    
</mat-table>

代码解读:

  • ng-container:本身不会渲染任何内容,但是会为 Angular mat-table 的元素提供一个能够接收 matColumnDef 指令的元素
  • matColumnDef 指令:用于声明/指派给 列(Column)一个 Key(可以理解成列名的 ID,Key 不能重复)作为标识

这两个元素以及 mat-table 可以单独来看,因为他们凌驾于其他指令和组件之上,且为他们提供了一个所谓的载体,或者也可以理解为一个外部框架,用于做一些基本宏观上的定义。

Material Data Table 有一套辅助型的指令,通常都是以 *directiveName 为规则的。这些指令在我们设计表结构和内容实现的时候会十分方便。辅助型的指令通常命名规范都是以 Def 为结尾的,且这种指令通常都是给模板的某个部分定义特定的角色而存在的,比如:matHeaderCellDef, matCellDef 等,我们会在接下来的章节中仔细研究一下。

3 给 Data Table 加 Material Design 效果

继续沿用上面的设计,做一些细微的调整:

<mat-table class="lessons-table mat-elevation-z8" [dataSource]="dataSource">

    <ng-container matColumnDef="seqNo">
        <mat-header-cell *matHeaderCellDef>#</mat-header-cell>
        <mat-cell *matCellDef="let lesson">{{lesson.seqNo}}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="description">
        <mat-header-cell *matHeaderCellDef>Description</mat-header-cell>
        <mat-cell class="description-cell"
                  *matCellDef="let lesson">{{lesson.description}}</mat-cell>

    </ng-container>

    <ng-container matColumnDef="duration">
        <mat-header-cell *matHeaderCellDef>Duration</mat-header-cell>
        <mat-cell class="duration-cell"
                  *matCellDef="let lesson">{{lesson.duration}}</mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>

    <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>

</mat-table>

这个配置中我们添加了 mat-header-cell 和 mat-cell 组件,这样就给普通的表格提供了渲染效果。

列配置指令:

在 ng-container 中可以对列做一系列定义配置比如:

  • matHeaderCellDef 指令: 定义列名(名字可随意定义不像 Key / ID)
  • mat-header-cell 组件: 给 Header 提供 mat 渲染效果
  • matCellDef 指令: 定义如何显示列中数据,且它会直接访问数据。在例子中 data table 展示了一个lessons 数组, let lesson 可以访问到数组中的每个 lesson 对象,我们可以在模板中像调用变量一样使用它
  • mat-cell 组件: 提供列 mat 效果
  • matHeaderRowDef 指令: 声明表头行,它可以定义表头中列名的先后顺序。比如例子中的
<mat-header-row *matHeaderRowDef="displayedColumns">

这个 displayedColumns 在组件 ts 代码中定义了,且与 matColumnDef 定义的 Key 名字必须一一对应!

displayedColumns = ["seqNo", "description", "duration"];

这样就通过数组 displayedColumns 中元素的顺序定义了表中列的先后排列顺序。

  • mat-header-row 组件: 该组件提供了基础的样式风格
  • matRowDef 指令:在 mat-table 中的元素提供定义了 data row 的外形,但是不包括 styling
  • mat-row 组件:使 matRowDef 定义的数据具有 Material Styling

通过观察我们可以发现,这里的每一个组件都对应一个指令指令主要定义了模板组件的功能,数据(比如:目标是 header 还是 cell),但是不是用来改变列样式(style)的指令。在上面的例子中他们的功能更像是个 div , 而组件则使指令选中的数据带有了 Material Desgin 的效果。

4 表数据的交互

Angular 等高端框架的牛逼之处就在于他们给开发人员提供了大量的可操作机会,且大多都通过配置的方式很容易地可以实现。

比如:通过 matRowDef 我们可以监听到 data row 是否有被鼠标点到的事件。在 html 中添加

<mat-row *matRowDef="let row; columns: displayedColumns" 
         (click)="onRowClicked(row)">
</mat-row>

在 ts 文件中添加相应的方法来处理这个点击事件:

onRowClicked(row) {
    console.log('Row clicked: ', row);
}

这样如果我们用鼠标去点击表中某一行的数据,就会触发到这个事件,并在 console log 中输出出来。

5 Data Source 与 Reactive Design

数据表中的数据在例子中是来自于基于 Observable API 的接口并且遵循着响应式设计的原则。也就是说数据表格本身不关系或者根本就不知道数据是哪里来的。数据源有可能是服务器后台,有可能是服务端等等,但他们最后都是会被表格用同样的方式地展现出来。

换个角度我们可以说,数据表订阅(subscribe)了一个 Observable 的动态数据源后,一旦这个 Observable 提供 / 发射了新的数据,这些数据就会体现到我们的数据表上。

5.1 数据表的核心设计理念

如上所说,这些基于 Observable 实现的数据表是不知道数据源是哪里来的,但是数据表也不知道是什么触发新的数据更新,这里我们列举一些可能触发更新数据的方法:

  1. 数据表初始化
  2. 用户点击分页键
  3. 用户点击了的表头的数据排序功能
  4. 用户输入搜索内容

不难看出,前端数据的更新是建立于用户行为的基础上的,而不是后端数据的更新驱动了前端数据展现的变化。注意:数据表的功能仅仅局限在如何展示数据上,而不是如何调取的数据!

5.2 如何设计响应式数据源

在设计上类似于 Java 代码,我们通常会把不同功能的服务单独抽取出来。比如例子中用了 LessonsService,这是一个标准的 Observable 的无状态、单例服务,在底层使用了 Angular 的 HTTP Client。

@Injectable()
export class CoursesService {

    constructor(private http:HttpClient) {}

    findLessons(
        courseId:number, filter = '', sortOrder = 'asc',
        pageNumber = 0, pageSize = 3):  Observable<Lesson[]> {

        return this.http.get('/api/lessons', {
            params: new HttpParams()
                .set('courseId', courseId.toString())
                .set('filter', filter)
                .set('sortOrder', sortOrder)
                .set('pageNumber', pageNumber.toString())
                .set('pageSize', pageSize.toString())
        }).pipe(
            map(res =>  res["payload"])
        );
    }
}

这个 HttpClient API 是被自动装载的(类似 Java Springboot 的依赖注入)。有了它之后就可以模拟一个 http get 的无状态调用(见代码)。然后通过 pipe 返回一个 Observable<Lessons[]> 对象。这里我们假设我们的后台已经准备好了 API 接口,在用这些参数调用后会返回分页且排序了的数据。

这个 HTTP API 无非就是做了一个字符串拼接,为了得到通用的 url

http://localhost:4200/api/lessons?courseId=1&filter=&sortOrder=asc&pageNumber=0&pageSize=3

发给后台。技术上实现没什么好讲的直接跳过。

5.3 实现 Custom Angular CDK Data Source

在有了后台调用后,我们就可以进一步实现 Data Source 的功能了。 DataSource 是一个接口,我们需要在代码中做进一步的实现。 先看下接口功能:

import {CollectionViewer, DataSource} from "@angular/cdk/collections";

export class LessonsDataSource implements DataSource<Lesson> {

    private lessonsSubject = new BehaviorSubject<Lesson[]>([]);

    constructor(private coursesService: CoursesService) {}

    connect(collectionViewer: CollectionViewer): Observable<Lesson[]> {
      ...
    }

    disconnect(collectionViewer: CollectionViewer): void {
      ...
    }
  
    loadLessons(courseId: number, filter: string,
                sortDirection: string, pageIndex: number, pageSize: number) {
      ...
    }  
}

如上所示,想实现 CDK Data Source 我们需要实现该接口的几个方法。注意这里我们需要一个 CollectionViewer 对象,它提供了 Observable,以便于发送出信息,比如哪些数据会被展现出来(开始,结束索引)。

connect 方法:这个方法会在数据表初始的时候调用一次。数据表使用它提供的 Observable 对象中的数据展示给前端。这些数据的来源就是刚才我们从 Service 层调用后端所得到的结果,但是是由一个我们定义的 subject (BehaviorSubject) 对象发射出去的,比如通过用户点击了表的某个部分而出发的行为。

BehaviorSubject:前面提到的 subject 是这个类的实例,这个实例的 subscriber 会一直获取最后(the latest data)发射出去的数据(或者初始化数据)。这个 BehaviorSubject 设计之巧妙在于它完全不依赖于对数据操作的先后顺序。这一特点特别适合异步操作,比如:调用后台服务、binding 数据源与表等等。

具体点说就是:数据源不知道数据表,也不知道数据表在何时需要加载数据。但是由于数据表订阅(subscribe)了这个 connect 方法的 Observable 对象,所以它是不管你的后台如何设计的,它最终还是会取得到数据。比如:不管数据还在 HTTP 调用后台的传输中;或者数据已经结束加载。

6 Custom Material CDK Data Source 具体实现

下面是 DataSource 接口的具体实现,先上代码:

export class LessonsDataSource implements DataSource<Lesson> {

    private lessonsSubject = new BehaviorSubject<Lesson[]>([]);
    private loadingSubject = new BehaviorSubject<boolean>(false);

    public loading$ = this.loadingSubject.asObservable();

    constructor(private coursesService: CoursesService) {}

    connect(collectionViewer: CollectionViewer): Observable<Lesson[]> {
        return this.lessonsSubject.asObservable();
    }

    disconnect(collectionViewer: CollectionViewer): void {
        this.lessonsSubject.complete();
        this.loadingSubject.complete();
    }

    loadLessons(courseId: number, filter = '',
                sortDirection = 'asc', pageIndex = 0, pageSize = 3) {

        this.loadingSubject.next(true);

        this.coursesService.findLessons(courseId, filter, sortDirection,
            pageIndex, pageSize).pipe(
            catchError(() => of([])),
            finalize(() => this.loadingSubject.next(false))
        )
        .subscribe(lessons => this.lessonsSubject.next(lessons));
    }    
}

这里面有一个 loading$ 是我们之前没讨论到的,它的作用是控制加载图标,在类中我们用 private 修饰了这个变量意在声明:只有当前类知道,当前是否在加载。在初始时把它设为 false 表示目前没有加载操作。

实现 connect() 方法:

connect(collectionViewer: CollectionViewer): Observable<Lesson[]> {
    return this.lessonsSubject.asObservable();
}

这个方法返回一个 Observable 对象,该对象负责发射 lessons data,我们将它也设成 private 的变量,表示不对外暴露。如果这个对象对外暴露,则表示任何人都可以控制数据源什么时候发射数据,这是不合理的。在设计中,我们希望只有实现了DataSource 的类可以有这一功能。

实现 disconnect() 方法:

disconnect(collectionViewer: CollectionViewer): void {
    this.lessonsSubject.complete();
    this.loadingSubject.complete();
}

这个方法在数据表对象销毁的时候会被自动调用。在方法中我们需要停止订阅所有我们之前创建的 Observable 对象,以避免出现内存泄漏问题。

实现 loadLessons() 方法:

loadLessons(courseId: number, filter = '',
            sortDirection = 'asc', pageIndex = 0, pageSize = 3) {

    this.loadingSubject.next(true);

    this.coursesService.findLessons(courseId, filter, sortDirection,
        pageIndex, pageSize).pipe(
        catchError(() => of([])),
        finalize(() => this.loadingSubject.next(false))
    )
    .subscribe(lessons => this.lessonsSubject.next(lessons));
}

该方法为 public 方法,目的在于可以为多个用户提供调用。它的实现逻辑流程如下:

  1. 首先我们需要将 loading$ 设为 true,表示开始加载数据
  2. 然后启动 LessonsService 通过调用后台服务来获取数据
  3. 通过 findLessons() 方法来获取 Observable 对象
  4. 然后订阅这个获取到的 Observable 对象,这样我们就可以出发 HTTP Request 事件了
  5. 如果成功获取数据,则通过 connect() 的 Observable 将 lessons 数据对象传给数据表,而这一操作是通过调用 this.lessonsSubject.next(lessons) 的

至此我们就打通了后台服务(Backend)与 Data Source 的联系。

7 将 Data Source 与数据表关联起来

回顾下之前的一系列操作,我们在前端模板中使用了 mat-table 并将其与 ts 的数据进行了绑定。然后我们实现了一个简单的后台 HTTP 的服务,并将这个服务与我们的 Data Source 关联到一起。

我们之前说过,Reactive Design 的思路是将数据表(数据展示)从后台调用(Backend)功能解耦出来,继而达到只负责展现的功能。 就是说数据表是不知道数据源是哪里来的,而且也不知道是什么触发新的数据更新。我们通过实现 Data Source 实现了数据表需要的一系列功能,剩下需要做的只是将数据表与Data Source 建立起关联,这样我们就可以自由地异步、无状态地传输数据给前端。

代码如下:

@Component({
    selector: 'course',
    templateUrl: './course.component.html',
    styleUrls: ['./course.component.css']
})
export class CourseComponent implements OnInit {

    dataSource: LessonsDataSource;
    displayedColumns= ["seqNo", "description", "duration"];

    constructor(private coursesService: CoursesService) {}

    ngOnInit() {
        this.dataSource = new LessonsDataSource(this.coursesService);
        this.dataSource.loadLessons(1);
    }
}

这个类包括了两个重要的成员变量:

  • displayedColumns 数组,它定义了列在数据表中的展现顺序(见之前的章节)
  • dataSource 这个对象是我们刚才实现的类,用来连接 CoursesService 调用 HTTP 取后台数据,并返回 Observable 对象,然后通过其中转再传给前端的 mat-table 数据表

在 ngOnInit 方法中,我们初始化了 dataSource,然后调用 loadLessons() 来触发加载数据,并映射到数据表中,他的大概流程为:

  1. dataSource 调用 LessonsService 触发 HTTP Request 来获取后台数据
  2. 然后 dataSource 对象通过 lessonsSubject 以及 connect 方法发射获得的数据给前端
  3. mat-table 组件订阅了 connect() 的 observable 对象,然后刷新表中数据

这样我们就完成了一个最简单的获取数据。

注意:目前的实现仅仅获取后台第一页数据,且没有搜索功能。 这些我们可以在后面的步骤中慢慢补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值