65.9K
CodeProject 正在变化。 阅读更多。
Home

Angular 虚拟滚动 - ngVirtualScrolling

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2018 年 10 月 25 日

CPOL

6分钟阅读

viewsIcon

11092

关于 Angular 7 的一项功能,即虚拟滚动的一点解释

引言

是的!Angular 7 已经发布,带来了一些很棒的新功能。我非常欣赏您想要体验全新的 Angular。在这篇文章中,我将详细介绍 Angular 7 的一项功能,即虚拟滚动。在本文结束时,您将拥有一个应用程序,该应用程序通过使用虚拟滚动功能从数据库获取真实数据并将其绑定到 UI。我不确定您怎么样,但我非常兴奋能够使用此功能开发一个示例应用程序。话不多说,让我们开始设置吧。希望这篇文章对您有所帮助。

背景

由于 Angular 7 上周已经发布,我想对它做一些尝试,这也是我写这篇文章的原因。如果您是 Angular 新手,并且需要尝试其他一些内容,那么 浏览我关于同一主题的文章 会是一个不错的选择。

创建 ngVirtualScrolling 应用

我们要做的第一件事是创建一个模拟应用程序。

安装 Angular CLI

是的,正如您所猜到的,我们正在使用 Angular CLI。如果您还没有安装 Angular CLI,我建议您安装它。它是一个很棒的 Angular CLI 工具,我相信您会喜欢它。您可以通过运行以下命令来安装它

npm install -g @angular/cli

设置好项目后,我们将使用 Angular CLI 命令,您可以在 这里 查看 CLI 的功能。

生成新项目

现在是时候生成我们的新项目了。我们可以使用以下命令来实现

ng new ngVirtualScrolling

您将能够看到 CLI 为我们所做的所有辛勤工作。

生成 ng 项目

现在我们已经创建了应用程序,让我们运行它,看看它是否正常工作。

构建并在浏览器中打开

在开发过程中,我们将使用 Angular Material 进行设计,现在就可以将其与动画和 cdk 一起安装。

安装 material、cdk 和 animation

对于 Angular 6+ 版本,您也可以通过以下命令执行此操作

ng add @angular/material

生成组件

现在我们的项目已经准备就绪,我们可以开始创建组件了,同样,CLI 将免费为我们完成工作。如果是一个自由职业者,您会付给他/她多少钱?

ng g c home

ng g c header

ng g c footer

现在,我们有三个组件可以处理。所以让我们开始吧。

设置 Header 组件

我将只编辑 header 组件的 HTML,而不添加任何逻辑。您可以添加任何您想要的。

<div style="text-align:center">
  <h1>
    Welcome to ngVirtualScrolling at <a href="https://sibeeshpassion.com">Sibeesh Passion</a>
  </h1>
</div>

设置 Footer 组件

<p>
  Copyright @SibeeshPassion 2018 - 2019 :)
</p>

设置 app-routing.module.ts

我们将只为 home 创建一个路由。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
const routes: Routes = [
  {
    path: '',
    redirectTo: '/home',
    pathMatch: 'full'
  },
  {
    path: 'home',
    component: HomeComponent
  }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

在 app.component.html 中设置 Router Outlet

现在我们有了路由,是时候设置 outlet 了。

<app-header></app-header>
<router-outlet>
</router-outlet>
<app-footer></app-footer>

设置 app.module.ts

每个 Angular 应用至少会有一个 NgModule 类,它被命名为 AppModule,并驻留在名为 app.module.ts 的文件中。您始终可以在 这里 了解 Angular 架构。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {MatButtonModule, MatCheckboxModule, MatMenuModule, MatCardModule, MatSelectModule} 
from '@angular/material';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
import { HomeComponent } from './home/home.component';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MovieComponent } from './movie/movie.component';
import { MovieService } from './movie.service';
import { HttpModule } from '@angular/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    FooterComponent,
    HomeComponent,
    MovieComponent
  ],
  imports: [
    HttpModule,
    BrowserModule,
    AppRoutingModule,
    ScrollingModule,
    MatButtonModule, MatCheckboxModule, MatMenuModule, 
          MatCardModule, MatSelectModule, BrowserAnimationsModule
  ],
  exports: [
    HttpModule,
    BrowserModule,
    AppRoutingModule,
    ScrollingModule,
    MatButtonModule, MatCheckboxModule, MatMenuModule, MatCardModule, MatSelectModule
  ],
  providers: [ MovieService ],
  bootstrap: [AppComponent]
})
export class AppModule { }

您看到 ScrollingModule 了吗?您需要导入它才能使用虚拟滚动,它驻留在 @angular/cdk/scrolling 中。正如您可能已经注意到的,我们在 providers 数组中添加了一个名为 MovieService 的服务。我们现在就创建一个。

创建 Movie 服务

import { Injectable } from '@angular/core';
import { RequestMethod, RequestOptions, Request, Http } from '@angular/http';
import { config } from './config';
@Injectable({
  providedIn: 'root'
})
export class MovieService {
  constructor(private http: Http) {
  }
get(url: string) {
    return this.request(url, RequestMethod.Get)
  }
request(url: string, method: RequestMethod): any {
    const requestOptions = new RequestOptions({
      method: method,
      url: `${config.api.baseUrl}${url}${config.api.apiKey}`
    });
const request = new Request(requestOptions);
    return this.http.request(request);
  }
}

正如您所见,我并没有在服务类中做太多事情,也没有实现错误机制和其他东西,因为我想尽可能地缩短篇幅。这个服务将从在线数据库 TMDB 获取电影,在这篇文章和存储库中,我使用的是我的。我强烈建议您创建自己的,而不是使用我的。我们现在可以设置配置文件了吗?

设置 config.ts

配置文件是一种整理事物的方式,您必须在您处理的所有项目中实施它。

const config = {
  api: {
        baseUrl: 'https://api.themoviedb.org/3/movie/',
        apiKey:'&api_key=c412c072676d278f83c9198a32613b0d',
        topRated:'top_rated?language=en-US&page=1'
    }
}
export { config };

创建 Movie 组件

现在让我们创建一个新组件来加载电影。基本上,我们将在 cdkVirtualFor 中使用这个 movie 组件,以便它每次都会调用这个组件并渲染它。我们的 movie 组件将具有如下 HTML

<div class="container">
  <mat-card style="min-height: 500px;" class="example-card">
    <mat-card-header>
      <div mat-card-avatar class="example-header-image"></div>
      <mat-card-title>{{movie?.title}}</mat-card-title>
      <mat-card-subtitle>Release date: {{movie?.release_date}}</mat-card-subtitle>
    </mat-card-header>
    <img mat-card-image src="https://image.tmdb.org/t/p/w370_and_h556_bestv2/{{movie?.poster_path}}" 
     alt="{{movie?.title}}">
    <mat-card-content>
      <p>
        {{movie?.overview}}
      </p>
    </mat-card-content>
  </mat-card>
</div>

而打字脚本文件将有一个带有 @Input 装饰器的属性,以便我们可以从 home 组件输入值给它。

import { Component, OnInit, Input } from '@angular/core';
import { Movie } from '../models/movie';
@Component({
  selector: 'app-movie',
  templateUrl: './movie.component.html',
  styleUrls: ['./movie.component.scss']
})
export class MovieComponent implements OnInit {
  @Input()
  movie: Movie;
  
  constructor() { 
  }
ngOnInit() {
  }
}

设置 Home 组件

现在这是主要部分,虚拟滚动发生的地方。现在让我们设计 HTML。

<div class="container" style="text-align:center">
  <div class="row">
    <cdk-virtual-scroll-viewport itemSize="500" class="example-viewport">
      <app-movie *cdkVirtualFor="let movie of ds" 
       [movie]="movie" class="example-item">{{movie || 'Loading...'}}</app-movie>
    </cdk-virtual-scroll-viewport>
  </div>
</div>

这里 itemSize 是一个必需的属性,您可以根据要加载到组件的数据量来给出任何数字。我们通过使用 [movie]=”movie” 来输入值给我们的 app-movie 组件。

现在让我们看看打字脚本代码

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { DataSource, CollectionViewer } from '@angular/cdk/collections';
import { BehaviorSubject, Subscription, Observable } from 'rxjs';
import { MovieService } from '../movie.service';
import { Movie } from '../models/movie';
import { config } from '../config';
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent {
  constructor(private movieService: MovieService) {
  }
  ds = new MyDataSource(this.movieService);
}
export class MyDataSource extends DataSource<Movie | undefined> {
  private page = 1;
  private initialData: Movie[] = [
    {
      id: 19404,
      title: 'Dilwale Dulhania Le Jayenge',
      overview: 'Raj is a rich, carefree, happy-go-lucky second generation NRI. 
      Simran is the daughter of Chaudhary Baldev Singh, who in spite of being an NRI 
      is very strict about adherence to Indian values. Simran has left for India to be married 
      to her childhood fiancé. Raj leaves for India with a mission at his hands, 
      to claim his lady love under the noses of her whole family. Thus begins a saga.',
      poster_path: '\/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg'
    }
  ];
  private dataStream = new BehaviorSubject<(Movie | undefined)[]>(this.initialData)
  private subscription = new Subscription();
constructor(private movieService: MovieService) {
    super();
  }
connect(collectionViewer: CollectionViewer): Observable<(Movie | undefined)[]> {
    this.subscription.add(collectionViewer.viewChange.subscribe((range) => {
      console.log(range.start)
      console.log(range.end)
      this.movieService.get(config.api.topRated)
        .subscribe((data) => {
          this.formatDta(JSON.parse(data._body).results);
        });
    }));
    return this.dataStream;
  }
disconnect(): void {
    this.subscription.unsubscribe();
  }
formatDta(_body: Movie[]): void {
    this.dataStream.next(_body);
  }
}

我有一个父级 HomeComponent,我从一个名为 MyDataSource 的类中获取数据,该类扩展了 DataSource<Movie | undefined>。这个 DataSource 是一个 abstract 类,驻留在 @angular/cdk/collections 中。由于 MyDataSource 类扩展了 DataSource,我们必须覆盖两个函数,即 connect()disconnect()。根据 Angular Material 文档connect 方法将被虚拟滚动视口调用,以接收一个流,该流发出应该渲染的数据数组。当 viewport 被销毁时,视口将调用 disconnect,这可能是清理在 connect 过程中注册的任何订阅的合适时机。

connect 方法内部,我们调用我们自己的服务来获取数据。

this.movieService.get(config.api.topRated)
        .subscribe((data) => {
          this.formatDta(JSON.parse(data._body).results);
        });

自定义样式

我为一些组件应用了一些自定义样式,您可以在 GitHub 仓库中看到。如果您从头开始创建应用程序,请从那里复制这些样式。

输出

在实现了所有步骤之后,您将拥有一个使用 Angular 7 虚拟滚动和实际服务器数据的应用程序。现在让我们运行应用程序,看看它的效果。

Angular 虚拟滚动演示开始

Angular 虚拟滚动演示中间

结论

在这篇文章中,我们学习了如何

  1. 创建一个 Angular 7 应用程序
  2. 使用 Angular CLI
  3. 在 Angular 中生成组件
  4. 使用 Material Design
  5. 在 Angular 7 中使用虚拟滚动

请随时玩转这个 GitHub 仓库。这可能不是一篇关于这个主题的完美文章,所以在您进行操作时,请分享您的发现。我将非常感激,提前感谢。

现在轮到你了。你有什么想法?

非常感谢您的阅读。我将很快带来另一篇关于同一主题的文章。您认为我遗漏了什么吗?您觉得这篇文章有用吗?请分享您宝贵的建议和反馈,但请尽量保持主题。如果您有一个与本文无关的问题,最好将其发布到 Stack Overflow 而不是在此评论。在 Twitter 上给我发推文或通过电子邮件发送链接到您的问题,如果我能帮忙,我会尽力帮助您。

© . All rights reserved.