前端換膚的一些思考

先看看大家怎麼做的。下面是兩篇別人寫的文章,最後是我自己的方法。

第一篇:聊一聊前端換膚

之前在做網站換膚,所以想談談網站換膚的實現。網頁換膚就是修改顏色值,因此重點就在於怎麼來替換。

一般實現

 

image

 

如上圖,我們會看到在某些網站的右上角會出現這麼幾個顏色塊,點擊不同的顏色塊,網站的整體顏色就被替換了。要實現它,我們考慮最簡單的方式:點擊不同的按鈕切換不同的樣式表 ,如:

 

  • theme-green.css
  • theme-red.css
  • theme-yellow.css

可以看出,我們需要爲每個顏色塊編寫樣式表,那如果我要實現幾百種或者讓用戶自定義呢,顯而易見這種方式十本笨拙,且拓展性並不高,另外,如果考慮加載的成本,那其實這種方式並不可取。

ElementUI 的實現

 

image

 

 

ElementUI 的實現比上面的實現高了好幾個level,它能讓用戶自定義顏色值,而且展示效果也更加優雅。當前我的實現就是基於它的思路來實現。 我們來看看他是怎麼實現的(這裏引用的是官方的實現解釋):

下面我具體講下我參考它的原理的實現過程 (我們的css 編寫是基於 postcss 來編寫的):

  1. 先確定一個主題色,其他需在在換膚過程中隨主題色一起修改的顏色值就根據主題色來調用例如(上面已經說到了我們是基於postcss來編寫的,所以就使用瞭如下函數來計算顏色值): tint(var(--color-primary), 20%)darken(var(--color-primary), 15%)shade(var(--color-primary), 5%) 等。這也類似就實現了上面的第一步
  2. 然後根據用戶選擇的顏色值來生成新的一輪對應的一系列顏色值: 這裏我先把全部css文件中可以通過主題色來計算出其他顏色的顏色值彙總在一起,如下:
// formula.js
const formula = [
    {
        name: 'hoverPrimary',
        exp: 'color(primary l(66%))',
    },
    {
        name: 'clickPrimary',
        exp: 'color(primary l(15%))',
    },
    {
        name: 'treeBg',
        exp: 'color(primary l(95%))',
    },
    {
        name: 'treeHoverBg',
        exp: 'color(primary h(+1) l(94%))',
    },
    {
        name: 'treeNodeContent',
        exp: 'color(primary tint(90%))',
    },
    {
        name: 'navBar',
        exp: 'color(primary h(-1) s(87%) l(82%))',
    }  
];

export default formula;

這裏的color函數 是後面我們調用了 css-color-function 這個包,其api使然。

既然對應關係彙總好了,那我們就來進行顏色值的替換。在一開始進入網頁的時候,我就先根據默認的主題色根據 formula.js 中的 計算顏色彙總表 生成對應的顏色,以便後面的替換,在這過程中使用了css-color-function 這個包,

import Color from 'css-color-function';

componentDidMount(){
this.initColorCluster = ['#ff571a', ...this.generateColors('#ff571a')];
        // 拿到所有初始值之後,因爲我們要做的是字符串替換,所以這裏利用了正則,結果值如圖2:
        this.initStyleReg = this.initColorCluster  
            .join('|')
            .replace(/\(/g, '\\(') // 括號的轉義
            .replace(/\)/g, '\\)')
            .replace(/0\./g, '.');  // 這裏替換是因爲默認的css中計算出來的值透明度會缺省0,所以索性就直接全部去掉0
}

generateColors = primary => {
        return formula.map(f => {
            const value = f.exp.replace(/primary/g, primary);  // 將字符串中的primary 關鍵字替換爲實際值,以便下一步調用 `Color.convert`
            return Color.convert(value);     // 生成一連串的顏色值,見下圖1,可以看見計算值全部變爲了`rgb/rgba` 值
        });
    };

圖1:

image

 

 

圖2,黑色字即爲顏色正則表達式:

image

 

 

好了,當我們拿到了原始值之後,就可以開始進行替換了,這裏的替換源是什麼?由於我們的網頁是通過如下 內嵌style標籤 的,所以替換原就是所有的style標籤,而 element 是直接去請求網頁 打包好的的css文件

 

image

 

 

注:並不是每次都需要查找所有的 style 標籤,只需要一次,然後,後面的替換只要在前一次的替換而生成的 style 標籤(使用so-ui-react-theme來做標記)中做替換

下面是核心代碼:

changeTheme = color => {
        // 這裏防止兩次替換顏色值相同,省的造成不必要的替換,同時驗證顏色值的合法性
        if (color !== this.state.themeColor && (ABBRRE.test(color) || HEXRE.test(color))) {
            const styles =
                document.querySelectorAll('.so-ui-react-theme').length > 0
                    ? Array.from(document.querySelectorAll('.so-ui-react-theme')) // 這裏就是上說到的
                    : Array.from(document.querySelectorAll('style')).filter(style => {  // 找到需要進行替換的style標籤
                          const text = style.innerText;
                          const re = new RegExp(`${this.initStyleReg}`, 'i');
                          return re.test(text);
                      });

            const oldColorCluster = this.initColorCluster.slice();
            const re = new RegExp(`${this.initStyleReg}`, 'ig');  // 老的顏色簇正則,全局替換,且不區分大小寫

            this.clusterDeal(color);  // 此時 initColorCluster 已是新的顏色簇

            styles.forEach(style => {
                const { innerText } = style;
                style.innerHTML = innerText.replace(re, match => {
                    let index = oldColorCluster.indexOf(match.toLowerCase().replace('.', '0.'));

                    if (index === -1) index = oldColorCluster.indexOf(match.toUpperCase().replace('.', '0.'));
                    // 進行替換
                    return this.initColorCluster[index].toLowerCase().replace(/0\./g, '.');
                });

                style.setAttribute('class', 'so-ui-react-theme');
            });
          

            this.setState({
                themeColor: color,
            });
        }
    };

效果如下:

image

 

 

至此,我們的顏色值替換已經完成了。正如官方所說,實現原理十分暴力😂,同時感覺使用源css通過 postcss 編譯出來的顏色值不好通過 css-color-function 這個包來計算的一模一樣,好幾次我都是對着 rgba 的值一直在調🤣🤣,( 👀難受

antd 的實現

antd 的樣式是基於 less 來編寫的,所以在做換膚的時候也利用了 less 可以直接 編譯css 變量 的特性,直接上手試下。頁面中頂部有三個色塊,用於充當顏色選擇器,下面是用於測試的div塊。

image

 

 

下面div的css 如下,這裏的 @primary-color@bg-color 就是 less 變量:

.test-block {
    width: 300px;
    height: 300px;
    text-align: center;
    line-height: 300px;
    margin: 20px auto;
    color: @primary-color;
    background: @bg-color;
}

當我們點擊三個色塊的時候,直接去加載 less.js,具體代碼如下(參考antd的實現):

import React from 'react';
import { loadScript } from '../../shared/utils';
import './index.less';
const colorCluters = ['red', 'blue', 'green'];

export default class ColorPicker extends React.Component {
    handleColorChange = color => {
        const changeColor = () => {
            window.less
                .modifyVars({  // 調用 `less.modifyVars` 方法來改變變量值
                    '@primary-color': color,
                    '@bg-color': '#2f54eb',
                })
                .then(() => {
                    console.log('修改成功');
                });
        };
        const lessUrl =
            'https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js';

        if (this.lessLoaded) {
            changeColor();
        } else {
            window.less = {
                async: true,
            };

            loadScript(lessUrl).then(() => {
                this.lessLoaded = true;
                changeColor();
            });
        }
    };

    render() {
        return (
            <ul className="color-picker">
                {colorCluters.map(color => (
                    <li
                        style={{ color }}
                        onClick={() => {
                            this.handleColorChange(color);
                        }}>
                        color
                    </li>
                ))}
            </ul>
        );
    }
}

然後點擊色塊進行試驗,發現並沒有生效,這是why?然後就去看了其文檔,原來它會找到所有如下的less 樣式標籤,並且使用已編譯的css同步創建 style 標籤。也就是說我們必須吧代碼中所有的less 都以下面這種link的方式來引入,這樣less.js 才能在瀏覽器端實現編譯。

<link rel="stylesheet/less" type="text/css" href="styles.less" />

這裏我使用了 create-react-app ,所以直接把 less 文件放在了public目錄下,然後在html中直接引入:

image

 

 

 

image

 

 

點擊blue色塊,可以看見 colorbackground 的值確實變了:

 

image

 


並且產生了一個 id=less:color 的style 標籤,裏面就是編譯好的 css 樣式。緊接着我又試了link兩個less 文件,然後點擊色塊:

 

 

image

 

 

從上圖看出,less.js 會爲每個less 文件編譯出一個style 標籤。 接着去看了 antd 的實現,它會調用 antd-theme-generator 來把所有antd 組件 或者 文檔 的less 文件組合爲一個文件,並插入html中,有興趣的可以去看下 antd-theme-generator 的內部實現,可以讓你更加深入的瞭解 less 的編程式用法。

注:使用less 來實現換膚要注意 less 文件html 中編寫的位置,不然很可能被其他css 文件所幹擾導致換膚失敗

基於 CSS自定義變量 的實現

先來說下 css自定義變量 ,它讓我擁有像less/sass那種定義變量並使用變量的能力,聲明變量的時候,變量名前面要加兩根連詞線(--),在使用的時候只需要使用var()來訪問即可,看下效果:

 

image

 

 

如果要局部使用,只需要將變量定義在 元素選擇器內部即可。具體使用見使用CSS變量關於 CSS 變量,你需要了解的一切

使用 css 自定義變量 的好處就是我們可以使用 js 來改變這個變量:

  • 使用 document.body.style.setProperty('--bg', '#7F583F'); 來設置變量
  • 使用 document.body.style.getPropertyValue('--bg'); 來獲取變量
  • 使用 document.body.style.removeProperty('--bg'); 來刪除變量

有了如上的準備,我們基於 css 變量 來實現的換膚就有思路了:將css 中與換膚有關的顏色值提取出來放在 :root{} 中,然後在頁面上使用 setProperty 來動態改變這些變量值即可。

上面說到,我們使用的是postcss,postcss 會將css自定義變量直接編譯爲確定值,而不是保留。這時就需要 postcss 插件 來爲我們保留這些自定義變量,使用 postcss-custom-properties,並且設置 preserve=true 後,postcss就會爲我們保留了,效果如下:

 

image

 

 

 

image

 

 

這時候就可以在換膚顏色選擇之後調用 document.body.style.setProperty 來實現換膚了。

不過這裏只是替換一個變量,如果需要根據主顏色來計算出其他顏色從而賦值給其他變量就可能需要調用css-color-function 這樣的顏色計算包來進行計算了。

import colorFun from "css-color-function"

document.body.style.setProperty('--color-hover-bg', colorFun.convert(`color(${value} tint(90%))`));

其postcss的插件配置如下(如需其他功能可自行添加插件):

module.exports = {
    plugins: [
        require('postcss-partial-import'),
        require('postcss-url'),
        require('saladcss-bem')({
            defaultNamespace: 'xxx',
            separators: {
                descendent: '__',
            },
            shortcuts: {
                modifier: 'm',
                descendent: 'd',
                component: 'c',
            },
        }),

        require('postcss-custom-selectors'),
        require('postcss-mixins'),
        require('postcss-advanced-variables'),
        require('postcss-property-lookup'),
        require('postcss-nested'),
        require('postcss-nesting'),
        require('postcss-css-reset'),
        require('postcss-shape'),
        require('postcss-utils'),

        require('postcss-custom-properties')({
            preserve: true,
        }),

        require('postcss-calc')({
            preserve: false,
        }),
    ],
};

聊下 precsspostcss-preset-env

它們相當於 babelpreset

precss 其包含的插件如下:

使用如下配置也能達到相同的效果,precss 的選項是透傳給上面各個插件的,由於 postcss-custom-properties 插件位於 postcss-preset-env 中,所以只要按 postcss-preset-env 的配置來即可:

plugins:[
require('precss')({
            features: {   
                'custom-properties': {
                    preserve: true,
                },
            },
        }),
]
複製代碼

postcss-preset-env 包含了更多的插件。這了主要了解下其 stage 選項,因爲當我設置了stage=2 時(precss 中默認 postcss-preset-envstage= 0 ),我的 字體圖標 竟然沒了:

image

 

 

這就很神奇,由於沒有往 代碼的編寫 上想,就直接去看了源碼

它會調用 cssdb,它是 CSS特性 的綜合列表,可以到各個css特性 在成爲標準過程中現階段所處的位置,這個就使用 stage 來標記,它也能告知我們該使用哪種 postcss 插件 或者 js包 來提前使用css 新特性。cssdb 包的內容的各個插件詳細信息舉例如下

{ id: 'all-property',
    title: '`all` Property',
    description:
     'A property for defining the reset of all properties of an element',
    specification: 'https://www.w3.org/TR/css-cascade-3/#all-shorthand',
    stage: 3,
    caniuse: 'css-all',
    docs:
     { mdn: 'https://developer.mozilla.org/en-US/docs/Web/CSS/all' },
    example: 'a {\n  all: initial;\n}',
    polyfills: [ [Object] ] }
複製代碼

當我們設置了stage的時候,就會去判斷 各個插件的stage 是否大於等於設置的stage,從而篩選出符合stage的插件集來處理css。最後我就從stage小於2的各個插件一個一個去試,終於在 postcss-custom-selectors 時候試成功了。然後就去看了該插件的功能,難道我字體圖標的定義也是這樣?果然如此:

 

image

 

 

總結

上面介紹了四種換膚的方法,個人更加偏向於 antd 或者基於 css 自定義變量 的寫法,不過 antd 基於 less 在瀏覽器中的編譯,less 官方文檔中也說到了:

This is because less is a large javascript file and compiling less before the user can see the page means a delay for the user. In addition, consider that mobile devices will compile slower.

所以編譯速度是一個要考慮的問題。然後是 css 自定義變量 要考慮的可能就是瀏覽器中的兼容性問題了,不過感覺 css 自定義變量 的支持度還是挺友好了的🤣🤣。

ps:如果你還有其他換膚的方式,或者上面有說到不妥的地方,歡迎補充與交流🤝🤝


作者:大搜車無線開發中心
鏈接:https://juejin.im/post/5ca41617f265da3092006155
來源:掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

 

第二篇:一文總結前端換膚換主題

最近項目涉及換主題/換膚的工作, 查了查資料,總結出五種換膚方案:

序號 方法 特點
1 利用class 命名空間 最簡單的換膚方案
2 準備多套CSS主題 傳統前端最常用
3 利用CSS預處理生成多套主題樣式 現代前端最常用
4 動態換膚 支持瀏覽器熱換膚,最酷炫
5 CSS變量換膚 不考慮IE,最佳換膚方式

這是五種均爲通用方案,可以適用於各種前端框架,腳手架中

1. 利用class 命名空間

這是最簡單的換膚方式, 看下面示例即可輕鬆理解。

1.利用class 名稱準備兩個主題:


<style>

  p.red-theme {

    color: red

  }

  p.blue-theme {

    color: blue

  }
</style>

2.如果用紅色主題, 給body增加 red-theme標籤


<body class="red-theme">

    <p> 這裏是紅色主題 </p>

     ...

</body>

3.如果用藍色主題, 用 blue-theme 代替 red-theme


<body class="blue-theme">

    <p> 這裏是藍色主題 </p>

     ...

</body>

優缺點

  • 優點: 簡單,好理解,好實現
  • 缺點: CSS中需多寫主題的class,代碼容易混亂;需手動編寫

參考

基於element動態換膚

2.準備多套CSS主題

本地存放多套主題, 根據需要切換加載不同的樣式

  1. 準備份默認樣式主題

/*theme-default.css*/
p {
  color: #333
}
...
  1. 準備各主題的樣式
/* theme-red.css */
p {
  color: #red
}
...
/* theme-blue.css */
p {
  color: #blue
}
...
  1. 頁面加載後,根據用戶需求加載不同的樣式列表
   var link = document.createElement('link');
   link.type = 'text/css';
   link.id = "theme-blue";  
   link.rel = 'stylesheet';
   link.href = '/css/theme-blue.css';
   document.getElementsByTagName("head")[0].appendChild(link);
  1. 有時候需要保存用戶使用的主題,可以通過如下方式:
    • 利用路由標記
    • 利用cookie標記
    • 利用localstorage
    • 保存到後端服務器

優缺點

  • 優點: 簡單,好理解,好實現
  • 缺點: 需要手寫兩份以上CSS配色樣式; 切換樣式需要下載CSS的時間

參考

web網頁中主題切換的實現思路 中有更多細節

3. 利用CSS預處理生成多套主題樣式

這是“準備多套CSS主題”的優化方案,利用CSS預處理生成多套主題樣式,再根據需要切換

  1. 利用Less,stylus 或 sass 的變量代替顏色值

  2. 配置多個主題顏色配置
  3. 利用webpack等工具輸出多套主題樣式
  4. 頁面加載後,根據用戶需求加載不同的樣式列表(同方案2)

優缺點

  • 優點: 不用手寫兩套CSS
  • 確定: 配置複雜;生成冗餘的CSS文件; 切換樣式需要下載CSS的時間

參考

webpack的配置比較複雜,可以看這篇文章:webpack 換膚功能多主題/配色樣式打包解決方案
ant 環境下可以利用antd-theme-generator 快速配置,詳見:antd-theme-generatorantd在線換膚定製功能

4.動態換膚

這是element ui中的換膚方案,支持瀏覽器熱換膚。生成一套主題, 將主題配色配置寫在js中,在瀏覽器中用js動態修改style標籤覆蓋原有的CSS。

  1. 準備一套默認theme.css樣式
/* theme.css */
.title {
  color: #FF0000
}
  1. 準備主題色配置
var colors = {
     red: {
       themeColor: '#FF0000'
     },
     blue: {
       themeColor: '#0000FF'
     }
   }
  1. 異步獲取 theme.css ,將顏色值替換爲關鍵詞
    關鍵字可以確保以後能多次換色
var styles = ''
axios.get('theme.css').then((resp=> {
 const colorMap = {
   '#FF0000': 'themeColor'
 }
 styles = resp.data
 Object.keys(colorMap).forEach(key => {
   const value = colorMap[key]
   styles = styles.replace(new RegExp(key, 'ig'), value)
 })
}))

style 變爲:

.title {
  color: theme-color
}
  1. 把關鍵詞再換回剛剛生成的相應的顏色值,並在頁面上添加 style 標籤
 // console 中執行 writeNewStyle (styles, colors.blue)  即可變色
 function writeNewStyle (originalStyle, colors) {
      let oldEl = document.getElementById('temp-style')
      let cssText = originalStyle
       // 替換顏色值
      Object.keys(colors).forEach(key => {
        cssText = cssText.replace(new RegExp(key, 'ig'), colors[key])
      })
      const style = document.createElement('style')
      style.innerText = cssText
      style.id = 'temp-style'
 
      oldEl ? document.head.replaceChild(style, oldEl) : 
      document.head.appendChild(style)  // 將style寫入頁面
    }

此時style 變爲:

.title {
  color: '#0000FF'
}

優缺點

  • 優點: 只需一套CSS文件; 換膚不需要延遲等候;可自動適配多種主題色;
  • 缺點: 稍難理解; 需準確的css顏色值;可能受限於瀏覽器性能;

參考

本文最後有該方案的完整代碼
Vue 換膚實踐
elementUI 及 vuetifyjs動態換色實踐
vue-element-admin 動態換膚
webpack 插件抽取CSS中的主題色

5. CSS 變量換膚

利用CSS 變量設置顏色, 用js動態修改CSS變量,進而換色。
如果不考慮IE兼容,這是最佳換膚方案
看下面的例子,很好理解

<html>
  <head>
    <title>CSS varies</title>
    <style>
      :root {
        --theme-color: red /* css 變量賦值位置 */
      }
      .title {
        color: var(--theme-color) /* 用css變量標記顏色 */
      }
    </style>
  </head>
  <body>
    <h3 class="title">CSS 變量換膚</h3>
    <script>
      // console 中執行 changceColor('blue') 即可變色
      function changeColor(color = 'blue') {
        document.documentElement.style.setProperty("--theme-color",color);
      }
    </script>
  </body>
</html>

優缺點

  • 優點:只需一套CSS文件; 換膚不需要延遲等候;對瀏覽器性能要求低;可自動適配多種主題色;
  • 缺點: 不支持IE, 2016年前的chrome,safari; 兼容性參見Can I Use CSS Variables

參考

附A: 方案四 態換換膚完整代碼

dynamic.html

<html lang="en">
<head>
  <title>js 動態換膚</title>
   <!-- 利用axios 實現異步加載樣式-->
  <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
</head>
<body>
 <h3 class="title">js 動態換膚</h3>
 <script>
   // 1. 主題顏色配置
   var colors = {
     red: {
       themeColor: '#FF0000' 
     },
     blue: {
       themeColor: '#0000FF'
     }
   }

   // 2. 異步獲取樣式
   var styles = ''
   axios.get('theme.css').then((resp=> {
     const colorMap = {
       '#FF0000': 'themeColor'
     }
     styles = resp.data
     Object.keys(colorMap).forEach(key => {
       const value = colorMap[key]
       styles = styles.replace(new RegExp(key, 'ig'), value)
       console.log(styles)
     })
     writeNewStyle (styles, colors.red)
   }))

   // 3.換色
   // console.log 中輸入 writeNewStyle (styles, colors.blue)可以換藍色主題
   // console.log 中輸入 writeNewStyle (styles, colors.blue)可以換紅色主題
   function writeNewStyle (originalStyle, colors) {
     let oldEl = document.getElementById('temp-style')
     let cssText = originalStyle

     Object.keys(colors).forEach(key => {
       cssText = cssText.replace(new RegExp(key, 'ig'), colors[key])
     })
     const style = document.createElement('style')
     style.innerText = cssText
     style.id = 'temp-style'

     oldEl ? document.head.replaceChild(style, oldEl) : document.head.appendChild(style)
   }
 </script>
</body>
</html>

theme.css

.title {
  color: #FF0000
}


作者:seaasun
鏈接:https://www.jianshu.com/p/35e0581629d2
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

 

我的方法

根據上面兩篇文章的分析和總結。從易用性和複雜度上面權衡,我更傾向與使用class結合動態寫入樣式表的方案。

比如我們有下面這個結構代碼:

<div class="theme">
...
  <div class="th-bg-primary">
    <p class="th-primary">title</p>
    <p class="th-text">desc</p>
  </div>
...
</div>

我們把所有會根據主題改變的顏色的地方,都使用一個特定的class標記出來。然後維護一個class到顏色的css文件模板。比如這樣:

const bgPrinmaryColor = '#cecece'
const primaryColor = '#333333'
const textColor = '#666666'
let themeTemplate = `
  .theme.th-bg-primary {
    background-color: ${bgPrinmaryColor} !important;
  }
  .theme.th-primary {
    color: ${bgPrinmaryColor};
  }
  .theme.th-text {
    color: ${textColor};
  }`

const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = themeTemplate;
document.getElementsByTagName('HEAD').item(0).appendChild(style);

然後當用戶切換主題的時候,就修改themeTemplate裏的變量值,然後重更新把樣式表添加到界面上。當然添加之前可以刪除之前添加的樣式表。

這樣只用寫一份樣式表,又能動態切換。

優點:

1、簡單直觀,不編寫多個樣式文件

2、也不需要引入第三方插件來動態編譯樣式表

3、沒有新概念,新語法,新佔位符

缺點:

1、顏色和class綁定了

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章