使用Angular Material学习“适用于任何应用”的组件开发
首先
这篇文章的目标是针对那些已经基本掌握了Angular功能,但却无法制作出出色的用户界面的人群,致力于提供初级和中级的桥梁。
本文所讲述的主题分为两个部分 – 设计出炫酷的导航栏,以及制作“忙碌转动”的加载动画。

这些是许多应用程序中常见的组件,但正因为如此,其中有一些方面很难欺骗,并且我自己在探索最佳实践的过程中不断尝试和犯错。
本文的前半是相对简单的内容,后半则涉及稍微有难度的内容,我努力着为广大读者提供尽可能广泛的乐趣。由于都涉及实用的主题,希望您能够读完,并感到满意。
目标读者
-
- Angularの基本的な機能をある程度知っている
-
- Angular CLIのコマンドがちょっと使える
-
- Angular v17の構文をキャッチアップしたい
-
- Angular Materialのカスタマイズ方法を知りたい
- ちょっとかっこいいコンポーネントの作り方を知りたい
软件开发环境
各种工具的版本假定如下所示。
Node.js: 20.10.0 (LTS)
npm: 10.2.4
Angular CLI: 17.0.5
OS: Windows 11
免责事项
本文仅为提供信息而存在。
作者对基于内容的实施和运营不承担任何责任,包括因此而导致的任何损失。
准备运动
创建项目(工作空间)
让我们在命令提示符中输入以下内容,创建一个名为sample的项目。
> ng new sample --routing --standalone --style scss --ssr false --skip-tests
在ng new命令中,有几个选项可以使用,但在这里我们指定了以下选项。有关其他选项,请参阅Angular CLI文档。
--routing
ルーティング(ページ遷移)を有効化します。--standalone
NgModule
に依存しない「スタンドアロンコンポーネント」によるアプリケーションのテンプレートを生成します。--style
アプリケーションで利用するスタイルファイルの種類を指定します。css
、scss
、sass
、less
のいずれかが指定できます。今回はscss
を指定しました。--ssr
サーバサイドレンダリングを有効化するかどうかを指定できます。今回はサーバサイドレンダリングは有効化しません。--skip-tests
ユニットテストコードが生成されないようにします。当执行先前的命令时,将会生成以下的模板。

对于习惯于往常的Angular的人来说,可能会感到不适应,因为生成的文件中不再包含app.module.ts和app-routing.module.ts等NgModule。这是因为通过推出Angular v15的“独立组件”,NgModule不再是必需的。以前,每次添加外部模块引用或组件时都需要编辑NgModule,导致多个开发成员频繁编辑同一文件,容易出现竞争和合并冲突。这些问题已经通过独立组件得到了相当大的改善。
如果项目创建成功,请将其移动到示例文件夹中。
> cd sample
安裝Angular Material
接下来,我们将安装Angular Material1。
请确保当前目录为sample,并输入以下命令。
> ng add @angular/material
在安装过程中会出现一些问题,但是如果你不清楚的话,可以选择默认回答2,没有问题。
但是,对于“请选择一个预设的主题名称,或者选择“自定义”以自定义主题”的问题,我们建议您像下图一样选择“自定义”。

安装成功后,会显示“Packages installed successfully”(如下图红色区域),然后会自动更新一些文件。

主题的定制化
选择Custom选项后,您可以自定义应用程序的主题(颜色)。
让我们这次尝试使用外部工具Material Design调色板生成器来创建主题。
请访问 http://mcg.mbitson.com/,选择您喜欢的配色,创建称为”调色板”的配色模式。
基于每个调色板,选择一种基本颜色,会自动创建基于该颜色的明暗变化的多样化选项。在这里,我们以primary、accent和warn为例,创建了三个调色板,并分别选择了#04127C、#A0D8AD和#BE375A作为它们的基色。

完成调色板后,将其应用到Angular项目中。
点击工具的右上角的 “↓” 按钮后,将会显示如下图所示的 “Output Formats” 对话框,然后从侧边菜单中选择 “ANGULAR JS 2 (MATERIAL 2)”,最后点击对话框右上角的 “COPY” 按钮。
现在,调色板的代码已经以SCSS格式复制到剪贴板中了。

接下来,在Angular项目的assets文件夹下创建一个名为palette.scss的新文件,将复制的调色板全部粘贴并保存覆盖。

以下是复制并粘贴的 “palette.scss” 的示例图(在这个编辑器中,为了能够完整显示,我进行了分割显示,但事实上,它是一个整体文件)。

最后,为了将创建的调色板应用到Angular项目中,我们需要按照以下方式编辑styles.scss文件。
// Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming
@use '@angular/material' as mat;
// Plus imports for other components in your app.
+ @use "./assets/palette" as palette;
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat.core();
// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
- $sample-primary: mat.define-palette(mat.$indigo-palette);
+ $sample-primary: mat.define-palette(palette.$md-primary);
- $sample-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
+ $sample-accent: mat.define-palette(palette.$md-accent);
// The warn palette is optional (defaults to red).
- $sample-warn: mat.define-palette(mat.$red-palette);
+ $sample-warn: mat.define-palette(palette.$md-warn);
// Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography".
$sample-theme: mat.define-light-theme((
color: (
primary: $sample-primary,
accent: $sample-accent,
warn: $sample-warn,
)
));
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include mat.all-component-themes($sample-theme);
/* You can add global styles to this file, and also import other style files */
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
准备运动已经完成,辛苦了。
设计一个漂亮的导航栏
接下来,我们将以之前介绍的Angular Material作为实践示例,来构建一个导航栏。
在展示完整形式之前,任何人都可以轻松地创建这种互动体验。

在Angular Material中,有一个名为”Schematics”的自动代码生成功能,可以帮助我们建立一个相当完整的用户界面,而几乎不需要编写太多的代码。关于Schematics的详细信息可以参考Angular Material的文档。
生成导航栏代码的命令如下所示。
> ng generate @angular/material:navigation navigation
执行上述命令后,将在navigation文件夹下自动生成三个文件。

从这里开始,我们将对现有的源代码进行修改,以便在屏幕上显示我们创建的导航。
我们首先需要从 app.component.ts 中引用导航,可以像下面这样添加导入:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
+ import { NavigationComponent } from './navigation/navigation.component';
@Component({
selector: 'app-root',
standalone: true,
- imports: [CommonModule, RouterOutlet],
+ imports: [CommonModule, RouterOutlet, NavigationComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
- title = 'sample';
}
再者,将app.component.html替换为以下内容3。这样,导航就被放置在了app.component.html的最上方。
<app-navigation>
<router-outlet></router-outlet>
</app-navigation>
接下来,打开navigation.component.html文件,并进行以下修改4。
<mat-sidenav-container class="sidenav-container">
<mat-sidenav #drawer class="sidenav" fixedInViewport
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
[mode]="(isHandset$ | async) ? 'over' : 'side'"
[opened]="(isHandset$ | async) === false">
<mat-toolbar>Menu</mat-toolbar>
<mat-nav-list>
<a mat-list-item href="#">Link 1</a>
<a mat-list-item href="#">Link 2</a>
<a mat-list-item href="#">Link 3</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar color="primary">
@if (isHandset$ | async) {
<button
type="button"
aria-label="Toggle sidenav"
mat-icon-button
(click)="drawer.toggle()">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
}
<span>sample</span>
</mat-toolbar>
<!-- Add Content Here -->
+ <div class="main-container">
+ <ng-content></ng-content>
+ </div>
</mat-sidenav-content>
</mat-sidenav-container>
到目前为止,最基本的导航栏已经完成了。
需要注意的是,由于这个规范更改的影响,@、{和}等字符在模板中具有特殊意义,因此无法直接使用这些字符,必须进行转义。@转义为@,{转义为{,}转义为}。
确认行动
为了确认到目前为止的工作内容,让我们在浏览器中检查一下页面的外观。
输入以下命令到命令提示符中,Angular项目将被构建并启动浏览器以进行验证。
> ng serve --open
执行结果将会是下面这样的感觉。

这个本身还可以,但老实说,如果要集成到销售系统中,外观有点简陋。我们将通过专业技术将其改进为更美观的用户界面。
问题1:导航栏被滚动条遮挡。
当我们意识到应用程序的内容真正变得丰富时,我们会发现由Schematics生成的导航栏与滚动条相比相形见绌(见下图红框)。

希望页面顶部固定位置的导航栏在滚动时可以超越页面内容,因此我们想要调整滚动条的上端位置,以避免与下面所示的滚动条重叠。

在这种情况下,首先要对navigation.component.scss进行以下编辑。
.sidenav-container {
height: 100%;
}
.sidenav {
width: 200px;
}
.sidenav .mat-toolbar {
background: inherit;
}
.mat-toolbar.mat-primary {
position: sticky;
top: 0;
z-index: 1;
}
+ mat-sidenav-content {
+ height: 100%;
+ display: grid;
+ grid-template-rows: max-content 1fr;
+ }
+ .main-container {
+ overflow: auto;
+ }
此外,为了视觉上显示导航栏比其他组件更加突出,我们可以轻松地添加一些阴影。
在Angular Material中,定义了CSS类mat-elevation-z0~mat-elevation-z24用于添加阴影效果,通过将其应用于导航栏可以产生立体的视觉效果(在这里我们选择指定mat-elevation-z8以突出效果)。
<mat-sidenav-container class="sidenav-container">
<mat-sidenav #drawer class="sidenav" fixedInViewport
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
[mode]="(isHandset$ | async) ? 'over' : 'side'"
[opened]="(isHandset$ | async) === false">
<mat-toolbar>Menu</mat-toolbar>
<mat-nav-list>
<a mat-list-item href="#">Link 1</a>
<a mat-list-item href="#">Link 2</a>
<a mat-list-item href="#">Link 3</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
- <mat-toolbar color="primary">
+ <mat-toolbar color="primary" class="mat-elevation-z8">
@if (isHandset$ | async) {
<button
type="button"
aria-label="Toggle sidenav"
mat-icon-button
(click)="drawer.toggle()">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
}
<span>sample</span>
</mat-toolbar>
<!-- Add Content Here -->
<div class="main-container">
<ng-content></ng-content>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
执行结果如下所示。导航栏的存在感更加突出。

問題2: 无法关闭侧边菜单。
使用Schematics自动生成导航栏时,侧边菜单(如下图红框所示)也会一同构建。

然而,这个侧边菜单有一些特色,不一定非常方便使用。
例如,如果在常见的个人电脑浏览器中打开应用程序,侧边菜单会被强制显示,并且没有关闭的方法。
然而,在通常情况下,可能会有一些需要将菜单隐藏起来,并根据需要进行互动显示或隐藏的情况。
因此,在本节中,我们将修改导航栏以实现自由展开和折叠的侧边菜单。只需要修改模板中的三个地方就可以实现这一功能。
每个修改的内容都大致如下。
-
- 将启动时的初始状态设置为菜单隐藏
-
- 在菜单的右上方放置一个×按钮
-
- → 这样就可以关闭菜单
-
- 修改显示菜单的三条线按钮的显示条件
- → 其实原本为了在智能手机上打开菜单而编码了按钮,现在只需修改条件使其在电脑上也能使用。
<mat-sidenav-container class="sidenav-container">
<mat-sidenav #drawer class="sidenav" fixedInViewport
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
[mode]="(isHandset$ | async) ? 'over' : 'side'"
- [opened]="(isHandset$ | async) === false">
+ [opened]="false">
- <mat-toolbar>Menu</mat-toolbar>
+ <mat-toolbar>Menu
+ <span style="flex: 1 1 auto;"></span>
+ <button mat-icon-button (click)="drawer.close()">
+ <mat-icon>close</mat-icon>
+ </button>
+ </mat-toolbar>
<mat-nav-list>
<a mat-list-item href="#">Link 1</a>
<a mat-list-item href="#">Link 2</a>
<a mat-list-item href="#">Link 3</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar color="primary" class="mat-elevation-z8">
- @if (isHandset$ | async) {
+ @if (!drawer.opened) {
<button
type="button"
aria-label="Toggle sidenav"
mat-icon-button
(click)="drawer.toggle()">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
}
<span>sample</span>
</mat-toolbar>
<!-- Add Content Here -->
<div class="main-container">
<ng-content></ng-content>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
通过这个修正,导航菜单的左侧将显示一个用于打开侧边菜单的三条横线按钮。

另外,现在在侧边菜单的右上方添加了一个“X”按钮来关闭菜单。

通过这样做,折叠菜单可以充分利用应用程序的宽度空间。
問題3: 导航系统太普通了。
这是一个个人喜好的问题,但是例如在引入系统的企业中加入他们的主题颜色或标志,仅仅这样就能显著增加独特性。
要进行这个改造,需要进行一些准备工作。首先,准备一个合适的图片(例如系统的标志)并将其放置在assets文件夹下。文件夹结构如下所示。

接下来,我们要为导航栏设置渐变的背景色。在 navigation.component.scss 文件中,我们需要添加一个新的类。
.sidenav-container {
height: 100%;
}
.sidenav {
width: 200px;
}
.sidenav .mat-toolbar {
background: inherit;
}
.mat-toolbar.mat-primary {
position: sticky;
top: 0;
z-index: 1;
}
mat-sidenav-content {
height: 100%;
display: grid;
grid-template-rows: max-content 1fr;
}
.main-container {
overflow: auto;
}
+ .toolbar-background {
+ background: linear-gradient(82deg, #04127C 35%, #65B3A4 90%, #A0D8AD 100%);
+ }
最后,我们将修改模板。在导航栏的样式中,设置之前提到的CSS类,并放置系统的标志。
<mat-sidenav-container class="sidenav-container">
<mat-sidenav #drawer class="sidenav" fixedInViewport
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
[mode]="(isHandset$ | async) ? 'over' : 'side'"
[opened]="false">
<mat-toolbar>Menu
<span style="flex: 1 1 auto;"></span>
<button mat-icon-button (click)="drawer.close()">
<mat-icon>close</mat-icon>
</button>
</mat-toolbar>
<mat-nav-list>
<a mat-list-item href="#">Link 1</a>
<a mat-list-item href="#">Link 2</a>
<a mat-list-item href="#">Link 3</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
- <mat-toolbar color="primary" class="mat-elevation-z8">
+ <mat-toolbar color="primary" class="mat-elevation-z8 toolbar-background">
+ <mat-toolbar-row>
@if (!drawer.opened) {
<button
type="button"
aria-label="Toggle sidenav"
mat-icon-button
(click)="drawer.toggle()">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
}
- <span>sample</span>
+ <img src="/assets/logo.png" style="height: 1rem;">
+ <span style="flex: 1 1 auto;"></span>
+ <button mat-button class="mat-headline-4">
+ <mat-icon>account_circle</mat-icon>
+ 日電 太郎
+ </button>
+ </mat-toolbar-row>
</mat-toolbar>
<!-- Add Content Here -->
<div class="main-container">
<ng-content></ng-content>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
执行到这一步的内容,结果如下。外观看起来相当不错。关于导航栏,目前暂且就这样吧。

只需稍微改动一点点,就能展现出如此独特性,因此在制作模型等时,尝试运用这个方法可能会让客户感到高兴。
生产装载部件
在这里,我们之前只是对自动生成的代码进行了一些小小的修改,接下来我们要进入正式的阶段了,即”本気ガチ篇”。我们将开始创建用于在数据加载期间让用户感到愉悦的UI组件,俗称”ぐるぐる”。
在需要进行耗时的搜索处理等操作时,如果能够巧妙地利用,可以减轻用户的感知压力,因此在关键时刻提前准备好非常有用。

这个部件看起来似乎很简单,但实际上是借助Angular CDK的遮罩效果和动画模块精心制作的复杂组件。
在设计中,我们不仅关注了外观的表现,还特别注重了产品的易用性和通用性作为组件的方面。
那么,让我们立即来看一下制作方法。
> ng generate component loading
> ng generate service loading/loading
执行上述命令后,将在loading文件夹下生成组件和服务的集合。 这些将成为组件的主体。

下一步,我们需要进行必要的设置才能进行叠加。
请在styles.scss文件的“/* 您可以在此文件中添加全局样式,并导入其他样式文件 */”注释的下一行添加以下内容:
@import '@angular/cdk/overlay-prebuilt.css';
接下来,让我们来写一个控制覆盖组件显示/隐藏的服务loading.service。
实现show()和hide()这两个方法作为服务的公开方法,并在每个方法被调用时执行更改旋转状态的逻辑。
由于Guruguru中有动画显示,所以不会立即切换显示/隐藏,而是在出现时有一个持续100毫秒的“柔和过渡”,在消失时有一个持续1000毫秒的“柔和过渡”。为了防止在这个短暂的时间内连续调用show()和hide()时发生奇怪的事情,在内部进行了相当繁琐的控制。
import { Overlay } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { Injectable, inject } from '@angular/core';
import { LoadingComponent } from './loading.component';
@Injectable({ providedIn: 'root' })
export class LoadingService {
// 画面全体を覆うようにオーバレイを作成
private readonly overlay = inject(Overlay);
private readonly overlayRef = this.overlay.create({ height: '100%', width: '100%' });
private readonly duration = 1000;
private _visible: boolean = false;
private _timeoutId?: any;
// 可視状態フラグ
get visible() {
return this._visible;
}
// ぐるぐるをオーバレイ表示する
show() {
if (!this._timeoutId) {
clearTimeout(this._timeoutId);
this.overlayRef.detach();
this._timeoutId = undefined;
}
this._visible = true;
this.overlayRef.attach(new ComponentPortal(LoadingComponent));
}
// ぐるぐるを非表示にする
hide() {
this._visible = false;
this._timeoutId = this._timeoutId || setTimeout(() => {
this.overlayRef.detach();
this._timeoutId = undefined;
}, this.duration);
}
}
接下来,我们来编写用于显示「轮转」效果的SCSS代码。我们会将组件的宽度和高度都设置为100%,以覆盖整个显示区域,并将「轮转」组件居中放置在其中。
另外,为了突显旋转的感觉,通过让背景变模糊来制造出远近感。
:host {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: none;
}
.main-container {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: none;
/* 背景をぼかす */
backdrop-filter: saturate(25%) blur(5px);
background-color: rgba(255, 255, 255, 0.3);
/* コンポーネントを上下左右中心に配置する */
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
接下来是组件的实现。在这里导入的MatProgressSpinnerModule是”转圈圈”的真正身份。
同样,在“转转”显示/隐藏切换的时机触发一个细微的淡入/淡出的动画也在此定义。
在用户的视角下,由于具有淡入淡出效果,使突然感减轻,所以推荐不仅仅限于此的适度动画。
import { Component, inject } from '@angular/core';
import { animate, style, transition, trigger } from "@angular/animations";
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { LoadingService } from './loading.service';
@Component({
selector: 'app-loading',
standalone: true,
imports: [MatProgressSpinnerModule],
templateUrl: './loading.component.html',
styleUrl: './loading.component.scss',
animations: [
trigger('foreTrigger', [
transition(':enter', [
style({ opacity: 0 }),
animate('100ms', style({ opacity: 1 })),
]),
transition(':leave', [
style({ opacity: '*' }),
animate('1000ms', style({ opacity: 0 })),
]),
])
]
})
export class LoadingComponent {
readonly loading = inject(LoadingService);
}
最后,是模板的标记语言。我们使用@if来切换显示/隐藏,并将动画触发器绑定到该切换的时机上,以实现淡入淡出效果。
@if(loading.visible) {
<div @foreTrigger class="main-container">
<div>
<mat-spinner strokeWidth="15"></mat-spinner>
</div>
</div>
}
到目前为止,我们已经完成了旋转部件。虽然部件内部结构错综复杂,但相应地,使用方法非常简单。
转动零件的使用方法
让我们现在开始看一下如何正确使用这些旋转零件。
主要是以组件为例简单解释如下:
-
- 可以使用DI将LoadingService注入(无论是通过inject()函数还是构造函数参数)
调用LoadingService#show()会显示一个旋转的图形
调用LoadingService#hide()会隐藏旋转的图形
只是这样而已。为了保险起见,下面给出一个伪代码示例。非常简单对吧。
import { Component } from '@angular/core';
import { LoadingService } from './loading/loading.service';
import { inject } from '@angular/core';
@Component({ /* 略 */ })
export class AppComponent {
// LoadingServiceをインジェクト
private readonly loading = inject(LoadingService);
/* 中略 */
// ぐるぐるを表示させたいとき
this.loading.show();
// ぐるぐるを非表示にしたいとき
this.loading.hide();
}
总结
你觉得怎么样呢?
在这次活动中,我们使用Angular的新功能和Angular Material,介绍了如何开发可以在任何应用中都能发挥作用的组件。
希望让更多的人了解变得更加强大的Angular版本17以及仅需稍加定制便能大幅改进的Angular Material的魅力。
我希望继续从前端到后端为大家介绍有趣的小知识。
非常感谢您阅读到最后。
请删除app.component.html中原有的所有源代码,并用本文中示例的源代码进行替换。↩
由于接下来的说明,现在先指定一个叫做main-container的CSS类,但请暂时不用关心它。↩
然而,如果滥用的话反而会让人讨厌,所以要注意不要使用过度。↩
当在实际的项目中使用时,如果过度模糊化会显得很做作,而且还会降低前端性能,所以要适度调整。↩