前段时间有人提出,希望工具箱可以加入暗色皮肤。在热心网友的帮助下实现了一份,但问题是把原来的主题覆盖掉了……为了不让热心网友的心血浪费,花了一下午时间突击学习了Angular换肤的实现。

其实从本质上来说,换肤不过是动态切换一下页面所引用的CSS文件而已,这并不难。但在工具箱的实现里,这很麻烦,因为使用了基于MDC-WebcomponentBlox Material组件库,这套组件库使用SCSS,在编译期根据预先设置好的变量,动态生成各种中间色等等。详细来说,这套组件库的原理是:

  1. 自定义$mdc-theme(-primary, -secondary, -surface, -background, -on-primary, -on-secondary, -on-surface)等等颜色变量;
  2. 在根styles.scss中先引入上述自定义变量,再引入MDC库;
  3. MDC库会使用Mixins在编译时使用上述变量(如果没有的话就用缺省变量),生成很多中间色(比如$mdc-theme-text-primary-on-background等);
  4. 编译成为一个主题文件styles.{HASH}.css,自动插入到页面头部。

由于这种生成方式原理上无法支持“运行时使用JS修改SCSS变量”,所以剩下的只有一种办法:生成两套主题,运行时切换CSS文件。

查了很久之后终于在这里找到了可用的方法。但实际上还是需要对这篇文章里的内容做一些调整。

  1. 创建dark.scss & light.scss:
    Dark & Light Scss File
  2. 在根目录的angular.json中,projects/{project-name}/architect/build/options中需要添加两项:"extractCss" : "true",以及"styles"下指定每个CSS文件的输出名和Lazy。 extractCss是为了在DEBUG时(ng serve -o)就生成单独的css文件(否则会提示找不到),而styles中的每一项代表一个主题的输出文件,lazy则表示是否直接插入到index.html的开头中去,true则不会自动加入(我们要在后面手动加入):
    angular.json preview
  3. 创建一个Service来切换主题(ng generate service switch-theme)。这里我对原文中的代码做了小幅修改,这样将来如果有多套主题,切换更方便。
import { Injectable, RendererFactory2, Renderer2, Inject } from '@angular/core';
import { Observable, BehaviorSubject, combineLatest } from 'rxjs';
import { DOCUMENT } from '@angular/common';

@Injectable({
  providedIn: 'root'
})

// Copied and modified from https://medium.com/better-programming/angular-multiple-themes-without-killing-bundle-size-with-material-or-not-5a80849b6b34

export class SwitchThemeService {
  private _theme: BehaviorSubject<string> = new BehaviorSubject("light");

  private _renderer: Renderer2;
  private head: HTMLElement;
  private themeLinks: HTMLElement[] = [];

  theme$: Observable<string>;

  constructor(
    rendererFactory: RendererFactory2,
    @Inject(DOCUMENT) document: Document
  ) {
    this.head = document.head;
    this._renderer = rendererFactory.createRenderer(null, null);
    this.theme$ = this._theme;
    this.theme$.subscribe(async (target) => {
      const cssFilename = target + ".css";
      await this.loadCss(cssFilename);
      if (this.themeLinks.length == 2)
        this._renderer.removeChild(this.head, this.themeLinks.shift());
    })
  }

  setTheme(name: string) {
    this._theme.next(name);
  }

  private async loadCss(filename: string) {
    return new Promise(resolve => {
      const linkEl: HTMLElement = this._renderer.createElement('link');
      this._renderer.setAttribute(linkEl, 'rel', 'stylesheet');
      this._renderer.setAttribute(linkEl, 'type', 'text/css');
      this._renderer.setAttribute(linkEl, 'href', filename);
      this._renderer.setProperty(linkEl, 'onload', resolve);
      this._renderer.appendChild(this.head, linkEl);
      this.themeLinks = [...this.themeLinks, linkEl];
    })
  }
}

使用起来也很简单,只要引入Service,然后调用setTheme("{theme-name}")即可,比如:

export class AppComponent {
  constructor(private switchTheme: SwitchThemeService,
              private fetchService: FetchService) {
    ...
    // FetchService是内部使用的获取数据的Service,下面这行的意思是获取localStorage中名为'theme'的项,若没有则返回'dark'
    this.theme = this.fetchService.getLocalStorage('theme','dark');
    switchTheme.setTheme(this.theme);
  }
}
  1. 在页面内添加对应的按钮和函数实现,这样即可实现主题的动态切换了。如果使用了localStorage作为存储,别忘了更改主题后及时保存。
Last modification:March 4th, 2020 at 08:22 pm
本文作者:Graue Neko

本文链接:Angular 8.0下更换主题功能的实现 - https://graueneko.com/archives/32/

版权声明:如无特别声明,本文即为原创文章,仅代表个人观点,版权归 灰格猫的编程日记 所有,未经允许不得转载。