恭喜你,走到这里了!
在前面的七章中,我们系统地学习了 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>这个方案的核心是:
使用 data-theme 属性作为主题标识。
CSS 变量在不同主题下有不同的值。
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 方法论灵活运用于实战,在前端进阶之路上更进一步。期待下个专题(应该会有的),我们不见不散!