基於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來實現主題切換的好處是可以動態切換主題,不需要重新載入頁面,而且程式碼實現比較簡潔。