让我们实现一个像 Angular Material Site 一样的主题切换🎨
欢迎来到Dev.to难波!
免责声明:撰写本文时,我会尽量详细地阐述我的实现方法。所以,如果您是 Angular 新手或刚刚开始学习,希望您能够顺利阅读。如果您对本文仍有任何理解上的问题,请随时在文章下方留言。我非常乐意为您更详细地讲解。🙂
话虽如此,本文主要面向初学者。为了以防万一您是更高级的 Angular 开发者,我在下面添加了一个TL;DR; 👇🏻。您可以随意浏览您可能更感兴趣的标题。
TL;DR;
为什么要切换主题?🤷🏻♂️
好吧!黑暗模式其实不算什么新鲜炫酷的功能。大多数网站都把它作为自定义主题添加到自己的应用中。
他们为什么不这么做呢?黑暗模式太棒了!它看起来更舒服,更省电,还能提供极佳的用户体验(尤其是在低光照条件下)。
是的!你没听错。昏暗的氛围最适合使用黑暗模式。我的意思是,即使在光线充足的环境中,也没人会阻止你切换到黑暗模式。不过,黑暗模式在黑暗中看起来更舒服,也更直观。(明白我的意思了吗?😉)
另外,我之前提到过黑暗模式可以增强用户体验,对吧?其实,它有很多方面。在不影响设计语言的前提下,为用户提供一种自定义应用外观和体验的方式总是更好的。大多数 Web 应用(或者说,一般的应用)都是通过主题来实现这一点的。
另一个方面对某些人来说可能或多或少看起来像个噱头。但你可以通过根据用户所处的环境光照条件智能切换主题,将 Web 应用的用户体验提升到一个新的水平。我稍后会再谈到这一点。
我最喜欢的实现主题的网站之一是Angular Material Site。你可能已经看到过这个开关,它可以让你在网站上更改主题。

我们将在 Angular 应用中复制几乎相同的效果。事不宜迟,让我们开始吧。
设置
我已经在 StackBlitz 上设置了 Angular Material,您可以将其用作入门模板:
从现在开始,让我们添加一些 Angular Material 组件,用于在 UI 上显示一些内容。我将添加一个工具栏、一个图标、一个主题选项菜单和一个按钮。
由于所有这些 Angular Material 组件都将在我的中使用AppModule
,因此创建一个单独的AppMaterialModule
组件并从中重新导出所有与 Material 相关的模块是有意义的。
app-material.module.ts
...
import { MatButtonModule } from "@angular/material/button";
import { MatIconModule } from "@angular/material/icon";
import { MatMenuModule } from "@angular/material/menu";
import { MatToolbarModule } from "@angular/material/toolbar";
...
@NgModule({
exports: [
MatButtonModule,
MatIconModule,
MatMenuModule,
MatToolbarModule,
]
})
export class AppMaterialModule {}
现在我可以将添加AppMaterialModule
到imports
我的数组中AppModule
。
app.module.ts
...
import { AppMaterialModule } from "./app-material.module";
...
@NgModule({
imports: [
...
AppMaterialModule,
...
],
...
})
export class AppModule {}
注意:我在这里这样做是因为我将在我的项目中使用这些 Angular Material 模块暴露的所有 Angular Material 组件
AppModule
。这在实际应用中没有多大意义,因为我们通常不会在所有模块中使用所有 Angular Material 组件。因此,创建一个单独的组件AppMaterialModule
,然后在每个 Angular 模块中导入它可能会导致性能下降。所以在这种情况下,你可能需要避免这样做。
接下来,我现在应该能够在我的应用中使用这些 Angular Material 组件了。我想要的效果非常简单。就是这样👇🏻
从上图来看,我们需要一个HeaderComponent
,一个MenuComponent
在点击🎨图标时打开的,其余部分已经由我们的示例 StackBlitz 容纳。
实施HeaderComponent
:
我计划将其做成一个智能组件。
附言:你可以从Stephen Fluin的这个视频📺中了解有关智能和愚蠢组件模式的更多信息
好的,现在继续我们的HeaderComponent
,它需要将菜单的一些选项传递给MenuComponent
。每个选项都会包含 、 、 和 等内容,backgroundColor
用于buttonColor
显示headingColor
每个菜单项上的图标;以及label
、 和 ,分别value
对应于每个标签。
现在我们知道Angular Material 有 4 个预建主题,分别是:
deeppurple-amber.css
indigo-pink.css
pink-bluegrey.css
purple-green.css
因此,我们需要 4 个选项。为了避免在组件本身中对这些选项进行硬编码,我只需将这些数据公开为 JSON 文件,并将其存储在assets
名为 的文件夹中options.json
。这样做可以让我通过路径获取它。/assets/options.json
该文件看起来是这样的:
options.json
[
{
"backgroundColor": "#fff",
"buttonColor": "#ffc107",
"headingColor": "#673ab7",
"label": "Deep Purple & Amber",
"value": "deeppurple-amber"
},
{
"backgroundColor": "#fff",
"buttonColor": "#ff4081",
"headingColor": "#3f51b5",
"label": "Indigo & Pink",
"value": "indigo-pink"
},
{
"backgroundColor": "#303030",
"buttonColor": "#607d8b",
"headingColor": "#e91e63",
"label": "Pink & Blue Grey",
"value": "pink-bluegrey"
},
{
"backgroundColor": "#303030",
"buttonColor": "#4caf50",
"headingColor": "#9c27b0",
"label": "Purple & Green",
"value": "purple-green"
}
]
注意:考虑到这些数据是以 REST API 的形式公开的,我可以在自己的应用中获取它
HttpClient
。这就是我在本文中要做的事情。或者,您也可以将其作为静态资产使用,而不是通过 REST API 公开此数据。如果这样做,您可以直接在
HeaderComponent
using中导入它import options from 'path-to-options.json'
。你只需要在/中的中设置
resolveJsonModule
和esModuleInterop
即可。如果你感兴趣的话,我在这个示例 StackBlitz中有一个这个版本的实现。🙂true
compilerOptions
tsconfig.app.json
tsconfig.json
好的。我们继续。现在,既然我已经有了option
对象的结构,我可以创建一个interface
用于静态类型的代码。我们把它存储在一个名为的文件中option.model.ts
:
option.model.ts
export interface Option {
backgroundColor: string;
buttonColor: string;
headingColor: string;
label: string;
value: string;
}
完美!现在的职责HeaderComponent
是:
- 渲染标题(显然!🤷🏻♂️)
- 获取选项并将其提供给
MenuComponent
。
但我们确实需要在某个时候更改主题。所以最好将与主题相关的整个业务逻辑抽象到一个服务中,我称之为ThemeService
。那么,我们先来实现它:
theme.service.ts
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import { Option } from "./option.model";
@Injectable()
export class ThemeService {
constructor(
private http: HttpClient,
) {}
getThemeOptions(): Observable<Array<Option>> {
return this.http.get<Array<Option>>("assets/options.json");
}
setTheme(themeToSet) {
// TODO(@SiddAjmera): Implement this later
}
}
太棒了!我们现在可以将此服务作为依赖项注入,HeaderComponent
如下所示:
header.component.ts
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Option } from "../option.model";
import { ThemeService } from "../theme.service";
@Component({
selector: "app-header",
templateUrl: "./header.component.html",
styleUrls: ["./header.component.css"]
})
export class HeaderComponent implements OnInit {
options$: Observable<Array<Option>> = this.themeService.getThemeOptions();
constructor(private readonly themeService: ThemeService) {}
ngOnInit() {
this.themeService.setTheme("deeppurple-amber");
}
themeChangeHandler(themeToSet) {
this.themeService.setTheme(themeToSet);
}
}
如您所见,HeaderComponent
现在还负责更改主题。
模板看起来是这样的:
header.component.html
<mat-toolbar color="primary">
<mat-toolbar-row>
<span>Dora</span>
<span class="spacer"></span>
<app-menu
[options]="options$ | async"
(themeChange)="themeChangeHandler($event)">
</app-menu>
</mat-toolbar-row>
</mat-toolbar>
注意,我们没有直接在组件类中subscribe
传入,而是使用了管道来解包。这是一种让 Angular 具有响应式特性的模式,你应该尽可能地遵循这种模式。获取选项后,我们就可以将其作为输入传递给 的属性。options$
Observable
async
MenuComponent
options
@Input
另外,由于 也承担了更改主题的责任HeaderComponent
,我们可以将 实现MenuComponent
为一个哑/展示组件。现在就开始吧。
实施MenuComponent
:
现在我们可以知道它MenuComponent
会接受options
一个@Input
,然后遍历它们来渲染这些选项。我们还可以清楚地看到它有一个themeChange
@Output
属性,它会使用新选择的主题来调用处理程序。因此,我们可以MenuComponent
像这样实现这个类:
menu.component.ts
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Option } from "../option.model";
import { ThemeService } from "../theme.service";
@Component({
selector: "app-menu",
templateUrl: "./menu.component.html",
styleUrls: ["./menu.component.css"]
})
export class MenuComponent {
@Input() options: Array<Option>;
@Output() themeChange: EventEmitter<string> = new EventEmitter<string>();
constructor(private themeService: ThemeService) {}
changeTheme(themeToSet) {
this.themeChange.emit(themeToSet);
}
}
模板看起来是这样的:
menu.component.html
<mat-icon
class="icon"
[matMenuTriggerFor]="menu">
palette
</mat-icon>
<mat-menu #menu="matMenu">
<button
*ngFor="let option of options"
mat-menu-item
(click)="changeTheme(option.value)">
<mat-icon
role="img"
svgicon="theme-example"
aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%"
height="100%"
viewBox="0 0 80 80"
fit=""
preserveAspectRatio="xMidYMid meet"
focusable="false">
<defs>
<path
d="M77.87 0C79.05 0 80 .95 80 2.13v75.74c0 1.17-.95 2.13-2.13 2.13H2.13C.96 80 0 79.04 0 77.87V2.13C0 .95.96 0 2.13 0h75.74z"
id="a">
</path>
<path
d="M54 40c3.32 0 6 2.69 6 6 0 1.2 0-1.2 0 0 0 3.31-2.68 6-6 6H26c-3.31 0-6-2.69-6-6 0-1.2 0 1.2 0 0 0-3.31 2.69-6 6-6h28z"
id="b">
</path>
<path d="M0 0h80v17.24H0V0z" id="c"></path>
</defs>
<use xlink:href="#a" [attr.fill]="option.backgroundColor"></use>
<use xlink:href="#b" [attr.fill]="option.buttonColor"></use>
<use xlink:href="#c" [attr.fill]="option.headingColor"></use>
</svg>
</mat-icon>
<span>{{ option.label }}</span>
</button>
</mat-menu>
好了!现在一切都准备就绪。我们只需要一个切换主题的方法。该怎么做呢?
实现主题切换🎨
这是拼图的最后一块。我们可以通过几种不同的方式来实现。Angular Material 官网已经实现了这个功能,对吧?而且好消息是,它是开源的。所以我们可以访问源代码。
因此,我不会尝试重新发明轮子,而是稍微作弊一下,看看 Angular Material Docs App 是如何做到的。
Angular Material 网站是如何做到的?🤔
如果你检查实际的实现,你会发现他们实现了一种叫做 的东西ThemePicker
。这就是我们在右上角的标题中看到的内容。
顾名思义,该组件负责切换网站的主题。该组件调用一个名为 的服务StyleManager
。
你可能会问,这项服务是做什么的?好吧,当你从它更改主题时ThemePicker
:
- 检查 HTML 文档中是否存在带有
class
属性的链接标签,其值为style-manager-theme
::- 如果不存在这样的
link
标签,它会将此link
标签添加到文档的头部,然后href
在其上设置所选主题路径的属性。 - 如果存在这样的
link
标签,那么它只需将此标签href
上的属性设置link
为所选的主题路径。
- 如果不存在这样的
太好了,现在我们明白了它的作用StyleManager
,我可以直接把这个StyleManager
服务复制到我的项目中。复制完成后,我就可以把它注入到我的项目中ThemeService
,然后用相应的值调用setStyle
它的方法,理想情况下它应该可以正常工作。
那么让我们尝试一下吧。
我们的实施
我将首先把 style-manager.ts 复制到一个名为 style-manager.service.ts 的文件中:
style-manager.service.ts
/**
* Copied from https://github.com/angular/material.angular.io/blob/master/src/app/shared/style-manager/style-manager.ts
* TODO(@SiddAjmera): Give proper attribution here
*/
import { Injectable } from "@angular/core";
@Injectable()
export class StyleManagerService {
constructor() {}
/**
* Set the stylesheet with the specified key.
*/
setStyle(key: string, href: string) {
getLinkElementForKey(key).setAttribute("href", href);
}
/**
* Remove the stylesheet with the specified key.
*/
removeStyle(key: string) {
const existingLinkElement = getExistingLinkElementByKey(key);
if (existingLinkElement) {
document.head.removeChild(existingLinkElement);
}
}
}
function getLinkElementForKey(key: string) {
return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
}
function getExistingLinkElementByKey(key: string) {
return document.head.querySelector(
`link[rel="stylesheet"].${getClassNameForKey(key)}`
);
}
function createLinkElementWithKey(key: string) {
const linkEl = document.createElement("link");
linkEl.setAttribute("rel", "stylesheet");
linkEl.classList.add(getClassNameForKey(key));
document.head.appendChild(linkEl);
return linkEl;
}
function getClassNameForKey(key: string) {
return `app-${key}`;
}
太好了。既然我已经按照计划准备好了这项服务,我会将它作为依赖项注入到我的项目中ThemeService
,并实现以下setTheme
方法:
主题.服务.ts
...
import { StyleManagerService } from "./style-manager.service";
@Injectable()
export class ThemeService {
constructor(
...
private styleManager: StyleManagerService
) {}
...
setTheme(themeToSet) {
this.styleManager.setStyle(
"theme",
`node_modules/@angular/material/prebuilt-themes/${themeToSet}.css`
);
}
}
我在这里所做的就是使用样式键的名称(在本例中为主题)以及它必须设置的属性的值来调用该setStyle
方法。StyleManagerService
href
该setStyle
方法再次创建一个新link
标签,然后href
在其上设置属性;或者更新href
预先存在的link
标签上的属性。
差不多就是这样了。这就是我们最终的代码。
太棒了!我们现在有了主题切换功能,就像 Angular Material 网站上的一样。而且它运行正常 😍
下一步👣
这一切都很棒。但如果我们的应用能够根据环境光自动切换主题,那岂不是很棒?🤔 嗯,这正是我们下一篇文章要做的事情。
等不及了?现在就可以读。链接如下:


在你的 Angular 应用中,像狐狸一样根据环境光💡切换主题🦊
Siddharth Ajmera 🇮🇳 for Angular ・ 2020 年 3 月 23 日 ・ 阅读时间 9 分钟
结束语🎉
哇!你还在吗?谢谢你的关注。希望你喜欢。
我非常感谢Martina Kraus和Rajat Badjatya花时间校对本文并提供所有建设性反馈,使本文更加完善。
希望本文能让你学到一些与 Angular 相关的新知识。如果觉得有用,请点击🧡/🦄图标,并将其添加到你的阅读列表(🔖)。也欢迎分享这篇文章给你刚接触 Angular 并想实现类似目标的朋友。
我正在上传本文的视频版本,请继续关注。📺
图标来源:AngularIO Press Kit | CSS by Monkik,来自 Noun Project
那么下次再见。👋🏻
文章来源:https://dev.to/angular/angular-apps-with-a-theme-switch-like-the-ngular-material-site-darkmode-3mno