很早就听过和简单看过 CSS 变量了,但是一直没有正式找场景使用过,这两天实装了一下本站的 Setting 页,里面用了 CSS 变量,本文总结一下经验:
Setting | 系统设置# 声明和使用变量 ↵
可以按照如下方式声明和使用一个 CSS 变量
00:root { 01 --REM: 14px; 02}
00html { 01 font-site: var(--REM); 02}
在上述两个例子中,声明了一个叫做
--REM
的变量,其值为 14px, 然后通过 var()
的方式将其绑定到 html 的 fontSizte 属性上# :root 是什么 ↵
通过 :root 可以指定变量的作用域为全局。在大多数情况下我们只需要声明到
:root
全局中即可, 我暂时没有碰到需要单独定义作用域的情况, 可以通过这种方式单独指定变量作用域:00div { 01 --PRIMARY-COLOR: #BBB; 02}
叙述 CSS 变量的用法不是本文重点,相关细节可以参考 MDN:
MDN - 使用 CSS 自定义属性(变量)# 站点主题实践 ↵
CSS 变量一个最常见的用法就是实现站点主题的配置化、动态化 —— 以往 less / sass 那种预处理器的主题是要过静态编译才出来的,很多时候有各种限制,不好用, 现在借助 CSS 变量, 配合 js 就能做更完善的用户主题实践
定义类型
实现站点主题配置,需要先设计相关类型,举个例子:实现按钮颜色的配置
00// css-vars.tsx 01// CSS 变量 02export interface CSSVars { 03 buttonColor: string 04} 05 06// 获取默认变量配置 07export function initialCSSVars(): CSSVars { 08 return { 09 buttonColor: '#e3e3e3' 10 } 11}
CSSVars 渲染到 <style>
还需要将这个 CSSVars 类型转成一段 CSS 内容并注入到 style 标签中才能使其作为 CSS 变量使用:
00// render-css-vars.tsx 01import { CSSVars} from './css-vars' 02 03// 将 vars 的内容渲染到 style 中 04// 以此处的 CSSVars 来说,渲染结果类似这样: 05// <style> 06// :root { --buttonColor: #BBB; } 07// <\/style> 08export function renderCSSVars( 09 vars: CSSVars, 10 style?: Element 11): void { 12 const allDefine = Object.keys(vars).map(k => { 13 const v = vars[k as keyof CSSVars] 14 const cssValue = typeof v === 'number' ? `${v}px` : v 15 return `--${k}:${cssValue};` 16 }).join('\n') 17 18 if (style) { 19 style.innerHTML = `:root { ${allDefine} }` 20 } else { 21 console.error(`style#CSS_VARS NotFound !`) 22 } 23}
考虑持久化
在正式调用 renderCSSVars 之前我们还需要考虑怎么保存用户配置的 CSSVars, 即持久化, 可以使用 localStorage 进行存储:
00import { 01 CSSVars, 02 initialCSSVars, 03} from './css-vars' 04 05/** 保存 CSS Vars */ 06export function saveCSSVars(cssVars: CSSVars): void { 07 const j = JSON.stringify(cssVars) 08 localStorage.setItem('EXAMPLE_CSS_VARS', j) 09} 10 11/** 读取 CSSVars */ 12export function loadCSSVars(): CSSVars { 13 // 服务端渲染场景直接返回初始 CSS 变量即可 14 if (typeof window === 'undefined') { 15 return initialCSSVars() 16 } 17 18 const j = localStorage.getItem('EXAMPLE_CSS_VARS') 19 const initialVars = initialCSSVars() 20 // 如果之前没存过, 直接返回 21 if (!j) return initialVars 22 23 try { 24 // 注意这里是覆盖的 25 return { 26 ...initialVars, 27 ...JSON.parse(j) as CSSVars 28 } 29 } catch (error) { 30 console.error('load css vars with error', error) 31 // 出错后直接返回 initialVars 32 return initialVars 33 } 34}
用户配置界面 (demo)
最后一步是写组件让用户可以配置并持久化存储自己的 CSSVars, 以下是我实现的一个简单的组件 DEMO:
请点击下面色框选取颜色,
当前色值: #e3e3e3
当前色值: #e3e3e3
完整实现如下,已折叠,点击展开
00// user-cssvars.tsx 01import React from 'react' 02import { Col, Button } from 'rally/@@' 03import { 04 renderCSSVars 05} from './render-css-vars' 06import { 07 loadCSSVars, 08 saveCSSVars, 09} from './storage-css-vars' 10 11function getStyleElement(): Element { 12 const styleId = 'example-style' 13 let style = document.querySelector(`#${styleId}`); 14 if (style) return style; 15 // 如果没有则构造一个并插入到 body 中 16 style = document.createElement('style') 17 style.id = styleId 18 document.body.appendChild(style) 19 return style 20} 21 22export function UserCssVars() { 23 // 读取变量并作为 react state 使用 24 const [cssvars, setCssVars] = React.useState( 25 () => loadCSSVars() 26 ); 27 28 React.useEffect(() => { 29 // 每次状态变化后渲染并存储 30 // 这里刷新比较频繁, 写个 timer 优化一下 31 const timer = setTimeout(() => { 32 renderCSSVars(cssvars, getStyleElement()) 33 saveCSSVars(cssvars) 34 }, 32); 35 return () => { 36 clearTimeout(timer); 37 } 38 }, [cssvars.buttonColor]); 39 40 return ( 41 <Col> 42 <div> 43 <div> 44 请点击下面色框选取颜色, <br /> 45 当前色值: {cssvars.buttonColor} 46 </div> 47 <input 48 type="color" 49 value={cssvars.buttonColor} 50 onChange={e => { 51 setCssVars({ 52 buttonColor: e.target.value 53 }); 54 }} 55 /> 56 </div> 57 <Button 58 icon="play" 59 style={{ 60 backgroundColor: cssvars.buttonColor, 61 }}> 62 这是一个空按钮 63 </Button> 64 </Col> 65 ) 66}
# EOF ↵
至此一个基于 CSS 变量的主题化场景配置实践就算完成了 —— 在设置颜色的过程中,将最新的色值重新持久化存储,后续用户再刷新的时候就可以拿出来并且应用到 CSS 变量中从而主题化站点样式, 后续只需要横向拓展更多的样式选项即可
但是这里还有些可以优化的点,能想到的比如说持久化存储写到后台数据库,这样就可以多端保持主题同步。另外可以考虑弄一个 style CSS 变量直出服务, 让页面打开的一瞬间就主题化而不是等 js 和 ajax 加载完:
00<style href="/api/cssvars"></style>
然后对应的这个服务其实就是去读用户配置的 css 变量并渲染直出到用户侧:
00app.get('/api/cssvars', async (ctx, next) => { 01 await next() 02 const cssvars = await loadUserCssVarsFromDB(ctx) 03 const styleContent = renderCssVars(cssvars) 04 ctx.sendFile( 05 contentType: '.css', 06 content: ` 07 :root { 08 buttonColor: ${cssvars.buttonColor}; 09 } 10 `, 11 ) 12})
* 注意这里可能有各种注入或者其他 Web 安全问题, 如果你真的要搞一个这样的方案建议要好好 review 一下可能的安全漏洞