2023-08-12
Hello CSS 变量
很早就听过和简单看过 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
这是一个空按钮
完整实现如下,已折叠,点击展开
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 一下可能的安全漏洞




回到顶部