头图

Angular Material 主题系统(三)--多主题切换

locotor

上一篇介绍了如何自定义 Material 主题,现在来看看怎么定义多个主题,并在运行时动态切换。
可以采用官网介绍的类名包裹方式,或者我们也可以预先编译,按需引入。话不多说,let's rock the code!😎

按类名切换明暗风格

如上篇所言,我们首先就来实现怎么切换网站的暗黑主题。
注意✨:可以在 styles.scss 中调用 angular-material-theme mixin 多次,来生成多套不同的主题,但 @mat-core 始终只能调用一次。

创建两个主题

咱们先创建两个主题,一个明亮风格的主题作为默认,另一个暗黑风格的主题放在 .unicorn-dark-theme 这个类名中。这样一来,任何在该类名内的元素,都会应用暗黑风格的主题样式。

@import '~@angular/material/theming';
@include mat-core();

// 定义一个自定义颜色配置 (和前一篇一样)
$candy-app-primary: mat-palette($mat-indigo);
$candy-app-accent:  mat-palette($mat-pink, A200, A100, A400);
$candy-app-theme:   mat-light-theme((
  color: (
    primary: $candy-app-primary,
    accent: $candy-app-accent,
  )
));

// 包含默认主题颜色以及文字排版
@include angular-material-theme($candy-app-theme);

// 定义另一个暗黑风格的主题
$dark-theme:   mat-dark-theme((
  color: (
    primary: $candy-app-primary,
    accent: $candy-app-accent,
  )
));

// 用一个类名包裹暗黑主题样式
.unicorn-dark-theme {
  @include angular-material-color($dark-theme);
}

注意✨:暗黑主题采用 angular-material-color,而不是默认主题使用的 angular-material-theme,因为后者不光会生成主题颜色的样式,还会生成其他样式。这里只需要改变颜色即可。而如果需要单独改变文字排版,可以使用 angular-material-typography。 类似的,细粒度的自定义某组件也是同理(例如:mat-button-theme / mat-button-color / mat-button-typography)。

额外处理基于覆盖层的组件

因为基于覆盖层的组件(例如:menu、select、dialog 等)不是被应用程序的根组件包裹,而是和根组件平级的 <div class="cdk-overlay-container"> 节点。因此,要让它们也能根据类名切换主题样式,需要做一点额外的处理:

import {OverlayContainer} from '@angular/cdk/overlay';

@Component({
  // ...
})
export class AppComponent {
  // 用以绑定类名
  isDarkMode = false;

  constructor(private overlayContainer: OverlayContainer) {}

  private processOverlayBaseComponentTheme(checked: boolean) {
    // 获取这个 div 元素
    const overlayContainerElement = this.overlayContainer.getContainerElement()
    const themeWrapperClassName = 'unicorn-dark-theme'
    if (checked) {
      overlayContainerElement.classList.add(themeWrapperClassName);
    } else {
      overlayContainerElement.classList.remove(themeWrapperClassName);
    }
  }
}

动态切换类名

通过 Angular 类名绑定,来切换不同的主题:

<div [class.unicorn-dark-theme]="isDarkMode">
  <!-- 受主题切换影响的元素和组件 -->
</div>

现在,就可以在两个主题之间自由切换了!😋

看看效果:
明亮主题
暗黑主题

多整几个

虽然上面我们实现了动态主题切换,不过只有暗黑/明亮的两个主题反复横跳显得不太够看。所以,现在到了整活儿时间,一次性整它十个颜色!

创建多个主题

相比创建两个主题时手动去创建,创建一个多个主题的 styles.scss,咱们就利用 Sass 的循环来实现,主要分为几步:

  1. 自定义主题的开头操作都一样,先引入 mat-core,并先定义一个默认主题:

    @import '~@angular/material/theming';        
    @include mat-core();
    
    // 默认主题颜色配置
    $candy-app-primary: mat-palette($mat-indigo);
    $candy-app-accent:  mat-palette($mat-pink, A200, A100, A400);
    $candy-app-theme:   mat-light-theme((
      color: (
        primary: $candy-app-primary,
        accent: $candy-app-accent,
      )
    ));
    
    // 生成默认主题样式 (颜色,文字排版,间距)
    @include angular-material-theme($candy-app-theme);
    @include app-component-color($candy-app-theme);
    @include theme-test-color($candy-app-theme);
  2. 创建一个 map 对象,在这个 map 中定义各个主题配置。每个主题配置的 key 就是这个主题的 class 类名:

    $app-themes: (
      indigo-pink : (primary-base: $mat-indigo, accent-base: $mat-pink),
      deeppurple-amber: (primary-base: $mat-deep-purple, accent-base: $mat-amber),
      blue-yellow : (primary-base: $mat-blue, accent-base: $mat-yellow),
      pink-bluegrey : (primary-base: $mat-pink, accent-base: $mat-blue-gray, ),
      purple-green : (primary-base: $mat-purple, accent-base: $mat-green),
    );
  3. 通过 @each 循环,创建明暗个五个主题颜色:

    @each $css-class, $theme in $app-themes {
      $primary: mat-palette(map-get($theme, primary-base))
      $accent: mat-palette(map-get($theme, accent-base))
      
      // key 值作为类名
      .#{$css-class} {
        $light-theme: mat-dark-theme((
          color: (
            primary: $primary,
            accent: $accent,
          )
        ));
        // 生成颜色样式
        @include angular-material-color($light-theme);
        // 添加两个业务组件测试主题适配
        @include app-component-color($light-theme);
        @include theme-test-color($light-theme);
      }
      
      // key 值作为类名
      .#{$css-class}-dark {
        $dark-theme: mat-dark-theme((
          color: (
            primary: $primary,
            accent: $accent,
          )
        ));
        // 生成颜色样式
        @include angular-material-color($dark-theme);
        // 添加两个业务组件测试主题适配
        @include app-component-color($dark-theme);
        @include theme-test-color($dark-theme);
      }
    }

动态切换主题

通过改变 root 元素的 class,来动态切换域名:

<div [class]="currentTheme">
  <!-- 受主题切换影响的元素和组件 -->
</div>
themelist = [
  "amber-lime",
  "amber-lime-dark",
  "deeppurple-amber",
  "deeppurple-amber-dark",
  "blue-yellow",
  "blue-yellow-dark",
  "pink-bluegrey",
  "pink-bluegrey-dark",
  "purple-green",
  "purple-green-dark"
]

switchTheme(theme: string) {
  if (this.currentTheme)
    this.overlayContainerEle.classList.remove(this.currentTheme);
  this.currentTheme = theme
  this.overlayContainerEle.classList.add(this.currentTheme);
}

ok,现在我们有了 10 套主题!
十个主题预览

不过可以看到,现在编译打包后的 styles.css 和 styles.js,未压缩的情况下,快高达 1MB 了(每个主题会增加 css 文件 50kb 大小)。所以,咱们试着懒加载它们。

懒加载动态切换

现在咱们已经可以动态的切换主题了,可只有两三个主题还好,如果我的应用程序,要提供很多的主题搭配,让用户自由选用呢,styles.scss 一次包含这么多份主题样式,实在有损加载性能(每个主题增加 50kb)。
遇到这种情况,我们可以预先编译好需要用到主题,在运行时动态切换它们,就可以按需加载了。

  1. 首先在 src > theme 文件夹中创建几个主题文件,例如这个 green-amber-dark.scss

    @import "~@angular/material/theming";
    
    @import "../app/app.component.theme.scss";
    @import "../app/component/theme-test/theme-test.component.theme";
    
    $green-primary: mat-palette($mat-green);
    $amber-accent: mat-palette($mat-amber);
    
    $green-amber-dark-theme: mat-dark-theme((
      color: (
        primary: $green-primary,
        accent: $amber-accent
      )
    ));
    
    @include angular-material-theme($green-amber-dark-theme);
    
    // 添加两个业务组件测试主题适配
    @include app-component-color($green-amber-dark-theme);
    @include theme-test-color($green-amber-dark-theme);
  2. 然后,在 angular.json > projects > architect > build > styles 中,新增刚刚创建的主题文件,inject 设置为 false,意味着它只会被打包为单独的 css 文件,但不会被引入项目中:

    "styles": [
      "src/styles.scss",
      {
        "input": "src/theme/green-amber-theme.scss",
        "inject": false
      }
    ],
  3. 最后,通过给 <link> 动态赋值即可:

    switchTheme(theme: string): void {
     const id = 'lazy-load-theme';
     const link = document.getElementById(id);
     // 第一次切换,新建一个 link 元素
     if (!link) {
       const linkEl = document.createElement('link');
       linkEl.setAttribute('rel', 'stylesheet');
       linkEl.setAttribute('type', 'text/css');
       linkEl.setAttribute('id', id);
       linkEl.setAttribute('href', `${theme}-theme.css`);
       document.head.appendChild(linkEl);
     } else {
       // 替换 link 元素的 href 地址 
       (link as HTMLLinkElement).href = `${theme}-theme.css`;
     }
    }

这样就实现了主题文件的懒加载,不过需要注意的是,默认的主题文件依旧需要在 styles.scss 中引用,它也懒加载的话,会导致初次加载时页面闪烁。

小结

本篇介绍了如何动态切换主题,渐进的包括了三种情况:

  1. 网站只需要在明亮和暗黑模式下切换主题,那么只需要通过一个独特的类名包裹主题即可,切换时只是切换类名即可
  2. 网站若是需要预设多个主题,可以利用 Sass 的 @each 方法,来批量生成,再根据类名动态切换。
  3. 如果网站的主题量很大或者可预见的会不断增加,又或者想要尽量多的优化加载性能,可以采用动态加载主题文件的方式,动态的控制一个 link 标签即可。

本篇的三种情况,可以参照 Github 示例,按 branch 切换

效果可以参考 效果展示,点击右上角工具栏可以切换各个主题。

有时候项目里可不止用上了 Material 的组件库,适配第三方组件库的主题,不像自己的业务组件,可以随意更改,就要另辟蹊径了,关于 Material 主题系统的工程实践,咱们下一篇见!🤠

阅读 2k

玻璃晴朗,橘子辉煌

166 声望
1 粉丝
0 条评论

玻璃晴朗,橘子辉煌

166 声望
1 粉丝
文章目录
宣传栏