1. 程式人生 > 實用技巧 >基於SCSS的angular專案的主題切換

基於SCSS的angular專案的主題切換

目前的主題切換大多要求動態主題切換,即使用者點選切換主題button後不需要重新載入頁面,頁面不需要重新整理即可切換主題。這就需要思考如下幾點:

  • 如何讓css監聽使用者切換主題了?
  • 類似:hover這一類的偽元素選擇器,css會動態監聽,當元素被hover時,css就會被新增上去

一、原理

類似:hover這一類的偽元素選擇器,css會動態監聽,當元素被hover時,css就會被新增上去。

css也會監聽元素上的attribute的值的變化,這樣我們可以利用這個特性來實現css監聽使用者行為。

實現原理是這樣的,我們在body元素上新增一個data-theme-style屬性,讓它的值根據使用者的行為變化,比如dark或者light。

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6     <title>Document</title>
 7     <style>
 8         [data-theme-style=dark] .content1{
 9             background
: black; 10 } 11 [data-theme-style=light] .content1{ 12 background: gray; 13 } 14 [data-theme-style=dark] .content2{ 15 background: red; 16 } 17 [data-theme-style=light] .content2{ 18 background: blue; 19 } 20 .content
{ 21 width: 200px; 22 height: 200px; 23 border: 1px solid red; 24 } 25 </style> 26 27 </head> 28 <body data-theme-style="dark"> 29 <button>Change Theme</button> 30 31 <div class="content content1"></div> 32 <div class="content content2"></div> 33 <script> 34 const button = document.getElementsByTagName('button')[0]; 35 const body = document.body; 36 button.addEventListener('click', () => { 37 body.setAttribute('data-theme-style', body.getAttribute('data-theme-style') === 'dark' ? 'light' : 'dark') 38 }) 39 </script> 40 </body> 41 </html>

可以看到,點選button時修改body的data-theme-style的值,這樣css就會監聽到變化來切換css,也就是主題切換的最基本的功能。

實際上,這是主題切換的最基本的原理:使用者觸發html元素屬性的變化,利用css監聽屬性的變化,再把需要變化的css新增到屬性選擇器下面,這樣就能夠讓css跟著屬性選擇器的變化來變化了。

二、優化

作為一個合格的單身的脫離了高階趣味的程式設計師來說,重複的程式碼是不可容忍的。

我們來解決一下這個問題,這裡需要用到一些css的高階語法,所以我們使用scss完成,我們先把程式碼遷移到使用了scss的angular專案中。

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>CustomElementsDemo</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body data-theme-style="dark">
  <app-root></app-root>
</body>
</html>

app.component.html

<button>Change Theme</button>
<div class="content content1"></div>
<div class="content content2"></div>

app.component.scss

[data-theme-style=dark] .content1{
    background: black;
}
[data-theme-style=light] .content1{
    background: gray;
}
[data-theme-style=dark] .content2{
    border:20pxsolidred;
}
[data-theme-style=light] .content2{
  border:20pxsolidblue;
}
.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

app.component.ts

import { Component, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements AfterViewInit{
  ngAfterViewInit(): void {
    const button = document.getElementsByTagName('button')[0];
    const body = document.body;
    button.addEventListener('click', () => {
      body.setAttribute('data-theme-style', body.getAttribute('data-theme-style') === 'dark' ? 'light' : 'dark')
    });
  }
}

如果運行了程式碼之後,你會發現content1 和 content2並沒有作用在元素上。

這是因為angular對於元件內的css是封閉的,[data-theme-style=dark]並不能找到body元素上,那怎麼找到呢?

css中有一個選擇器:host-context,它在當前元件宿主元素的祖先節點中查詢 CSS 類, 直到文件的根節點為止。

我們來修改scss

app.component.scss

:host-context([data-theme-style=dark]) .content1{
    background: black;
}
:host-context([data-theme-style=light]) .content1{
    background: gray;
}
:host-context([data-theme-style=dark]) .content2{
    border: 20px solid red;
}
:host-context([data-theme-style=light]) .content2{
    border: 20px solid blue;
}
.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

這樣,就又可以使用了。

:host-context([data-theme-style=dark]) 就可以找到body元素了。

我們來使用scss的mixin來去掉重複的程式碼:

@mixin bg-context1() {
    :host-context([data-theme-style=dark]) .content1 {
        background: black;
    }
    :host-context([data-theme-style=light]) .content1{
        background: gray;
    }
}
@mixin border-context2() {
    :host-context([data-theme-style=dark]) .content2 {
        border: 20px solid red; 
    }
    :host-context([data-theme-style=light]) .content2 {
        border: 20px solid blue; 
    }
}
.content1 {
    // 使用@include來使用@mixin
    @include bg-context1();
}

.content2 {
    @include border-context2();
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

@mixin是可以傳遞引數的,並且,&符號可以引用父級元素。

// @mixin指令是可以傳遞引數的
@mixin bg-context1($propname) {
    // & 符號可以引用父級元素
    :host-context([data-theme-style=dark]) & {
        // 當變數使用在屬性名稱時,需要使用插值表示式來使用變數
        #{$propname}: black;
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: gray;
    }
}
@mixin border-context2($propname) {
    :host-context([data-theme-style=dark]) & {
        #{$propname}: 20px solid red; 
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: 20px solid blue; 
    }
}
.content1 {
    // 使用@include來使用@mixin
    @include bg-context1(background);
}

.content2 {
    @include border-context2(border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

目前為止,並沒有去掉多少重複的程式碼,下面我們來定義2個map,一個是dark主題下的css值,一個是light主題下的css值。

$dark: (
    context1-bg-color: black,
    context2-border: 20px solid red,
);
$light: (
    context1-bg-color: gray,
    context2-border: 20px solid blue,
);
// @mixin指令是可以傳遞引數的
@mixin bg-context1($propname) {
    // & 符號可以引用父級元素
    :host-context([data-theme-style=dark]) & {
        // 當變數使用在屬性名稱時,需要使用插值表示式來使用變數
        // map-get是scss提供的獲取map中的value的方法
        #{$propname}: map-get($dark, context1-bg-color);
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: map-get($light, context1-bg-color);
    }
}
@mixin border-context2($propname) {
    :host-context([data-theme-style=dark]) & {
        #{$propname}: map-get($dark, context2-border);
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: map-get($light, context2-border);
    }
}
.content1 {
    // 使用@include來使用@mixin
    @include bg-context1(background);
}

.content2 {
    @include border-context2(border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

我們的目標是去掉重複的程式碼,需要想辦法把2個mixin合併在一起。可以把變數的名稱通過mixin的引數傳遞進去。

$dark: (
    context1-bg-color: black,
    context2-border: 20px solid red,
);
$light: (
    context1-bg-color: gray,
    context2-border: 20px solid blue,
);
// @mixin指令是可以傳遞引數的
@mixin theme($propname, $varname) {
    // & 符號可以引用父級元素
    :host-context([data-theme-style=dark]) & {
        // 當變數使用在屬性名稱時,需要使用插值表示式來使用變數
        // map-get是scss提供的獲取map中的value的方法
        #{$propname}: map-get($dark, $varname);
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: map-get($light, $varname);
    }
}
.content1 {
    // 使用@include來使用@mixin
    @include theme(background, context1-bg-color);
}

.content2 {
    @include theme(border, context2-border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

來來來,看看,選擇變成一個mixin了,我們再來簡化一下,mixin中的css會根據主題的數量增加而增加,比如這個時候再加個default theme,mixin中就會有3個了。

觀察一下,重複的地方很多,只有主題名稱不同,我們再定義一個主題的map,然後在mixin中使用@each來迴圈這個集合:

$dark: (
    context1-bg-color: black,
    context2-border: 20px solid red,
);
$light: (
    context1-bg-color: gray,
    context2-border: 20px solid blue,
);
$themes: (
    dark: $dark,
    light: $light,
);
// @mixin指令是可以傳遞引數的
@mixin theme($propname, $varname) {
    // @each迴圈map時,第一個引數是map的key,第二個引數是map的value
    @each $themename, $theme in $themes {
        // & 符號可以引用父級元素
        // 不能直接使用變數,需要使用插值表示式包含變數
        :host-context([data-theme-style=#{$themename}]) & {
            // 當變數使用在屬性名稱時,需要使用插值表示式來使用變數
            // map-get是scss提供的獲取map中的value的方法
            #{$propname}: map-get($theme, $varname);
        }
    }
}
.content1 {
    // 使用@include來使用@mixin
    @include theme(background, context1-bg-color);
}

.content2 {
    @include theme(border, context2-border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

這樣就把能簡化的都簡化了。

但是對於專案來說,需要把目錄整理一下,我們給主題單獨建立一個資料夾,把之前定義的主題$dark,$light獨立成檔案。把mixin指令獨立成單獨的檔案。

app/theme/dark.scss

$dark: (
    context1-bg-color: black,
    context2-border: 20px solid red,
);

app/theme/light.scss

$light: (
    context1-bg-color: gray,
    context2-border: 20px solid blue,
);

app/theme/mixin.scss

@import 'src/app/theme/dark.scss';
@import 'src/app/theme/light.scss';

$themes: (
    dark: $dark,
    light: $light,
);
// @mixin指令是可以傳遞引數的
@mixin theme($propname, $varname) {
    // @each迴圈map時,第一個引數是map的key,第二個引數是map的value
    @each $themename, $theme in $themes {
        // & 符號可以引用父級元素
        // 不能直接使用變數,需要使用插值表示式包含變數
        :host-context([data-theme-style=#{$themename}]) & {
            // 當變數使用在屬性名稱時,需要使用插值表示式來使用變數
            // map-get是scss提供的獲取map中的value的方法
            #{$propname}: map-get($theme, $varname);
        }
    }
}

在app.component.scss中使用

app.component.scss

@import 'src/app/theme/mixin.scss';

.content1 {
    // 使用@include來使用@mixin
    @include theme(background, context1-bg-color);
}

.content2 {
    @include theme(border, context2-border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

這樣,我們基本完成了主題的切換了。

使用主題時需要注意的是,mixin theme的第一個引數是css的屬性,比如border,比如background,第二個引數是定義在主題檔案中的變數名稱。

三、總結

使用scss來實現主題切換的好處是可以動態切換主題,不需要重新載入頁面,而且程式碼實現比較簡潔。