当某个选择必须让控件、文本和行为保持一致时,请更新组件状态。若只更改呈现方式,则覆盖 CSS。本指南通过 Mac 上 Safari 中的本地平铺预览,说明如何选择并验证这两种方法。

读者要完成的任务很具体:修改自己编写的一个小网页,让形状控件保持同步,同时让装饰样式易于编辑。这里的组件状态指由页面自身的更新函数管理的 JavaScript 对象。本文不假设使用任何框架或产品专属编辑功能。

哪些更改应归入组件状态?

先问自己:更改后,哪些内容必须保持一致?如果选择圆形平铺必须同时更新复选框、预览和文字说明,就应在状态中为这个选择指定唯一的所有者,再从中推导出这三项输出。这是一项实现建议:它能明确呈现选择与显示结果之间的关系。

CSS 自定义属性(例如 --tile-size)用途不同。它会向使用 var() 引用它的声明提供值。自定义属性会参与层叠,通常还会继承;即使由 JavaScript 设置,其行为仍遵循 CSS 规则。CSS 自定义属性规范定义了这一约定。

编辑前可参考以下判断表:

所需更改 建议的归属 本例中的原因
读者选择圆形或方形平铺 组件状态 复选框、说明和形状必须一致
读者选择平铺尺寸 组件状态,并渲染到 CSS 中 范围控件、说明和尺寸共用一个值
所有平铺都使用不同的装饰色 样式表声明 无需更新任何已选值或文本
预览需要更多周围空间 样式表声明 这会更改平铺周围的布局
检查时需要临时调整尺寸 本地 CSS 覆盖 这是之后应移除的视觉实验

具体归属取决于任务。在这个预览中,颜色属于 CSS,因为它是固定的装饰。如果之后添加颜色选择器和所选颜色的说明,就应将该选择视为状态,并渲染其视觉效果。同一个选择不要创建两个可以独立编辑的值。

原生控件也有自己的状态。对于复选框,请读取其当前的 checked 属性。HTML 的 checked 属性指定默认选中状态,不会跟踪用户之后的更改。MDN 的复选框属性参考对此作了说明。

在 Mac 上构建一个独立的预览

使用纯文本代码编辑器,将以下完整文档保存为 tile-preview.html,然后在 Safari 中打开。保留 .html 扩展名。示例包含 CSS 和 JavaScript,使用系统字体,并绘制一个无需外部资源的简单几何平铺。

样式表位于文档的 head 中。HTML 将 style 元素定义为嵌入 CSS 的方式,并区分自包含的内部样式与引用其他资源的样式表。让示例保持自包含,可以清楚地展示其样式输入。参见 HTML 标准中的样式和脚本章节。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tile preview</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      margin: 2rem;
      color: #222;
      background: #fff;
    }
    label { display: block; margin-block: 1rem; }
    .preview { padding: 24px; background: #eee; }
    .tile {
      width: 80px;
      height: 80px;
      border-radius: 0;
      width: var(--tile-size, 80px);
      height: var(--tile-size, 80px);
      border-radius: var(--tile-radius, 0);
      background-color: #547899;
    }
  </style>
</head>
<body>
  <h1>Tile preview</h1>
  <label>
    <input id="round" type="checkbox"> Round tile
  </label>
  <label>
    Tile size
    <input id="size" type="range" min="40" max="160"
           step="1" value="80">
  </label>
  <button id="reset" type="button">Reset choices</button>
  <p id="summary"></p>
  <div class="preview">
    <div id="tile" class="tile" aria-hidden="true"></div>
  </div>
  <script>
    const state = { rounded: false, size: 80 };
    const round = document.getElementById('round');
    const size = document.getElementById('size');
    const summary = document.getElementById('summary');
    const tile = document.getElementById('tile');
    const reset = document.getElementById('reset');

    function render() {
      round.checked = state.rounded;
      size.value = String(state.size);
      tile.style.setProperty('--tile-size', `${state.size}px`);
      tile.style.setProperty('--tile-radius', state.rounded ? '50%' : '0');
      summary.textContent = `Tile A: ${state.rounded ? 'round' : 'square'}, ${state.size}px.`;
    }

    function update(patch) {
      const next = { ...state, ...patch };
      if (typeof next.rounded !== 'boolean' ||
          !Number.isInteger(next.size) ||
          next.size < 40 || next.size > 160) {
        throw new RangeError('Use a boolean and a size from 40 to 160.');
      }
      Object.assign(state, next);
      render();
    }

    round.addEventListener('change', () => {
      update({ rounded: round.checked });
    });
    size.addEventListener('input', () => {
      update({ size: Number(size.value) });
    });
    reset.addEventListener('click', () => {
      update({ rounded: false, size: 80 });
    });
    render();
  </script>
</body>
</html>

此处使用的样式接口有文档说明:element.style.setProperty() 会写入内联声明,其中也包括自定义属性。属性名采用 CSS 拼写形式,例如 --tile-size。参见 MDN 的 setProperty 参考。

尝试勾选 Round tile,将 Tile size 移到其他数值,然后选择 Reset choices。前两项操作应同时更新形状和说明;重置应恢复为 80 像素的方形。在此程序中,仅为 state 中的字段赋值不会调用 render()。请通过 update() 处理更改,让验证和渲染一并执行。

已在 Node 中使用模拟控件和模拟样式对象,检查了更新与渲染逻辑在初始值、更改后的值、无效尺寸及重置情况下的表现。这些检查均通过,但不能证明浏览器中的渲染效果。请使用上述可视检查在 Safari 中验证文件。

何时应覆盖 CSS 声明?

要永久更改装饰样式,请编辑已保存的样式表。例如,将 .tile 的 background-color 改为另一种 CSS 颜色,或调整 .preview 的内边距。在此程序中,这两项更改都不需要新增状态字段,因为它们都不代表已选的选项。

如需进行临时实验,可通过 Safari → Settings… → Advanced → Show features for web developers 启用 Safari 检查器。打开 Develop → Show Web Inspector,或按下 Option-Command-I。这些是在 Mac 上启用检查器的步骤,见 WebKit 的检查器指南。

打开示例后,在控制台运行以下完整代码片段。它会临时更改平铺的颜色,并提供一个用于移除该覆盖的函数:

const previewTile = document.getElementById('tile');
if (!previewTile) throw new Error('Open tile-preview.html first.');
previewTile.style.setProperty('background-color', '#8069a6');
function restoreTileColor() {
  previewTile.style.removeProperty('background-color');
}
// Run restoreTileColor() when finished comparing colors.

移除这条内联声明后,剩余的层叠规则会再次决定颜色。这不会删除样式表中的颜色声明。移除方法见 MDN 的 removeProperty 参考。在此示例中,如果要在重新加载后保留所选颜色,请编辑并保存 CSS 源码。

临时覆盖不要用于渲染器负责的值。如果手动更改 --tile-size,说明仍会报告状态中的值,而下一次调用 render() 时,你的实验值会被替换。若希望整个组件保持一致,请通过控件或更新函数更改尺寸。

为什么覆盖看起来没有效果?

在提高选择器优先级之前,先排查该值的应用路径。声明本身可能有效,却在层叠中落败;也可能是它针对的自定义属性已不再被胜出的声明使用。

  1. 检查目标。 确认你选中的是显示形状的元素。
  2. 检查输入。 在本例中,尺寸必须是 40 到 160 之间的整数。无效的更新会在更改状态前抛出异常。
  3. 检查使用该值的声明。 平铺的尺寸使用 var(--tile-size)。胜出的直接 width 声明可以让宽度绕过该值,而高度仍使用该值。
  4. 检查优先级。 比较适用的声明、重要性、层和优先级。
  5. 检查下一次交互。 如果更改在移动控件后消失,请检查 render() 写入了什么。

普通的内联声明优先于普通的作者样式表规则。不过,重要性、动画和过渡会影响更广泛的优先顺序。较后的声明不一定自动胜出,更具体的选择器也无法解决所有冲突。MDN 的层叠指南说明了这些情况。

还要区分内联值和解析后的值。element.style 会显示内联样式,因此内联值为空并不表示元素没有样式。getComputedStyle() 会读取应用活动样式表后的解析值。参见 MDN 的 style 参考和 getComputedStyle 参考。

在示例页面的控制台中运行以下完整诊断代码片段:

const inspectedTile = document.getElementById('tile');
if (!inspectedTile) throw new Error('Open tile-preview.html first.');
console.table({
  inlineSizeToken: inspectedTile.style.getPropertyValue('--tile-size'),
  inlineWidth: inspectedTile.style.getPropertyValue('width'),
  resolvedWidth: getComputedStyle(inspectedTile).width,
  resolvedHeight: getComputedStyle(inspectedTile).height
});

刚打开未经修改的示例时,预期值是:内联尺寸标记为 80px,内联宽度为空,解析后的尺寸为 80px。这表明内联自定义属性可以为样式表声明提供值,而不会创建内联的 width 声明。

浏览器和资源方面有哪些限制?

MDN 标注,所有主流浏览器自 2017 年 4 月起都广泛支持 CSS 自定义属性。但这并不能证明所有较旧的浏览器也支持它们。示例在 var() 声明之前包含常规尺寸声明,因此当浏览器不支持自定义属性时,平铺仍会呈现静态外观。参见 MDN 的自定义属性参考。

var(--tile-size, 80px) 中的回退值不能让无法解析自定义属性的浏览器获得兼容性。它也不会验证传入的每个标记:像 blue 这样的已定义值不会仅仅因为存在回退值就变成有效宽度。参见 MDN 的自定义属性指南了解这些限制。

如果较旧的目标浏览器不支持自定义属性,受支持的替代方案是修改渲染器,让它使用 setProperty() 写入常规内联的 width、height 和 border-radius 声明。保留相同的状态验证和控件同步方式。这会改变渲染约定,因此应移除互相冲突的渲染器写入,并在目标浏览器中进行验证,不要假设支持情况完全相同。

对于这个本地预览,实际做法很明确:将形状和尺寸选择放在状态中,通过少量 CSS 输入渲染这些选择,并将装饰样式保留在样式表中。每次更改后,都要测试一次控件交互和重置操作。预览、控件和说明应始终展示相同的所选值。

来源