恭喜你,走到这里了!

在前面的七章中,我们系统地学习了 CSSOM 的方方面面:从获取样式表、操作样式规则,到修改内联样式、获取计算样式,再到 CSSOM View 的滚动与布局,以及 CSS Typed OM 这一前沿技术。每一章都像一块拼图,让你对 CSSOM 的理解越来越完整。

但知识只有应用到实际场景中,才真正产生价值。这一章,我们将把前面所有的拼图 组装起来,投入真实的开发场景。

本章不引入全新的 API,而是聚焦于 整合与应用 ——通过多个实战案例,展示如何将 CSSOM 的各项能力组合使用。同时,我们也会梳理出 CSSOM 开发中的 最佳实践 和 避坑指南,帮助你在实际项目中写出更优雅、更高效的代码。

一、动态切换主题样式(暗色 / 亮色模式)

这是最优雅的方式。将主题相关的颜色定义为 CSS 变量,切换时只需修改变量的值。

方案一:使用 CSS 自定义属性(推荐)
<style>
    :root {
        --bg-color: #ffffff;
        --text-color: #333333;
        --primary-color: #007bff;
        --border-color: #e0e0e0;
        --card-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }

    [data-theme="dark"] {
        --bg-color: #1a1a2e;
        --text-color: #e8e8e8;
        --primary-color: #6c63ff;
        --border-color: #333355;
        --card-shadow: 0 2px 8px rgba(0,0,0,0.5);
    }

    body {
        background: var(--bg-color);
        color: var(--text-color);
        transition: background 0.3s, color 0.3s;
        font-family: Arial, sans-serif;
        padding: 40px;
    }
    .card {
        background: var(--bg-color);
        border: 1px solid var(--border-color);
        box-shadow: var(--card-shadow);
        padding: 20px;
        border-radius: 8px;
        transition: all 0.3s;
    }
    .btn {
        background: var(--primary-color);
        color: #fff;
        border: none;
        padding: 10px 24px;
        border-radius: 4px;
        cursor: pointer;
    }
</style>

<div class="card">
    <h2>主题切换演示</h2>
    <p>当前主题由 data-theme 属性控制</p>
    <button class="btn" id="themeToggle">切换到暗色模式</button>
</div>

<script>
    const root = document.documentElement;
    const toggleBtn = document.getElementById('themeToggle');

    // 读取用户偏好
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const savedTheme = localStorage.getItem('theme');

    // 初始化主题:优先使用本地存储,其次使用系统偏好
    if (savedTheme) {
        root.setAttribute('data-theme', savedTheme);
    } else if (prefersDark) {
        root.setAttribute('data-theme', 'dark');
    }

    toggleBtn.addEventListener('click', function() {
        const current = root.getAttribute('data-theme');
        const next = current === 'dark' ? 'light' : 'dark';
        root.setAttribute('data-theme', next);
        localStorage.setItem('theme', next);
        this.textContent = next === 'dark' ? '切换到亮色模式' : '切换到暗色模式';
    });

    // 监听系统主题变化(用户无感知切换)
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
        if (!localStorage.getItem('theme')) {
            // 用户没有手动设置过,跟随系统
            root.setAttribute('data-theme', e.matches ? 'dark' : 'light');
        }
    });
</script>

这个方案的核心是:

  1. 使用 data-theme 属性作为主题标识。

  2. CSS 变量在不同主题下有不同的值。

  3. JavaScript 只负责切换属性,所有样式变化由 CSS 自动完成。

方案二:使用 CSSOM 动态修改样式表

如果你需要更精细的控制——比如动态计算主题色,或者需要兼容不支持 CSS 变量的浏览器,可以通过 CSSOM 直接修改样式表中的规则。

// 动态修改样式表中的规则,实现主题切换
function switchTheme(mode) {
    const sheet = document.styleSheets[0];
    
    // 找到 body 的样式规则(假设它是第一条规则)
    const bodyRule = sheet.cssRules[0];
    if (bodyRule.selectorText === 'body') {
        if (mode === 'dark') {
            bodyRule.style.setProperty('background-color', '#1a1a2e');
            bodyRule.style.setProperty('color', '#e8e8e8');
        } else {
            bodyRule.style.setProperty('background-color', '#ffffff');
            bodyRule.style.setProperty('color', '#333333');
        }
    }
}
决策建议

场景

推荐方案

现代项目,支持 CSS 变量

CSS 自定义属性 + data-* 属性

需要兼容旧浏览器

CSSOM 修改样式表规则

需要动态计算颜色(如按 HSL 生成主题色)

CSSOM + JavaScript 计算

简单场景,只切换预定义主题

classList 切换类名

二、实现 CSS 动画的 JavaScript 控制

CSS 动画很强大,但有些场景需要 JavaScript 的介入——比如根据用户鼠标位置控制动画进度,或者在动画过程中动态改变参数。

这是一个常见的需求:随着用户滚动页面,动画进度同步推进。结合 scroll 事件和 requestAnimationFrame 来实现:

<style>
    .progress-ring {
        position: fixed;
        top: 20px;
        right: 20px;
        width: 60px;
        height: 60px;
        border-radius: 50%;
        background: conic-gradient(var(--progress-color, #007bff) var(--progress, 0%), #e0e0e0 var(--progress, 0%));
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 14px;
        font-weight: bold;
        color: #333;
        transition: background 0.1s;
    }
    .progress-ring::after {
        content: attr(data-label);
    }
    .content {
        height: 3000px;
        padding: 40px;
        font-family: Arial, sans-serif;
    }
</style>

<div class="progress-ring" id="ring" data-label="0%"></div>
<div class="content">
    <h1>滚动驱动动画</h1>
    <p style="margin-top: 800px;">继续滚动,看进度环变化</p>
    <p style="margin-top: 800px;">快到顶了!</p>
</div>

<script>
    const ring = document.getElementById('ring');
    
    function updateProgress() {
        // 计算滚动进度
        const scrollTop = window.scrollY;
        const docHeight = document.documentElement.scrollHeight - window.innerHeight;
        const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
        
        // 更新 CSS 变量 —— 驱动动画
        ring.style.setProperty('--progress', progress + '%');
        ring.dataset.label = Math.round(progress) + '%';
        
        // 根据进度改变颜色
        if (progress < 30) {
            ring.style.setProperty('--progress-color', '#007bff');
        } else if (progress < 70) {
            ring.style.setProperty('--progress-color', '#28a745');
        } else {
            ring.style.setProperty('--progress-color', '#dc3545');
        }
    }
    
    // 关键:使用 requestAnimationFrame 优化滚动事件
    let ticking = false;
    window.addEventListener('scroll', function() {
        if (!ticking) {
            window.requestAnimationFrame(function() {
                updateProgress();
                ticking = false;
            });
            ticking = true;
        }
    });
    
    // 初始化
    updateProgress();
</script>
关键优化:使用 requestAnimationFrame

scroll 事件触发频率极高(每帧都可能触发)。如果在事件处理函数中直接进行 DOM 操作,会造成严重的性能问题。使用 requestAnimationFrame 可以将样式更新与屏幕刷新同步,避免不必要的布局抖动。

三、根据用户交互动态修改样式规则

有些场景下,我们需要在用户交互时动态添加或修改全局样式规则,而不仅仅是修改单个元素。

场景:动态注入覆盖样式

比如,用户通过界面调整了页面的“字体大小”偏好,需要全局生效:

// 查找或创建一个专用的样式表
let customSheet = document.getElementById('custom-styles');
if (!customSheet) {
    customSheet = document.createElement('style');
    customSheet.id = 'custom-styles';
    document.head.appendChild(customSheet);
}

// 通过 CSSOM 注入规则
function setGlobalFontSize(size) {
    const sheet = customSheet.sheet;
    const rule = `body { font-size: ${size}px !important; }`;
    
    // 删除旧规则(如果存在)
    if (sheet.cssRules.length > 0) {
        sheet.deleteRule(0);
    }
    // 插入新规则
    sheet.insertRule(rule, 0);
    
    // 保存用户偏好
    localStorage.setItem('fontSize', size);
}

// 用户交互触发
document.querySelector('.font-size-slider').addEventListener('input', function() {
    setGlobalFontSize(this.value);
});

这种方式的好处是:样式规则是全局生效的,所有匹配 body 的元素(包括未来动态添加的元素)都会自动应用,而不需要逐个设置。

场景:动态添加 @keyframes

更进阶的用法——动态创建动画关键帧:

function createBounceAnimation(duration, height) {
    const sheet = document.createElement('style').sheet;
    document.head.appendChild(sheet);
    
    const keyframes = `
        @keyframes dynamicBounce {
            0% { transform: translateY(0); }
            30% { transform: translateY(-${height}px); }
            50% { transform: translateY(0); }
            70% { transform: translateY(-${height * 0.6}px); }
            100% { transform: translateY(0); }
        }
    `;
    sheet.insertRule(keyframes, 0);
    
    // 应用动画
    document.querySelector('.target').style.animation = 
        `dynamicBounce ${duration}ms ease-in-out`;
}

createBounceAnimation(600, 80);

四、性能优化建议

CSSOM 操作涉及 DOM 和样式计算,如果使用不当,会影响页面性能。以下是最重要的几条优化原则。

4.1 避免频繁读写样式

布局抖动(Layout Thrashing) 是性能杀手。当你交替进行“读取布局属性”和“修改样式”时,浏览器被迫反复重新计算布局。

// 坏例子:每次循环都触发重排
const elements = document.querySelectorAll('.item');
for (let i = 0; i < elements.length; i++) {
    const height = elements[i].offsetHeight; // 读取 → 强制重排
    elements[i].style.height = height + 10 + 'px'; // 写入 → 再次重排
}

// 好例子:先批量读取,再批量写入
const elements = document.querySelectorAll('.item');
const heights = [];
for (let i = 0; i < elements.length; i++) {
    heights.push(elements[i].offsetHeight); // 批量读取
}
for (let i = 0; i < elements.length; i++) {
    elements[i].style.height = heights[i] + 10 + 'px'; // 批量写入
}
4.2 使用 requestAnimationFrame 优化动画

对于动画和滚动相关的样式更新,始终使用 requestAnimationFrame:

// 坏例子:setTimeout 无法与屏幕刷新同步
setInterval(() => {
    element.style.transform = `translateX(${x++}px)`;
}, 16);

// 好例子:使用 requestAnimationFrame
function animate() {
    element.style.transform = `translateX(${x++}px)`;
    if (x < 500) {
        requestAnimationFrame(animate);
    }
}
requestAnimationFrame(animate);
4.3 使用 transform 和 opacity 触发合成

transform 和 opacity 的变化不会触发布局(Layout)和绘制(Paint),只会触发合成(Composite),性能开销最小。

// 性能较差:修改 left/top 触发布局
element.style.left = x + 'px';
element.style.top = y + 'px';

// 性能更好:使用 transform 触发合成
element.style.transform = `translate(${x}px, ${y}px)`;
4.4 批量操作使用 DocumentFragment

如果你需要向 DOM 中添加大量元素并设置样式,先在内存中操作,最后一次性插入:

const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.className = 'item';
    div.textContent = `Item ${i}`;
    // 样式在内存中设置,不会触发重排
    div.style.color = i % 2 === 0 ? 'blue' : 'green';
    fragment.appendChild(div);
}
document.body.appendChild(fragment); // 一次性插入,触发一次重排

五、何时使用 className 切换 vs 直接操作样式

这是一个贯穿全系列的重要决策。我们来做一个清晰的总结。

优先使用 className 的场景

场景

原因

预定义的视觉状态(hover、active、选中、禁用等)

样式在 CSS 中定义,易于维护和复用

主题切换(暗色/亮色)

切换根元素的 class,所有子元素自动响应

响应式布局的变体

样式集中管理,便于媒体查询配合

需要多个样式属性同时变化

一次类切换,多个样式变化,性能更好

// 推荐
element.classList.add('active');
element.classList.toggle('expanded');

// 对应的 CSS
.active { background: blue; color: white; }
.expanded { height: auto; opacity: 1; }
优先使用 style 的场景

场景

原因

值依赖 JavaScript 运行时计算

值无法在 CSS 中预定义

动画过程中的连续变化

每一步的值都在变化,无法用类表示

拖拽、跟随鼠标等交互

位置/大小随着用户操作不断变化

需要覆盖内联样式或高优先级样式

style 的优先级高,适合做覆盖

// 推荐:值动态计算
element.style.left = mouseX + 'px';
element.style.top = mouseY + 'px';
element.style.transform = `rotate(${angle}deg)`;
决策速查表

问题

答案 → 使用

这个样式值是否可以提前写在 CSS 中?

是 → className

值是否需要在 JavaScript 中实时计算?

是 → style

是否涉及多个样式属性同时变化?

是 → className

变化频率是否很高(动画、拖拽)?

是 → style

是否需要在未来复用这套样式?

是 → className

是否需要覆盖第三方组件的内联样式?

是 → style

六、浏览器兼容性注意事项

CSSOM API 的浏览器支持情况各不相同。以下是关键 API 的支持状况及处理建议。

API

Chrome

Firefox

Safari

Edge

备注

document.styleSheets

全支持

全支持

全支持

全支持

基础功能,放心使用

CSSStyleSheet.insertRule()

全支持

全支持

全支持

全支持

放心使用

CSSStyleSheet.deleteRule()

全支持

全支持

全支持

全支持

放心使用

getComputedStyle()

全支持

全支持

全支持

全支持

放心使用

scrollIntoView()

全支持

全支持

全支持

全支持

behavior: smooth 需要检查

getBoundingClientRect()

全支持

全支持

全支持

全支持

放心使用

CSS 自定义属性

全支持

全支持

全支持

全支持

IE 不支持,需降级

CSS Typed OM

Chrome 105+

不支持

实验性

Chrome 105+

必须做特性检测

处理建议

1. 基础 CSSOM API 可以放心使用

document.styleSheets、insertRule、deleteRule、getComputedStyle 等核心 API 在主流浏览器中已有十多年的支持历史,可以放心使用。

2. 新特性需要特性检测

对于 CSS Typed OM 等较新的 API,务必使用特性检测:

const supportsTypedOM = 'attributeStyleMap' in Element.prototype;

3. CSS 变量需要降级方案

如果你需要支持 IE 或旧版浏览器,考虑提供降级方案:

/* 降级方案 */
body { background: #ffffff; color: #333333; }

/* 现代浏览器使用 CSS 变量 */
:root { --bg: #ffffff; --text: #333333; }
body { background: var(--bg, #ffffff); color: var(--text, #333333); }

4. 平滑滚动提供 fallback

scrollIntoView({ behavior: 'smooth' }) 在不支持时应该静默降级为即时滚动,这通常是可接受的。

function smoothScrollTo(element) {
    try {
        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
    } catch (e) {
        // 降级为即时滚动
        element.scrollIntoView();
    }
}

七、小结

本章通过六个实战维度,将全系列的知识点整合到了真实开发场景中:

1. 动态切换主题

  • 推荐使用 CSS 自定义属性配合 data-* 属性,JavaScript 只负责切换标识。

  • 方案二利用 CSSOM 修改样式表规则,适合需要动态计算颜色的场景。

2. CSS 动画的 JavaScript 控制

  • 使用 requestAnimationFrame 实现流畅的滚动驱动动画。

  • 通过 CSS 变量驱动动画进度,实现细粒度控制。

3. 用户交互动态修改样式规则

  • 使用 insertRule() / deleteRule() 动态注入或更新全局规则。

  • 支持动态创建 @keyframes 等复杂结构。

4. 性能优化

  • 避免频繁读写样式造成的布局抖动。

  • 使用 requestAnimationFrame 优化动画和滚动事件。

  • 优先使用 transform 和 opacity 触发合成。

  • 使用 DocumentFragment 批量操作 DOM。

5. className 切换 vs 直接操作样式

  • 预定义状态和可复用样式 → className。

  • 动态计算值和连续变化 → style。

  • 提供清晰的决策速查表。

6. 浏览器兼容性

  • 核心 API 支持良好,放心使用。

  • CSS Typed OM 需要特性检测。

  • CSS 变量和 behavior: smooth 需要为旧浏览器提供降级方案。

至此,我们走完了 CSSOM 的完整旅程。从第一章的概览,到最后一章的实战整合,你已掌握了 CSSOM 从理论到实践的完整知识体系。

CSSOM 是前端开发中不可或缺的能力——它让我们从“会写 CSS”进化到“能用 JavaScript 驾驭 CSS”。希望这个系列文章能成为你日常开发中值得信赖的参考。

多唠叨一句:所谓“工欲善其事,必先利其器”,愿大家能将这套 CSSOM 方法论灵活运用于实战,在前端进阶之路上更进一步。期待下个专题(应该会有的),我们不见不散!