Angular CDK 的意义

经过之前两篇文章 CDK Portal 和 CDK Layout の Breakpoints,我相信大家已经悟到了 CDK 的意义。

CDK 有 3 个方向:

  1. 包装 BOM / DOM 上层接口 (e.g. CDK Layout)

    这个方向主要是让我们不直接操作/依赖 BOM 和 DOM。

    还有把接口包装成 RxJS / Promise 之类的,方便我们使用。

  2. 包装 Angular 上层接口 (e.g. CDK Portal)

    这个方向主要是优化 Angular 上层接口调用,或者是扩展弥补一些 Angular 缺失的功能。

  3. 辅助制作 UI Component

    Angular Material Team 是在用 Angular Way 开发 Material Design UI Component 过程中提炼出 CDK 的。

    UI Component 通常需要 HTML,CSS,JS 三个地方实现,而 CDK 主要是抽象封装了 JS 的部分,CSS 几乎没有,HTML 有一点点 (大概是这个比例)。

    这个方向主要是让我们可以做以下几件事:

    a. 制作 Angular Material Team 多年都没有完成的 Material Design UI Component (e.g. Time pickers)

    b. 制作变种的 Material Design UI Component,有些 UI Component 没有在 Material Design 规范中,这些 UI Component Angular Material Team 是坚决不肯做的,

    但其实 Google Products (Gmail, Google Ads 等) 都有这些 UI Component 的具体实现,这时我们就可以依照它们自己制作出来。

    c. 制作不同风格的 UI Component。如果 UI Component 只是视觉上不同,交互逻辑差不多,那 CDK 确实非常合适。

    但要记得,CDK 服务的对象始终只有 Angular Material,就好比 Angular 服务的对象始终只有 Google Products 一样,它们不会为了你或者社区去做任何对 Google 没有利益的事。

好,理解了 CDK 的方向,我们在学习和使用它时就能得心应手了。

CDK Scrolling 简单介绍

在做 UI Component 时,我们经常需要监听 scroll event,获取 element scrollTop,操作 element scrollTo 等等。

CDK Scrolling 主要就是对这些 DOM Manipulation 做了封装。

另外,CDK Scrolling 还有一个强大的功能 -- Virtual Scrolling,不熟悉 Virtual Scrolling 的可以看这篇:CSS & JS Effect – Virtual Scrolling

虽然目前 Virtual Scrolling 还不完善

但也勉强可以用于一些小场景。

CdkScrollable 指令

需求

App Template

<div class="scrollable">
<div class="spacer"></div>
</div>

App Styles

.scrollable {
margin-inline: auto;
margin-top: 128px; overflow-y: auto;
height: 200px;
width: 100px;
border: 1px solid red; .spacer {
width: 100%;
height: 2000px;
}
}

效果

一个 div,里面有另一个 div 把它撑大,于是它可以 scroll。

需求是监听它的 scroll event。

Without CDK

不使用 CDK 的情况下,我们会这样实现

在 scrollable element 上添加 Template Binding Syntax

<div class="scrollable" (scroll)="handleScroll($event)">
<div class="spacer"></div>
</div>

接着在 App 组件写上 handleScroll 方法

export class AppComponent {
handleScroll(event: Event) {
console.log('scrolled');
}
}

效果

代码虽然简单,但这个方式往往不足够应付 UI Component。
UI Component 一般上交互会比较复杂,一个事件往往会牵连到很多事情,所以更好的方式是以 RxJS 的形式去监听 (RxJS Stream 比较容易拿来组合,或者变化等等,简单说就是比较灵活使用)。

with CDK

在 App 组件 import ScrollingModule,因为我们需要用到相关的指令。

把 CdkScrollable 指令 apply 到 scrollable element

<div class="scrollable" cdkScrollable>
<div class="spacer"></div>
</div>

通过 viewChild 获取 CdkScrollable 指令,然后使用 elementScrolled 方法监听 scroll event。

export class AppComponent implements OnInit {
// 1. 获取 CdkScrollable 指令
scrollable = viewChild.required(CdkScrollable); ngOnInit() {
// 2. 使用 elementScrolled 监听 scroll event
// 它会返回 RxJS Observable
this.scrollable()
.elementScrolled()
.subscribe(event => {
console.log('scrolled', event);
});
}
}

它返回的是 RxJS Stream。

源码在 scrollable.ts

没什么特别的,只是用 RxJS 监听了 element scroll event 而已。

注:ngZone 是 Change Detection 的知识,我们可以忽视它,因为 Angular v17.1.0 后 ngZone 基本已经废弃了。

对比使用 CDK 和不使用 CDK 两个版本,我们可以悟出一个道理 -- 开发 UI Component 和开发业务项目是不同的。

开发业务我们会远离 DOM,开发 UI Component 我们会贴近 DOM。

get scrollTop

监听到了 scroll event 下一步通常是获取 scrollTop 值。

CdkScrollable 指令也有针对这个的接口。

const scrollTop = this.scrollable().measureScrollOffset('top');

没什么特别的,底层就是拿 element.scrollTop 属性而已

scrollTo

scrollTo 就是对 DOM element.scrollTo 的封装而已。

this.scrollable().scrollTo({ top: 100, behavior: 'smooth' });
this.scrollable().scrollTo({ bottom: 100, behavior: 'smooth' });

接口比原生 DOM 好多了,甚至支持 scroll to bottom

相关源码

ScrollDispatcher

CdkScrollable 指令可以用来监听 element scroll event,但它监听不到 document scroll event,因为指令无法 apply 超出 App 组件范围。

这时,我们需要使用 ScrollDispatcher。

监听 document scroll event

ScrollDispatcher 是一个 Root Level Provider。

export class AppComponent {
constructor() {
// 1. inject ScrollDispatcher
const scrollDispatcher = inject(ScrollDispatcher); // 2. 监听 scroll event
scrollDispatcher.scrolled().subscribe(() => {
console.log('document scrolled');
});
}
}

效果

源码在 scroll-dispatcher.ts

not only document scroll event but all CdkScrollable

ScrollDispatcher 不仅仅监听 document scroll event,它还监听了所有 CdkScrollable 指令的 scroll event。

App Template 有一个 CdkScrollable 指令

<div class="scrollable" cdkScrollable>
<div class="spacer"></div>
</div>

然后 App 组件监听 ScrollDispatcher

export class AppComponent {
constructor() {
const scrollDispatcher = inject(ScrollDispatcher);
scrollDispatcher.scrolled().subscribe(scrollable => {
console.log('scroll dispatcher', scrollable);
});
}
}

如果是 document scrolled 那 callback 函数的参数 scrollable 会是 undefined。

如果是 CdkScrollable 指令 scrolled 那 callback 函数的参数 scrollable 会是 CdkScrollable 指令实例。

效果

提醒:ScrollDispatcher 会监听全世界所有的 CdkScrollable 指令哦

每一个 CdkScrollable 在 OnInit 时都会把自己注册到 ScrollDispatcher

所谓的注册就是监听 CdkScrollable 的 scroll event 然后 ScrollDispatcher 再转发。

only listen ancestor CdkScrollable

通过 ScrollDispatcher 监听 CdkScrollable scroll event 有时候是会比较混乱的,毕竟它是 Root Level Provider,意味着着全世界的 CdkScrollable 指令都会往它身上注册。

也因为这样,ScrollDispatcher 提供了一些过滤监听的方式,比如说 ancestorScrolled 方法。

getAncestorScrollContainers 方法

_scrollableContainsElement 方法

ancestorScrolled 的监听时机

ancestorScrolled 的监听时机是很讲究的,当我们调用 ancestorScrolled 时,请一定要确保 CdkScrollable 已经被注册到 ScrollDispatcher 里。

上面我们有提到,CdkScrollable 指令是在 OnInit 阶段被注册到 ScrollDispatcher 里的,不是 constructor 阶段哦。

总结

  1. 想监听指定某个 element 的 scroll event

    用 CdkScrollable 指令。

  2. 想监听 document scroll event

    用 ScrollDispatcher 监听,callback 函数的 scrollable 参数是 undefined 就表示是 document scroll。

  3. 想监听 parent / ancestor scroll event

    用 ScrollDispatcher.ancestorScrolled 方法。

题外话:Dependency injection based on ancestor element

我们知道 Angular 的 DI 查找的是 NodeInjector Tree 而不是 DOM Tree,不熟悉的朋友可以看这篇 Dependency Injection & NodeInjector

这是一个局限,有时候使用起来很不方便。

上面这个 ancestorScrolled 方法是一个很好的案例。

1 个 Root Service

1 个指令

1 个 element

指令可以 inject 其它 Service,然后把 element 和相关的 Service 一起记入到 Root Service。

要使用的人就可以通过 Root Service 依据 element 做查找,最后获得相关 Service。

ViewportRuler

ViewportRuler 其实和 Scrolling 没有什么关系,不知为什么 Angular Material 会把它纳入到 Scrolling Module。

ViewportRuler 也是一个 Root Level Provider。它可以监听 viewport resize event 和获取 viewport dimension。

export class AppComponent {
constructor() {
// 1. inject ViewportRuler
const viewportRuler = inject(ViewportRuler); // 2. 监听 resize
viewportRuler.change().subscribe(event => { // 3. 获取 viewport dimension
const { width, height } = viewportRuler.getViewportSize();
console.log('dimension', [width, height]);
});
}
}

viewport 指的是 window。

resize 指的是 window resize event 和 orientationchange event

dimension 指的是 window.innerWidth 和 innerHeight

另外,change 方法返回的 Observable 默认会有一个 auditTime 20ms

Virtual Scrolling

不熟悉 Virtual Scrolling 的可以先看这篇:CSS & JS Effect – Virtual Scrolling

Example

我用回文章里最后一个完整的例子

App 组件

@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ScrollingModule],
})
export class AppComponent {
itemHeight = signal(40);
itemTexts = signal(new Array(500).fill(null).map((_, index) => `item${index + 1}`));
}

App Template

<div class="viewport" cdkVirtualScrollingElement>
<div class="header">Header</div> <cdk-virtual-scroll-viewport [itemSize]="itemHeight()" class="item-list">
<ng-template let-item cdkVirtualFor [cdkVirtualForOf]="itemTexts()">
<div class="item">{{ item }}</div>
</ng-template>
</cdk-virtual-scroll-viewport> <div class="footer">Footer</div>
</div>

几个知识点:

  1. cdkVirtualScrollingElement 指令

    如果 scrollable 和 cdk-virtual-scroll-viewport 不是同一个 element,那就需要用到 CdkVirtualScrollingElement 指令。

  2. cdkVirtualForOf 指令

    它和 ngForOf 指令的接口完全一模一样,把它当成 ngForOf 指令使用就可以了。

  3. [itemSize] 就是 itemHeight,一定要提供。

App Styles

.viewport {
margin-top: 128px;
margin-inline: auto;
width: 256px;
height: 256px;
border: 2px solid red; .header,
.footer {
height: 80px;
background-color: #988beb;
display: flex;
justify-content: center;
align-items: center;
} .item-list .item {
height: 40px;
display: flex;
justify-content: center;
align-items: center; &:nth-child(odd) {
background-color: pink;
} &:nth-child(even) {
background-color: lightblue;
}
}
}

效果

Cache Concept

默认情况下 CDK Virtual Scrolling 会缓存 20 个创建过的 Item ViewRef。

相关源码在 recycle-view-repeater-strategy.ts

要缓存多少,我们可以自己配置

Elements with parent tag requirements

<cdk-virtual-scroll-viewport class="viewport" [itemSize]="itemHeight()">
<dl> <ng-template
let-keyValue
cdkVirtualFor
[cdkVirtualForOf]="keyValues()"
>
<dt>{{ keyValue.key }}</dt>
<dd>{{ keyValue.value }}</dd>
</ng-template> </dl>
</cdk-virtual-scroll-viewport>

注意看它的结构,cdk-virtual-scroll-viewport 里面先是一个 dl,dl 里面才是 <ng-template>。

其实 cdk-virtual-scroll-viewport 并不负责创建 item

它只负责 item-wrapper 和 spacer 而已,item 是由 cdkVirtualForOf 指令创建好后 transclude 进来的。

DataSource and lazy loading

虽然 Virtual Scrolling 解决了渲染时的性能问题,但是 cdkVirtualForOf 要求我们预先传入一个完整的 item list,

假如我有 10 万条 items 而且资料是从后端取来的,那我就需要一次性下载 10 万条资料,这样也会导致 http 请求性能问题。

为此,Angular Material 设计了一个叫 DataSource 的概念,它允许我们分量提供 items。

注:DataSoruce 概念不仅仅用于 Virtual Scrolling,其它地方比如 Table 和 Tree 也可以使用 DataSoruce (以后会教)

DataSource 是一个抽象 class,它长这样

我们需要 extends 它,并且 override connect 和 disconnect 方法。

顾名思义,connect 会在 CdkVirtualForOf 指令 OnInit 时被调用,disconnect 则是在 OnDestroy 时被调用。

class ItemTextsDataSource

class ItemTextsDataSource extends DataSource<string | null> {
private subscription = new Subscription();
private itemsBS: BehaviorSubject<readonly (string | null)[]>; constructor(itemCount: number) {
super();
// 1. 初始化一个空 item list
// 虽然我们不需要立刻 http request 10 万条 item 的具体信息,但我们依然需要知道 item 总数是多少。
this.itemsBS = new BehaviorSubject<readonly (string | null)[]>(new Array(itemCount).fill(null));
} override connect(collectionViewer: CollectionViewer): Observable<readonly (string | null)[]> {
this.subscription.add(
// 3. connect 时,我们会等到参数 collectionViewer
// 我们需要监听它,每当 user scroll 它会触发,并且告诉我们当前 user 需要看到哪些 item
collectionViewer.viewChange.subscribe(range => {
// 4. 比如 user scroll 到中间,
// 需要显示第 50001 到 50021 的 items
// start 和 end 指的是 index
const { start, end } = range; // 5. 这里可以发 http request 去拿 item 的具体信息
// 然后添加进 itemsBS
window.setTimeout(() => {
const newItems = new Array(end - start).fill(null).map((_, index) => `Item ${start + index + 1}`);
const items = this.itemsBS.value;
this.itemsBS.next([...items.slice(0, start), ...newItems, ...items.slice(end, items.length)]);
}, 1000);
}),
); // 2. 返回 items$ Observable
return this.itemsBS.asObservable();
} override disconnect() {
// 7. connect 的时候,我们监听了 CollectionViewer.viewChange,这里需要退订
// 题外话,即使不退订也不会造成内存泄漏的,因为 CdkVirtualForOf 指令 OnDestroy 的时候会 complete 掉 CollectionViewer.viewChange。
// 但是呢,我们最好还是养成退订的好习惯,毕竟 complete 和 unsubscribe 在一些特定情况下效果是不一样。
this.subscription.unsubscribe();
}
}

接着换上 ItemTextsDataSource

export class AppComponent {
readonly itemHeight = signal(40);
// 1. 把 itemTexts 换成 itemDataSource
// readonly itemTexts = signal(new Array(100000).fill(null).map((_, index) => `item${index + 1}`));
readonly itemTextsDataSource = new ItemTextsDataSource(100000);
}

App Template

item 是 null 就显示 loading...

效果

官网给了一个很不错的 Demo,大家也可以看一看。

Connect DataSource multiple times

DataSoruce 的接口设计是挺奇葩的

它让我们产生几个疑问:

  1. 每一次调用 connect 返回的 Observable 必须是同一个吗?

  2. unsubscribe connect 返回的 Observable 和 disconnect 是一样的效果吗?

我们看看 Material Table 如何实现 DataSource

  1. 假如有 multiple connect,它始终返回同一个 Observable (_renderData 是 BehaviorSubject),内部 subscription 也只维持一个。

  2. disconnect 会 unsubscribe 内部的 subscription,与此同时 _renderData 不会在 next 了,但它没用 complete,所以它是可以 re-connect 的。

  3. unsubscribe connect 返回的 Observable 和 disconnect 效果是不一样的。

结论:MatTableDataSource 大家可以 share 着用,只有其中一个人需要负责 disconnect 就可以了。

我没提到的功能:

有些功能很冷门,而且很简单,我就不一一讲解了:

  1. minBufferPx & maxBufferPx

    items 只需要显示足够填满 viewport 就可以了,但是如果我们希望它提早出现则可以 set buffer。

    这类似 IntersectionObserverrootMargin 设置。

  2. Viewport Orientation

    它还支持 horizontal

  3. Append Only Mode

    创建 append 后的 item 不会再被清除。

    这意味着 Node 会越来越多,可能会引起性能问题,要留意哦。

  4. Scroll Window

    通常 scrollable 是 cdk-virtual-scroll-viewport,

    如果遇到有 header 的场景,那 scrollable 就不是 cdk-virtual-scroll-viewport 了

    <div class="viewport" cdkVirtualScrollingElement>

    这时需要需要在 scrollable element 加上 CdkVirtualScrollingElement 指令。

    如果 scrollable 是 window / document,那就需要用 CdkVirtualScrollableWindow 指令

    <cdk-virtual-scroll-viewport scrollWindow itemSize="50">

目录

上一篇 Angular Material 18+ 高级教程 – CDK Layout の Breakpoints

下一篇 Angular Material 18+ 高级教程 – Material Icon

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding

Angular Material 18+ 高级教程 – CDK Scrolling的更多相关文章

  1. Angular Material 教程之布局篇

    Angular Material 教程之布局篇 (一) : 布局简介https://segmentfault.com/a/1190000007215707 Angular Material 教程之布局 ...

  2. Angular Material TreeTable Component 使用教程

    一. 安装 npm i ng-material-treetable --save npm i @angular/material @angular/cdk @angular/animations -- ...

  3. Angular Material design设计

    官网: https://material.io/design/ https://meterial.io/components 优秀的Meterial design站点: http://material ...

  4. angular使用@angular/material 出现"export 'ɵɵinject' was not found in '@angular/core'

    WARNING in ./node_modules/@angular/cdk/esm5/a11y.es5.js 2324:206-214 "export 'ɵɵinject' was not ...

  5. Material使用11 核心模块和共享模块、 如何使用@angular/material

    1 创建项目 1.1 版本说明 1.2 创建模块 1.2.1 核心模块 该模块只加载一次,主要存放一些核心的组件及服务 ng g m core 1.2.1.1 创建一些核心组件 页眉组件:header ...

  6. Siki_Unity_2-9_C#高级教程(未完)

    Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...

  7. Angular Material Starter App

      介绍 Material Design反映了Google基于Android 5.0 Lollipop操作系统的原生应用UI开发理念,而AngularJS还发起了一个Angular Material ...

  8. 关于 Angular引用Material出现node_modules/@angular/material/button-toggle/typings/button-toggle.d.ts(154,104): error TS2315: Type 'ElementRef' is not generic.问题

    百度了好久 ,,,最后谷歌出来了.. 该错误可能来自于您将@ angular / material设置为6.0.0, 但所有其他Angular包都是5.x.您应该始终确保Material主要版本与An ...

  9. Angular Material & Hello World

    前言 Angular Material(下称Material)的组件样式至少是可以满足一般的个人开发需求(我真是毫无设计天赋),也是Angular官方推荐的组件.我们通过用这个UI库来快速实现自己的i ...

  10. 基于 Angular Material 的 Data Grid 设计实现

    自 Extensions 组件库发布以来,Data Grid 成为了使用及咨询最多的组件.最开始 Data Grid 的设计非常简陋,经过一番重构,组件质量有了质的提升. Extensions 组件库 ...

随机推荐

  1. PowerBuilder现代编程方法X11:PB程序完全跨平台方案

    PB可能要支持Windows.macOS.Linux.iOS.Android与鸿蒙操作系统和X86.ARM.RISC-V与国产龙芯CPU的原生应用了! PowerBuilder现代编程方法X11:PB ...

  2. mysql 临时表的好处

    客户端新建了一个会话,这个会话只是服务器与客户端1对1的关系,客户端可能在服务端建立一个临时表,满足客户端处理某些事务的需求,当客户端退出会话后,这个临时表自动drop,没有任何数据信息占用数据库空间 ...

  3. [oeasy]python0074[专业选修]字节序_byte_order_struct_pack_大端序_小端序

    进制转化 回忆上次内容 上次 总结了 计算字符串值的函数 eval   四种进制的转化函数 bin oct int hex     函数名 前缀 目标字符串所用进制 bin 0b 二进制 oct 0o ...

  4. [oeasy]python0139_尝试捕获异常_ try_except_traceback

                                                          - 不但要有自己的报错 - 还要保留系统的报错 - 有可能吗? ​ ### 保留报错 ​ ! ...

  5. JavaScript高级~数组方法reduce

    reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值. 有点抽象,意思可以看做每个元素执行之后,都会有一个汇总结果,你可以通过这个汇总结果 ...

  6. vue3基础学习

    第一章:vue3.0基础 1,认识vue3.0 vue3.0发布时间为2020-9-18,从项目体验上,vue3.0比起vue2.0有以下优势: 打包大小减少41% 初次渲染块55%,更新渲染块133 ...

  7. jwt redis,微信登陆知识复习 uniapp 请求封装,统一异常处理 相关, HutoolDemo工具介绍)

    第三节   后台布局搭建,代码可以人工智能来写,但是环境初步搭建需要我们先建起来,所以以下记录快带搭建的过程, 思路: 后台首页的搭建 第一   用到了element--UI 自带的页面布局组件,它就 ...

  8. 安卓开发 StateListDrawable 应用

    基础部份     StateListDrawable 安卓开发中,如果要做一个按扭按下改变背景,或获取焦点改变背景,最简单的方法是利用将背景指向一个资源,然后果在资源中配置事件,总共分为三步, 1)  ...

  9. ComfyUI插件:ComfyUI Impact 节点(一)

    前言: 学习ComfyUI是一场持久战,而 ComfyUI Impact 是一个庞大的模块节点库,内置许多非常实用且强大的功能节点 ,例如检测器.细节强化器.预览桥.通配符.Hook.图片发送器.图片 ...

  10. Android 性能稳定性测试工具 mobileperf 开源 (天猫精灵 Android 性能测试-线下篇)

    Android 性能稳定性测试工具 mobileperf 开源 (天猫精灵 Android 性能测试-线下篇) 这篇文章写得很好!感谢阿里云开发者社区!!! 原文地址: https://develop ...