【W(wǎng)eb技術(shù)】990- 前端站點(diǎn)一鍵支持暗色模式
作者:馮偉堯 & 石嘉 & 劉大暢

需求 & 現(xiàn)狀
作為近兩年的「新時(shí)尚」,暗色/深色模式已經(jīng)幾乎成為了各家系統(tǒng)、應(yīng)用以及 Web 站點(diǎn)的標(biāo)配功能。想要支持暗色模式,本質(zhì)是判斷在暗色模式下,更改生效的顏色值。在 Web 站點(diǎn)中,一種簡單的做法是通過 CSS 變量[1] 來實(shí)現(xiàn):
.button {
background-color: var(--color-bg-0);
}
body {
--color-bg-0: #fff;
}
body[theme-mode=dark] {
--color-bg-0: #000;
}
CSS 變量就是一些自定義的屬性,遵循正常的的樣式覆蓋原則。如上所示,button的背景顏色由 --color-bg-0 CSS 變量決定,當(dāng)給 body 設(shè)置 <body theme-mode="dark">...</body> 時(shí), --color-bg-0: #000 會覆蓋 --color-bg-0: #fff,此時(shí) button 背景色為黑色。
以上即是 Semi Design 實(shí)現(xiàn)暗色模式的大致原理。如果你想讓自己的站點(diǎn)支持暗色模式,一個(gè)關(guān)鍵點(diǎn)是,CSS 樣式中所有的顏色值必須使用 Semi Design 顏色變量(比如前面的--color-bg-0)。
現(xiàn)在的問題是,對于已有項(xiàng)目,為了支持暗色模式,必須將項(xiàng)目中所有顏色值手動替換為 Semi 顏色變量,這個(gè)過程非常繁瑣且工作量巨大。因此本文將介紹 Semi 團(tuán)隊(duì)在內(nèi)部項(xiàng)目中如何通過自動化的方式來解決這個(gè)問題。
為了避免理解歧義,這里統(tǒng)一名詞概念:
顏色字面量:指樣式文件中確定的顏色值,如 white,#FFF,rgb(23,45,0)等;
顏色變量:指與顏色相關(guān)的 CSS 變量,這里特指 Semi 顏色變量,如 --color-primary;
解決思路
主要關(guān)注兩點(diǎn):
關(guān)注點(diǎn)一:如何解析識別 css/scss/stylus 等樣式文件中的【顏色字面量】,如:background: white 中的 white。
關(guān)注點(diǎn)二:計(jì)算識別的【顏色字面量】是否可以替換為 Semi 主題包中提供的【顏色變量】如:var(--color-bg-0)。如果【顏色字面量】與【顏色變量】對應(yīng)的顏色值相同或相近則認(rèn)為可以替換。
對于第二點(diǎn),計(jì)算是否有對應(yīng)的顏色變量,可以使用 chorma-js[2]。chorma-js 可以實(shí)現(xiàn)任意顏色格式之間的轉(zhuǎn)換以及計(jì)算顏色之間的相似度,比如:chroma.distance(color1, color2)。
更重要的是第一點(diǎn),如何解析識別【顏色字面量】,此時(shí)首先想到的是 PostCSS。
PostCSS[3]
PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.
PostCSS 與 CSS 的關(guān)系,就相當(dāng)于 Babel 與 JavaScript。PostCSS 可以將 CSS 解析為抽象語法樹 (AST),并提供了 API 允許分析和轉(zhuǎn)換 CSS 文件的內(nèi)容。
如下圖所示,左邊為一段 CSS 代碼,右邊為使用 PostCSS 解析后的結(jié)果(可訪問 AST explorer 站點(diǎn)[4] 體驗(yàn)):

不僅僅是 CSS,通過引入插件,PostCSS 可以解析任何形式的樣式代碼,比如 Sass, Less, Stylus以及 JSX 中的樣式代碼。
如何識別顏色值
CSS 中指定顏色值的方式有三類,分別為:
顏色關(guān)鍵字,如: white,blue等等;
RGB 16 進(jìn)制形式,如:
#FFF;函數(shù)形式,如:
rgb(255, 255, 0)或者rgba(255, 0, 255, 0.5),hsl(0deg, 0%, 13%)或者hsla(0deg, 0%, 13%, 1)或者hwb(...)gray(...);
因此借助 PostCSS 解析后的結(jié)果,針對以上三種形式的顏色值,識別算法為:
CSS 屬性值中是否包含窮舉的 顏色關(guān)鍵字[5]; CSS 屬性值以 '#' 開頭則認(rèn)為是 RGB 16 進(jìn)制形式;
CSS 屬性值為函數(shù)形式且名稱為
rgb,rgba,hsl,hsla等則認(rèn)為是函數(shù)形式。
由此,基于 PostCSS 我們可以完成樣式的解析以及顏色的識別。如果要達(dá)到自動完成項(xiàng)目中樣式文件的顏色值替換工作,只需要添加收集項(xiàng)目中的樣式文件的邏輯,并封裝為 CLI 工具方便使用即可。
PostCSS 已經(jīng)能夠滿足我們的需求,然而,還有另一種實(shí)現(xiàn)方式,Stylelint。
Stylelint[6]
A mighty, modern linter that helps you avoid errors and enforce conventions in your styles.
Stylelint 與 CSS 的關(guān)系,就相當(dāng)于 ESLint 與 JavaScript 的關(guān)系。Stylelint 是針對樣式文件的代碼審查工具。Stylelint 底層基于 PostCSS,對樣式文件進(jìn)行解析分析,可以對 CSS/Sass/Stylus/Less 等樣式文件進(jìn)行審查。
比如 Stylelint 其中一個(gè)規(guī)則,color-named,不允許使用顏色關(guān)鍵字,效果如下:

當(dāng)我們在代碼中使用了類似 white 顏色關(guān)鍵字,Stylelint 會給出提示,借助編輯器插件,我們能夠在編碼過程中獲得實(shí)時(shí)提示,非常方便。
每個(gè)規(guī)則在 Stylelint 中都是一個(gè)插件,插件的輸入是樣式文件通過 PostCSS 解析后的抽象語法樹,插件可以基于 PostCSS API[7] 遍歷語法樹,分析/修改樣式代碼。因此類似規(guī)則 color-named,我們也可以實(shí)現(xiàn)一個(gè) Stylelint 插件,分析并識別樣式文件中的【顏色字面量】,并給出提示,針對有對應(yīng)的 Semi 顏色變量的,支持自動替換(autofix)。具體可以查看官方的插件開發(fā)文檔[8]。
相比 PostCSS,使用 Stylelint 的好處是:
不需要關(guān)心樣式文件的解析,只需要關(guān)注拿到解析結(jié)果后如何分析 不需要關(guān)心根據(jù)不同的樣式語言,引入不同的 PostCSS 解析插件 StyleLint 有完善的 CLI 工具以及 VS Code 插件,可以直接復(fù)用
綜上我們最終基于 Stylelint 來實(shí)現(xiàn)需求。
最終的能力 & 使用方式
配合 Stylelint 可以達(dá)到如下效果。
通過 StyleLint CLI 命令行工具一鍵【查找 & 替換】所有顏色變量;
通過 Stylelint 編輯器插件,寫碼過程中提示哪些【顏色字面量】沒有使用 Semi 【顏色變量】,可設(shè)置保存時(shí)自動替換;
Step 1: 安裝 npm 包
$ npm i -D stylelint @ies/stylelint-semi
Step 2: 添加/更改配置文件
根目錄下添加 .stylelintrc.json 文件,配置 @ies/stylelint-semi 插件與 semi/color-no-literal 規(guī)則:
{
"plugins": ["@ies/stylelint-semi"],
"rules": {
"semi/color-no-literal": [true, { "severity": "warning", "fixExact": false }]
}
}
Step 3: 執(zhí)行 CLI 命令
$ npx stylelint "**/*.scss"
執(zhí)行后,會檢測項(xiàng)目下所有 .scss 文件,以下寫法將會得到提示:
.banner {
color: white;
background: #eee;
border: 1px solid rgb(0, 0, 0);
}

如果要替換為 Semi 顏色變量,可以使用:
$ npx stylelint "**/*.scss" --fix
替換后的結(jié)果為:
.banner {
color: var(--color-white);
background: var(--color-tertiary-light-hover);
border: 1px solid var(--color-black);
}
Step 4: 編輯器提示
VS Code 中安裝 Stylelint 插件[9]。此時(shí)鼠標(biāo) hover 到顏色值會有以下提示:

實(shí)際效果展示
支持暗色模式前后的效果:
淺色版

深色版

歡迎反饋

這里是 Semi Design 團(tuán)隊(duì),Semi 是由互娛社區(qū)前端團(tuán)隊(duì)與 UED 團(tuán)隊(duì)共同設(shè)計(jì)開發(fā)并維護(hù)的設(shè)計(jì)系統(tǒng)。包括設(shè)計(jì)語言、React 組件庫、物料市場、Semi DV、主題商店等一整套全面、易用、優(yōu)質(zhì)的中后臺解決方案。幫助設(shè)計(jì)師與開發(fā)者更容易地打造高質(zhì)量的、用戶體驗(yàn)一致的、符合設(shè)計(jì)規(guī)范的 Web 應(yīng)用。
參考資料
CSS 變量: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
[2]chorma-js: https://github.com/gka/chroma.js/
[3]PostCSS: https://github.com/postcss/postcss
[4]AST explorer 站點(diǎn): https://astexplorer.net/#/2uBU1BLuJ1
[5]顏色關(guān)鍵字: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#color_keywords
[6]Stylelint: https://github.com/stylelint/stylelint
[7]PostCSS API: https://api.postcss.org/
[8]官方的插件開發(fā)文檔: https://stylelint.io/developer-guide/rules#add-autofix
[9]Stylelint 插件: https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint
[10]Postcss plugin 開發(fā)文檔: https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md
[11]Stylelint rule 開發(fā): https://stylelint.io/developer-guide/rules
[12]AST 調(diào)試工具: https://astexplorer.net/#/2uBU1BLuJ1

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章
