DOM – Work with Document.styleSheets and JS/Scss Breakpoint Media Query
前言
为了方便管理, 我们会定义 CSS Variables, 类似于全局变量. 有时候做特效的时候还需要 JavaScript 配合,
这时就会希望 JavaScript 可以获取到 CSC Variables, 虽然 JS 可以通过 getComputedStyle 单独获取某个 CSS Variable 但是, 若想获取所有的 CSS Variables 就没那么容易了.
它需要通过 Document.styleSheets 的方式去获取, 这篇就是介绍这个的.
参考
stackoverflow – Get all css root variables in array using javascript and change the values
CSS-Tricks – How to Get All Custom Properties on a Page in JavaScript
介绍
Document.styleSheets 可以获取到页面里所有的 Style CSS information. 类似于 C# 的反射, 让你可以遍历所以的 Style.
这篇我们就要通过这个功能, 获取到想要的 CSS Variables. 当然不管你想获取什么 Style 都是可以通过这个方式的, 不限制在 CSS variables, 自己遍历, 自己过滤就可以了.
案子
我这次是想实现一个 JS 的 breakpoint media query. 通过 Scss/CSS variables 来做管理. 过程是 Scss 定义 variables 然后写入到 CSS variables,
JS 通过 Document.styleSheets 遍历出 breakpoint CSS variables. 然后配合 Window.matchMedia 做 media query.
Scss Breakpoint
先看看 Scss 怎样弄, JS 也会实现一摸一样的方式.
file 结构
我一般上用 2 个 scss file 做管理.
第一个是 _core.scss, 里面封装功能
第二个是 _base.scss, 里面写当前项目的逻辑
剩下的就是一个页面一个 .scss
home.scss 调用
@use '../base' as *; @include core-media-breakpoint-only('xl') { :root { --breakpoint-special: 123px; } }
_base.scss re-export 了 _core 所以调用方法时 core-media...
_base.scss 定义
$breakpoint-collection: ( xs: 0, sm: 640px, md: 768px, lg: 1024px, xl: 1280px, '2xl': 1536px, ); @forward './core' as core-* with ( $breakpoint-collection: $breakpoint-collection ); @use './core'; :root { @include core.root-breakpoint($breakpoint-collection); }
CSS media query 不支持 variable, :root 的 variables 只是 for JS 用而已.
_core.scss 核心代码

@use 'sass:list'; @function map-get-next($map, $key) { $keys: map-keys($map); $values: map-values($map); $index: list.index($keys, $key); $count: length($keys); $next-index: $index + 1; @if ($next-index > $count) { @return null; } @return list.nth($values, $next-index); } $breakpoint-collection: null !default; @function breakpoint($size) { @return map-get($breakpoint-collection, $size); } @function breakpoint-next($size) { @return map-get-next($breakpoint-collection, $size); } @mixin media-breakpoint-up($breakpoint) { @media (min-width: breakpoint($breakpoint)) { @content; } } @mixin media-breakpoint-down($breakpoint) { @media (max-width: breakpoint($breakpoint) - 0.02px) { @content; } } @mixin media-breakpoint-only($breakpoint) { $current: breakpoint($breakpoint); $next: breakpoint-next($breakpoint); @if ($next == null) { @media (min-width: $current) { @content; } } @else { @media (min-width: $current) and (max-width: $next - 0.02px) { @content; } } } @mixin media-breakpoint-between($from-breakpoint, $to-breakpoint) { @media (min-width: breakpoint($from-breakpoint)) and (max-width: breakpoint($to-breakpoint) - 0.02px) { @content; } } @mixin root-breakpoint($breakpoint-collection) { @each $breakpoint-key-value in $breakpoint-collection { // note 解忧: + '' 是为了 clear sass warning --breakpoint-#{'' + list.nth($breakpoint-key-value, 1)}: #{list.nth($breakpoint-key-value, 2)}; } }
第二部分是主角, up, down, only, between 这个是效仿 Bootstrap 的做法.
document.styleSheets
StyleSheetList
console.log('document.styleSheets', document.styleSheets);
document.styleSheets 是一个 List 对象.
页面所有的 CSS Style 都会在里面, 不管是 <link> 或者 <style>
CSSStyleSheet
CSSStyleSheet 最重要的属性是 cssRules 和 href
cssRules 就是所有具体的 style 内容, 下面会详细讲.
href = null 代表它是 <style>, href=url 表示是 <link>
通过 window.location 可以判断 CSS 是否是 thrid party
console.log('location', window.location.origin); console.log('document.styleSheets', document.styleSheets[1].href); console.log('same domain', document.styleSheets[1].href!.startsWith(window.location.origin + '/'));
效果
CSSRuleList > CSSRule (CSSStyleRule / CSSMediaRule)
CSSStyleSheet.cssRules 是一个 CSSRuleList
CSSRuleList 里面包含了 CSSStyleRule 和 CSSMediaRule (注意: CSSRule 是 CSSStyleRule 和 CSSMediaRule 的抽象)
每一个 rule 表示一个 selector 还有它的 style
比如下图有 3 个 CSSRule
第一个是 CSSStyleRule, selectorText 是 'body'
第二个是 CSSStyleRule, selectorText 是 'h1'
第三个是 CSSMediaRule, selectorText 是 ':root'
只要是在 media query 内声明的 selector 都属于 CSSMediaRule
CSSStyleRule
最重要的属性是 selectorText 还有 style.
它们长这样
style 的 interface 是 CSSStyleDeclaration, 和 window.getComputedStyle 的返回值相同的 interface.
它是一个 iterable 对象, 通过 for...of 可以获取所有的 keys, 想获取 value 就调用 style.getPropertyValue 方法
console.log('rule.style.keys', [...rule.style]); for (const key of rule.style) { const value = rule.style.getPropertyValue(key); console.log([key, value]); }
效果
value 前面有 space 是正常的, 因为 prettier formatting 为了整齐好看都会添加空格, 取值后最好是 trim() 一下.
CSSMediaRule
每一个 media query 都会产生一个 CSSMediaRule, 哪怕 media query 是一样的
@media (min-width: 1280px) and (max-width: 1535.98px) { :root { --breakpoint-special: 123px; } } @media (min-width: 1280px) and (max-width: 1535.98px) { body { --breakpoint-xx: 123px; } }
效果
CSSMediaRule 最重要的属性是 conditionText 和 cssRules
conditionText 就是 (min-width: 1280px) and (max-width: 1535.98px) 这些 media query
cssRules 就是 CSSRuleList 和上面提过的是一样的.
JavaScript Breakpoint
万事俱备, 有了 document.styleSheets 配上 Window.matchMedia, 我们就可以实现和 Scss 一摸一样的 break point media query 了.
调用
console.log('matches', mediaBreakpointUp('xl').matches); mediaBreakpointBetween('sm', 'lg').addEventListener('change', e => { console.log('matches', e.matches); });
返回的是 MediaQueryList
四大函数

function getBreakpointCollectionFromStyleSheet(): Map<string, string> { const breakpointCollection = new Map<string, string>(); const cssRules = Array.from(document.styleSheets).flatMap(sheet => Array.from(sheet.cssRules)); for (const cssRule of cssRules) { if (cssRule instanceof CSSStyleRule && cssRule.selectorText === ':root') { const keyStartsWith = '--breakpoint-'; for (const key of cssRule.style) { if (!key.startsWith(keyStartsWith)) { continue; } const value = cssRule.style.getPropertyValue(key).trim(); breakpointCollection.set(key.replace('--breakpoint-', ''), value); } } } return breakpointCollection; } function mediaBreakpointUp(breakpoint: string): MediaQueryList { const breakpointCollection = getBreakpointCollectionFromStyleSheet(); const breakpointValue = breakpointCollection.get(breakpoint)!; return window.matchMedia(`(min-width: ${breakpointValue})`); } function mediaBreakpointDown(breakpoint: string): MediaQueryList { const breakpointCollection = getBreakpointCollectionFromStyleSheet(); const breakpointValue = breakpointCollection.get(breakpoint)!; return window.matchMedia(`(max-width: ${parseFloat(breakpointValue) - 0.02}px)`); } function mediaBreakpointOnly(breakpoint: string): MediaQueryList { const breakpointCollection = getBreakpointCollectionFromStyleSheet(); const currentBreakpointValue = breakpointCollection.get(breakpoint)!; const nextBreakpointValue = (() => { const keys = Array.from(breakpointCollection.keys()); const currentIndex = keys.indexOf(breakpoint); const hasNext = currentIndex < keys.length - 1; if (!hasNext) { return null; } else { const nextKey = keys[currentIndex + 1]; return breakpointCollection.get(nextKey)!; } })(); const mediaQuery = nextBreakpointValue === null ? `(min-width: ${currentBreakpointValue})` : `(min-width: ${currentBreakpointValue}) and (max-width: ${ parseFloat(nextBreakpointValue) - 0.02 }px)`; return window.matchMedia(mediaQuery); } function mediaBreakpointBetween(fromBreakpoint: string, toBreakpoint: string): MediaQueryList { const breakpointCollection = getBreakpointCollectionFromStyleSheet(); const fromBreakpointValue = breakpointCollection.get(fromBreakpoint)!; const toBreakpointValue = breakpointCollection.get(toBreakpoint)!; return window.matchMedia( `(min-width: ${fromBreakpointValue}) and (max-width: ${parseFloat(toBreakpointValue) - 0.02}px)` ); }
没什么特别的, 就是 follow Scss 的写法改成 JS 而已
getBreakpointCollectionFromStyleSheet 函数
这个函数负责从 document.styleSheet 获取到 CSS variables
function getBreakpointCollectionFromStyleSheet(): Map<string, string> { const breakpointCollection = new Map<string, string>(); const cssRules = Array.from(document.styleSheets).flatMap(sheet => Array.from(sheet.cssRules)); for (const cssRule of cssRules) { if (cssRule instanceof CSSStyleRule && cssRule.selectorText === ':root') { const keyStartsWith = '--breakpoint-'; for (const key of cssRule.style) { if (!key.startsWith(keyStartsWith)) { continue; } const value = cssRule.style.getPropertyValue(key).trim(); breakpointCollection.set(key.replace('--breakpoint-', ''), value); } } } return breakpointCollection; }
注意, 这里用了许多潜规则, 也有一些隐患
1. 没有过滤 third party CSS (因为我用 Webpack 都会打包一块, 而且有时直接放 CDN 的 origin)
2. 没有考虑 CSSMediaRule, 因为 breakpoint 不可能会在 media query 里面修改
3. ‘--breakpoint-’ 是 Magic string
4. getBreakpointCollectionFromStyleSheet 每次都会遍历, 性能不太好, 应该缓存起来.
5. 没有监听 variable 的改变. 但 breakpoint 不太可能会 change 啦.
总结
如果想在 JS 和 CSS 间管理好 breakpoint 就可以采用以上的方案.
通过 Scss 定义 breakpoint, 然后放入 CSS Variables share 给 JS 用.
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析