前言
为了方便管理, 我们会定义 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- $next - 0.02px) { @content; } } } @mixin media-breakpoint-between($from-breakpoint, $to-breakpoint) { @media (min-width: breakpoint($from-breakpoint)) and (max- 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- 1280px) and (max- 1535.98px) { :root { --breakpoint-special: 123px; } } @media (min- 1280px) and (max- 1535.98px) { body { --breakpoint-xx: 123px; } }
效果
CSSMediaRule 最重要的属性是 conditionText 和 cssRules
conditionText 就是 (min- 1280px) and (max- 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- ${breakpointValue})`); } function mediaBreakpointDown(breakpoint: string): MediaQueryList { const breakpointCollection = getBreakpointCollectionFromStyleSheet(); const breakpointValue = breakpointCollection.get(breakpoint)!; return window.matchMedia(`(max- ${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- ${currentBreakpointValue})` : `(min- ${currentBreakpointValue}) and (max- ${ 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- ${fromBreakpointValue}) and (max- ${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 用.