如何在 JavaScript 中获取页面上的所有自定义属性

Avatar of Tyler Gaw
Tyler Gaw

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

我们可以使用 JavaScript 获取 CSS 自定义属性的值。Robin 在 使用 JavaScript 获取 CSS 自定义属性值 中对此进行了详细说明。为了回顾,假设我们在 HTML 元素上声明了一个自定义属性

html {
  --color-accent: #00eb9b;
}

在 JavaScript 中,我们可以使用 getComputedStylegetPropertyValue 访问该值

const colorAccent = getComputedStyle(document.documentElement)
  .getPropertyValue('--color-accent'); // #00eb9b

完美。现在我们可以访问 JavaScript 中的强调色了。你知道什么很酷吗?如果我们在 CSS 中更改了该颜色,它也会在 JavaScript 中更新!方便。

但是,如果我们不仅需要在 JavaScript 中访问一个属性,而是需要访问一大堆属性怎么办呢?

html {
  --color-accent: #00eb9b;
  --color-accent-secondary: #9db4ff;
  --color-accent-tertiary: #f2c0ea;
  --color-text: #292929;
  --color-divider: #d7d7d7;
}

我们最终得到看起来像这样的 JavaScript

const colorAccent = getComputedStyle(document.documentElement).getPropertyValue('--color-accent'); // #00eb9b
const colorAccentSecondary = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-secondary'); // #9db4ff
const colorAccentTertiary = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-tertiary'); // #f2c0ea
const colorText = getComputedStyle(document.documentElement).getPropertyValue('--color-text'); // #292929
const colorDivider = getComputedStyle(document.documentElement).getPropertyValue('--color-text'); // #d7d7d7

我们重复了很多代码。我们可以通过将常见任务抽象到一个函数中来缩短每一行代码。

const getCSSProp = (element, propName) => getComputedStyle(element).getPropertyValue(propName);
const colorAccent = getCSSProp(document.documentElement, '--color-accent'); // #00eb9b
// repeat for each custom property...

这有助于减少代码重复,但我们仍然面临着不太理想的情况。每次我们在 CSS 中添加自定义属性时,我们都必须编写另一行 JavaScript 代码来访问它。如果我们只有几个自定义属性,这种方法可以工作,而且确实有效。我以前在生产项目中使用过这种设置。但是,也可以自动化这个过程。

让我们逐步了解通过创建一个可工作的事物来实现自动化的过程。

我们正在做什么?

我们将创建一个调色板,这是模式库中的一个常见功能。我们将从 CSS 自定义属性中生成一个颜色色板网格。

这是我们将逐步构建的 完整演示

A preview of our CSS custom property-driven color palette. Showing six cards, one for each color, including the custom property name and hex value in each card.
这是我们的目标。

让我们做好准备。我们将使用无序列表来显示我们的调色板。每个色板都是一个 <li> 元素,我们将使用 JavaScript 渲染它。

<ul class="colors"></ul>

网格布局的 CSS 与本文中的技术无关,因此我们不会详细介绍。它在 CodePen 演示 中提供。

现在我们已经准备好了 HTML 和 CSS,我们将专注于 JavaScript。以下是我们将使用代码执行的操作的概述

  1. 获取页面上的所有样式表,包括外部样式表和内部样式表
  2. 丢弃托管在第三方域上的所有样式表
  3. 获取剩余样式表的所有规则
  4. 丢弃任何不是基本样式规则的规则
  5. 获取所有 CSS 属性的名称和值
  6. 丢弃非自定义 CSS 属性
  7. 构建 HTML 以显示颜色色板

让我们开始吧。

步骤 1:获取页面上的所有样式表

我们需要做的第一件事是获取当前页面上的所有外部样式表和内部样式表。样式表作为全局文档的成员可用。

document.styleSheets

这将返回一个类似数组的对象。我们希望使用数组方法,因此我们将它转换为一个数组。让我们也把它放在一个函数中,我们将在本文中使用它。

const getCSSCustomPropIndex = () => [...document.styleSheets];

当我们调用 getCSSCustomPropIndex 时,我们会看到一个 CSSStyleSheet 对象数组,每个对象代表当前页面上的每个外部样式表和内部样式表。

The output of getCSSCustomPropIndex, an array of CSSStyleSheet objects

步骤 2:丢弃第三方样式表

如果我们的脚本在 https://example.com 上运行,那么我们想要检查的任何样式表也必须在 https://example.com 上。这是一个安全功能。从 CSSStyleSheet 的 MDN 文档

在某些浏览器中,如果从不同的域加载样式表,访问 cssRules 会导致 SecurityError

这意味着如果当前页面链接到托管在 https://some-cdn.com 上的样式表,我们就无法获取自定义属性(或任何样式)。我们在此处采用的方法仅适用于托管在当前域上的样式表。

CSSStyleSheet 对象具有 href 属性。它的值是样式表的完整 URL,例如 https://example.com/styles.css。内部样式表具有 href 属性,但其值为 null

让我们编写一个函数来丢弃第三方样式表。我们将通过比较样式表的 href 值与 current location.origin 来实现。

const isSameDomain = (styleSheet) => {
  if (!styleSheet.href) {
    return true;
  }


  return styleSheet.href.indexOf(window.location.origin) === 0;
};

现在,我们将 isSameDomain 用作 document.styleSheets 的过滤器。

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain);

丢弃了第三方样式表后,我们可以检查剩余样式表的内容。

步骤 3:获取剩余样式表的所有规则

我们对 getCSSCustomPropIndex 的目标是生成一个包含数组的数组。为了实现这一点,我们将结合使用数组方法来循环遍历、查找我们想要的值并组合它们。让我们朝着这个方向迈出第一步,生成一个包含每个样式规则的数组。

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(...sheet.cssRules), []);

我们使用 reduceconcat,因为我们想要生成一个扁平数组,其中每个一级元素都是我们感兴趣的。在这个代码片段中,我们迭代每个 CSSStyleSheet 对象。对于每个对象,我们需要它的 cssRules。从 MDN 文档

只读 CSSStyleSheet 属性 cssRules 返回一个实时的 CSSRuleList,它提供了构成样式表的每个 CSS 规则的实时更新列表。列表中的每个项目都是一个 CSSRule,定义一个单独的规则。

每个 CSS 规则都是选择器、大括号和属性声明。我们使用扩展运算符 ...sheet.cssRules 将每个规则从 cssRules 对象中取出并放置到 finalArr 中。当我们记录 getCSSCustomPropIndex 的输出时,会得到一个 CSSRule 对象的一级数组。

Example output of getCSSCustomPropIndex producing an array of CSSRule objects

这为我们提供了所有样式表的所有 CSS 规则。我们希望丢弃其中的一些,所以让我们继续前进。

步骤 4:丢弃任何不是基本样式规则的规则

CSS 规则有不同的类型。CSS 规范使用常量名称和整数定义每种类型。最常见的规则类型是 CSSStyleRule。另一种规则类型是 CSSMediaRule。我们使用它们来定义媒体查询,例如 @media (min-width: 400px) {}。其他类型包括 CSSSupportsRuleCSSFontFaceRuleCSSKeyframesRule。有关完整列表,请参阅 MDN 文档中 CSSRule 的“类型常量”部分

我们只对定义自定义属性的规则感兴趣,在本篇文章中,我们将重点介绍 CSSStyleRule。这确实排除了在定义自定义属性时有效的 CSSMediaRule 规则类型。我们可以使用类似于我们在此演示中用来提取自定义属性的方法,但我们将排除这种特定的规则类型,以限制演示的范围。

为了将我们的关注点缩小到样式规则,我们将编写另一个数组过滤器

const isStyleRule = (rule) => rule.type === 1;

每个 CSSRule 都有一个 type 属性,它返回该类型常量的整数。我们使用 isStyleRule 来过滤 sheet.cssRules

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules].filter(isStyleRule)
  ), []);

需要注意的是,我们将 ...sheet.cssRules 括在方括号中,以便我们可以使用数组方法过滤器。

我们的样式表只有 CSSStyleRules,所以演示结果与之前相同。如果我们的样式表包含媒体查询或 font-face 声明,isStyleRule 将会丢弃它们。

步骤 5:获取所有属性的名称和值

现在我们已经有了我们想要的规则,我们可以获取构成它们的属性。CSSStyleRule 对象有一个样式属性,该属性是一个 CSSStyleDeclaration 对象。它由标准的 CSS 属性组成,例如 colorfont-familyborder-radius,以及自定义属性。让我们将它添加到 getCSSCustomPropIndex 函数中,以便它查看每条规则,并在此过程中构建一个二维数组。

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules]
      .filter(isStyleRule)
      .reduce((propValArr, rule) => {
        const props = []; /* TODO: more work needed here */
        return [...propValArr, ...props];
      }, [])
  ), []);

如果我们现在调用它,我们将得到一个空数组。我们还有更多工作要做,但这奠定了基础。因为我们想要最终得到一个数组,所以我们使用累加器(它是 reduce 的第二个参数)从一个空数组开始。在 reduce 回调函数的函数体中,我们有一个占位符变量 props,我们将在其中收集属性。return 语句将来自前一次迭代的数组(累加器)与当前 props 数组组合在一起。

现在,两者都是空数组。我们需要使用 rule.style 来填充 props,为当前规则中的每个属性/值创建一个数组。

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules]
      .filter(isStyleRule)
      .reduce((propValArr, rule) => {
        const props = [...rule.style].map((propName) => [
          propName.trim(),
          rule.style.getPropertyValue(propName).trim()
        ]);
        return [...propValArr, ...props];
      }, [])
  ), []);

rule.style 是类数组的,所以我们再次使用展开运算符将它的每个成员放入一个数组中,然后使用 map 对它进行循环。在 map 回调中,我们返回一个包含两个成员的数组。第一个成员是 propName(包括 colorfont-family--color-accent 等)。第二个成员是每个属性的值。为了获得它,我们使用 getPropertyValue 方法,它是 CSSStyleDeclaration 的方法。它接受一个参数,即 CSS 属性的字符串名称。 

我们在名称和值上都使用 trim 来确保我们不包含任何有时会遗留的开头或结尾空格。

现在,当我们调用 getCSSCustomPropIndex 时,我们将得到一个二维数组。每个子数组包含一个 CSS 属性名称和一个值。

Output of getCSSCustomPropIndex showing an array of arrays containing every property name and value

这就是我们想要的!好吧,几乎。我们正在获取所有属性,包括自定义属性。我们需要再过滤一次,以删除那些标准属性,因为我们只想要自定义属性。

步骤 6:丢弃非自定义属性

要确定一个属性是否是自定义属性,我们可以查看它的名称。我们知道自定义属性必须以两个连字符 (--) 开头。这是 CSS 世界中的唯一特性,因此我们可以用它来编写一个过滤器函数。

([propName]) => propName.indexOf("--") === 0)

然后,我们将其用作 props 数组的过滤器。

const getCSSCustomPropIndex = () =>
  [...document.styleSheets].filter(isSameDomain).reduce(
    (finalArr, sheet) =>
      finalArr.concat(
        [...sheet.cssRules].filter(isStyleRule).reduce((propValArr, rule) => {
          const props = [...rule.style]
            .map((propName) => [
              propName.trim(),
              rule.style.getPropertyValue(propName).trim()
            ])
            .filter(([propName]) => propName.indexOf("--") === 0);


          return [...propValArr, ...props];
        }, [])
      ),
    []
  );

在函数签名中,我们有 ([propName])。在那里,我们使用数组解构来访问 props 中每个子数组的第一个成员。从那里,我们对属性的名称进行 indexOf 检查。如果 -- 不在 propName 的开头,那么我们就不会将其包含在 props 数组中。

当我们记录结果时,我们得到了我们想要的确切输出:一个二维数组,包含每个自定义属性及其值,没有其他属性。

The output of getCSSCustomPropIndex showing an array of arrays containing every custom property and its value

展望未来,创建属性/值映射并不一定需要这么多代码。在 CSS Typed Object Model Level 1 草案中有一个替代方案,它使用 CSSStyleRule.styleMapstyleMap 属性是 CSS 规则的每个属性/值的类数组对象。我们还没有它,但如果我们有它,就可以通过删除 map 来缩短上面的代码。

// ...
const props = [...rule.styleMap.entries()].filter(/*same filter*/);
// ...

在撰写本文时,Chrome 和 Edge 实现了 styleMap,但其他主要浏览器没有。因为 styleMap 处于草案阶段,所以我们无法保证我们真的会得到它,也没有必要在演示中使用它。不过,知道这是一个未来的可能性还是很有趣的!

我们已经拥有了我们想要的数据结构。现在,让我们使用这些数据来显示颜色样本。

步骤 7:构建 HTML 以显示颜色样本

将数据整理成我们需要的精确形状是最困难的工作。我们还需要一点 JavaScript 来渲染我们漂亮的颜色样本。与其记录 getCSSCustomPropIndex 的输出,不如将其存储在一个变量中。

const cssCustomPropIndex = getCSSCustomPropIndex();

以下是我们用来在本帖开头创建颜色样本的 HTML 代码。

<ul class="colors"></ul>

我们将使用 innerHTML 来填充该列表,为每种颜色创建一个列表项。

document.querySelector(".colors").innerHTML = cssCustomPropIndex.reduce(
  (str, [prop, val]) => `${str}<li class="color">
    <b class="color__swatch" style="--color: ${val}"></b>
    <div class="color__details">
      <input value="${prop}" readonly />
      <input value="${val}" readonly />
    </div>
   </li>`,
  "");

我们使用 reduce 来遍历自定义 prop 索引,并为 innerHTML 构建一个 HTML 风格的字符串。但是 reduce 不是唯一的方法。我们可以使用 mapjoinforEach。任何构建字符串的方法都可以在此处使用。这只是我更喜欢的方式。

我想重点介绍几段代码。在 reduce 回调签名中,我们再次使用数组解构 [prop, val],这次是用来访问每个子数组的两个成员。然后,我们在函数体中使用 propval 变量。

为了显示每种颜色的示例,我们使用一个带有内联样式的 b 元素。

<b class="color__swatch" style="--color: ${val}"></b>

这意味着我们将最终得到类似于以下 HTML 代码的代码:

<b class="color__swatch" style="--color: #00eb9b"></b>

但这如何设置背景颜色?在 完整的 CSS 代码 中,我们使用自定义属性 --color 作为每个 .color__swatchbackground-color 值。因为外部 CSS 规则从内联样式继承,所以 --color 是我们在 b 元素上设置的值。

.color__swatch {
  background-color: var(--color);
  /* other properties */
}

现在,我们有了颜色样本的 HTML 显示,代表了我们的 CSS 自定义属性!


本演示侧重于颜色,但该技术并不局限于自定义颜色属性。我们没有理由不能将此方法扩展到生成模式库的其他部分,例如字体、间距、网格设置等。任何可能以自定义属性形式存储的内容都可以使用这种技术自动显示在页面上。