如何利用 SCSS 實現一鍵換膚

語言: CN / TW / HK

前言

在專案開發過程中,我們有時候遇到需要 更換站點主題色 的需求。乃至於 APP 底部的 banner 中的 icon、文案和背景圖都是運營 線上可配置的 。還有的功能比如 更換系統字型大小 等。

這些本質上都是 CSS 的 動態渲染 的需求。如果在開發過程中 寫死 CSS 樣式 的話在面對這樣的需求的時候就會真·痛苦面具了。所以我們需要提前定義 一整套 CSS 的環境變數體系 ,在開發過程中就使用這套體系,未雨綢繆才能立於不敗之地。

這裡我們用到 SCSS (Sassy CSS)來實現這套體系。 SASS 是 CSS 的 前處理器 ,由 Ruby 編寫。一開始並不支援 {} 和這種原生 CSS 的寫法,縮排也嚴格控制,增加了開發者的使用成本。具體的區別可以看下面這張 gif 圖。

但是由 SASS3 開始引入的 SCSS 語法完全相容現有的 CSS 語法,能夠在生成真正的 CSS 檔案之前預處理一些邏輯,比如變數,迴圈,巢狀,混合,繼承,匯入等,使其在邏輯上能夠擁有部分 JS 的特性。

我們可以在 這個網址 線上檢視編譯的 SCSS 編譯成 CSS 之後的程式碼。

整體專案效果

切換主題色之後,能夠按照選擇的主題色進行不同的展示。 如下圖展示。

專案目錄結構

src
├── App.vue
├── main.js
├── router
│   └── index.js
├── store
│   └── index.js
├── style
│   ├── settings
│   │   └── variable.scss  // 樣式變數定義檔案
│   └── theme
│       ├── default
│       ├── index.scss // 主題入口檔案
│       └── old
└── views
    ├── Home.vue // 主題切換頁面
    ├── List.vue
    └── Mine.vue

廢話不多說。我們直接開幹吧。

環境準備

首先我們需要安裝 scss 解析環境

npm i sass
// 注意 sass-loader 安裝需要指定版本 如果安裝最新版本會報錯 this.getOptions 這個方法未定義
npm i -D [email protected]
// 利用 normalize.css 初始化頁面樣式
npm i -S normalize.css

定義變數

我們需要提前把一些常用的主題色,字型大小,以及邊距這種與視覺溝通好,然後定義對應的變數。這裡我參考資料貼了一套自定義的顏色變數。當然裡面的具體顏色可以根據需求動態調整。

小技巧

這裡講一個小技巧,定義的時候可以先定義一個 基準變數 base-param 然後其他狀態的值可以依賴這個 base-param 進行縮放或放大實現。比如不同大小規模的字型可以採用這種方法。

// 行高
$line-height-base: 1.5 !default;
$line-height-lg: 2 !default;
$line-height-sm: 1.25 !default;
// ./style/settings/variable.scss

// 字型顏色
$info: #17a2b8 !default;
$danger: #dc3545 !default;

// 字型大小 瀏覽器預設16px
$font-size-base: 1rem !default;
$font-size-lg: $font-size-base * 1.25 !default;
$font-size-slg: $font-size-base * 1.75 !default;

// 字重
$font-weight-normal: 400 !default;
$font-weight-bold: 600 !default;

定義主題

我們目前接到的需求是適老化改造,目前市場上大多數的專案字型都比較小,對老年人使用者不太友好。所以針對老年人使用者需要放大系統字型,方便他們檢視。你也可以根據自己的需求進行不同的主題定製。

定義一個入口檔案

// ./style/theme/index.scss

@import "../settings/variable.scss";

$themes-color: (
  default: (
    // 全域性樣式屬性
    color: $info,
    font-weight: $font-weight-normal,
    font-size: $font-size-lg,
  ),
  old: (
    color: $danger,
    font-weight: $font-weight-bold,
    font-size: $font-size-slg,
  ),
);
// ... 可自定義其他主題

vue.config.js 配置項處理

我們不想每次都引入 CSS 變數,可以裡在配置項中利用 CSS 外掛自動注入全域性變數樣式。

// vue.config.js

module.exports = {
  css: {
    loaderOptions: {
      scss: {
        // 注意: 在 sass-loader v8 中,這個選項是 prependData
        additionalData: `@import "@/style/theme/index.scss";`,
      },
    },
  },
};

主題色切換

主題色定義好之後就需要對他進行切換了。這也是 一鍵換膚 最核心的邏輯。

  • 在 App.vue 檔案下的 mounted 中將 body 新增一個自定義的 data-theme 屬性,並將其設定為 default
// App.vue mounted() { document .getElementsByTagName("body")[0]
.setAttribute("data-theme", "default"); },
  • 利用 webpack 自定義外掛遍歷主題目錄檔案,自動生成自定義主題目錄陣列
// vue.config.js
const fs = require("fs");
const webpack = require("webpack");

// 獲取主題檔名
const themeFiles = fs.readdirSync("./src/style/theme");
let ThemesArr = [];
themeFiles.forEach(function (item, index) {
  let stat = fs.lstatSync("./src/style/theme/" + item);
  if (stat.isDirectory() === true) {
    ThemesArr.push(item);
  }
});

module.exports = {
  css: {...},
  configureWebpack: (config) => {
    return {
      plugins: [
        // 自定義webpack外掛
        new webpack.DefinePlugin({
          THEMEARR: JSON.stringify(ThemesArr),
        }),
      ],
    };
  },
};
  • 切換 js 邏輯實現

初始化頁面的時候,獲取到預設主題

// Home.vue
mounted() {
  this.themeValue = THEMEARR;
  this.currentThemeIndex = this.themeValue.findIndex(
    (theme) => theme === "default"
  );
  this.currentTheme = this.themeValue[this.currentThemeIndex];
},

把選擇的主題賦值給自定義屬性 data-theme

// Home.vue

// 核心切換邏輯
methods: {
  onConfirm(currentTheme) {
    this.currentTheme = currentTheme;
    this.showPicker = false;
    this.currentThemeIndex = this.themeValue.findIndex(
      (theme) => theme === currentTheme
    );
    document
      .getElementsByTagName("body")[0]
      .setAttribute("data-theme", THEMEARR[this.currentThemeIndex]);
  },
}

CSS 版本如何實現主題色切換

可能大家不太瞭解,CSS 也是可以支援自定義屬性的,這就為我們定義屬性變數提供了基礎。他通過在自定義屬性之前加上字首 -- 來實現。

body {
  --foo: #7f583f;
  --bar: #f7efd2;
}

首先想到的就是給標籤新增自定義主題屬性 data-theme,再通過 css 屬性選擇器+名稱空間來找到指定的元素並替換不同的主題色。這裡採用的 t-檔名-含義類名來命名,防止樣式衝突。

// ./default.scss
// 也可以換成其他的自定義變數顏色
[data-theme="default"] .t-list-title,
[data-theme="default"] .t-list-sub-title,
[data-theme="default"] .t-list-info {
  color: var(--foo);
  font-weight: 400;
  font-size: 1rem * 1.25;
}

// ./old.scss
// 也可以換成其他的自定義變數顏色
[data-theme="old"] .t-list-title,
[data-theme="old"] .t-list-sub-title,
[data-theme="old"] .t-list-info {
  color: var();
  font-weight: 600;
  font-size: 1rem * 1.75;
}
// ./List.vue
<template>
  <div class="home">
    <div class="container" v-for="(item, index) in 3" :key="index">
      <div class="t-list-title">標題</div>
      <div class="t-list-sub-title">副標題</div>
      <div class="t-list-info">
        這是一段很長的詳情資訊這是一段很長的詳情資訊這是一段很長的詳情資訊這是一段很長的詳情資訊這是一段很長的詳情資訊這是一段很長的詳情資訊這是一段很長的詳情資訊
      </div>
    </div>
  </div>
</template>

Scss 版本如何實現主題色切換

Scss 前置知識

在使用 sass 之前,需要知道一些知識點。

  • 使用@each 迴圈

    1.迴圈一個 list: 類名為 icon-10px 、icon-12px、icon-14px 寫他們的字型大小寫法就可以如下:

2、迴圈一個 map:類名為 icon-primary、icon-success、icon-secondary 等,但是他們的值又都是變數,寫法如下:

  • map-get

map-get(map,key) 函式的作用是根據 key 引數,返回 key 在 map 中對應的 value 值。如果 key 不存在 map 中,將返回 null 值。此函式包括兩個引數:

map:定義好的 map。 key:需要遍歷的 key。

假設要獲取 facebook 鍵值對應的值 #3b5998,我們就可以使用 map-get() 函式來實現:

  • 使用&巢狀覆蓋原有樣式

當一個元素的樣式在另一個容器中有其他指定的樣式時,可以使用巢狀選擇器讓他們保持在同一個地方。 .no-opacity & 相當於 .no-opacity .foo

  • map-merge

合併兩個 map 形成一個新的 map 型別,即將 map2 新增到 map1 的尾部

$font-sizes: ("small": 12px, "normal": 18px, "large": 24px)
$font-sizes2: ("x-large": 30px, "xx-large": 36px)
map-merge($font-sizes, $font-sizes2)
結果: "small": 12px, "normal": 18px, "large": 24px,
"x-large": 30px, "xx-large": 36px
  • @content

@content 用在 mixin 裡面的,當定義一個 mixin 後,並且設定了 @content@include 的時候可以傳入相應的內容到 mixin 裡面

綜合使用

定義混合指令,切換主題,並將主題中的所有規則新增到 theme-map 中

// ./Home.vue

@mixin themify() {
  @each $theme-name, $map in $themes-color {
    // & 表示父級元素  !global 表示覆蓋原來的
    [data-theme="#{$theme-name}"] & {
      $theme-map: () !global;
      // 迴圈合併鍵值對
      @each $key, $value in $map {
        $theme-map: map-merge(
          $theme-map,
          (
            $key: $value,
          )
        ) !global;
      }
      // 表示包含 下面函式 themed()
      @content;
    }
  }
}

@function themed($key) {
  @return map-get($theme-map, $key);
}
.t-list-title,
.t-list-sub-title,
.t-list-info {
  @include themify() {
    color: themed("color");
    font-weight: themed("font-weight");
    font-size: themed("font-size");
  }
}

整體編譯後的樣式程式碼如下圖所示

專案原始碼地址

想要看 demo 原始碼的可以點選這個連線檢視程式碼。

點選檢視專案原始碼

總結

  • 瞭解 SCSS 的基礎語法,並綜合使用,實現了一鍵換膚功能。
  • 利用 SCSS 強大的函式功能遍歷類名統一新增以自定義屬性名字首的名稱空間,利用迴圈自動生成 CSS 樣式。
  • 瞭解一鍵換膚的核心原理。