查找应用于元素的所有CSS规则

99
许多工具/API提供了选择特定类或ID元素的方法。还可以检查浏览器加载的原始样式表。
但是,为了渲染元素,浏览器会编译所有CSS规则(可能来自不同的样式表文件)并将其应用于元素。这就是您在Firebug或WebKit Inspector中看到的 - 元素的完整CSS继承树。
我如何在纯JavaScript中重新创建此功能,而无需额外的浏览器插件?
也许一个示例可以为我想要的东西提供一些澄清:
<style type="text/css">
    p { color :red; }
    #description { font-size: 20px; }
</style>

<p id="description">Lorem ipsum</p>

这里的 p#description 元素应用了两个 CSS 规则:红色颜色和字体大小为 20 像素。

我想找出这些计算出的 CSS 规则来源的源头(颜色来自 p 规则,等等)。


好的答案也可以在通过JavaScript从HTML节点中查找CSS规则是否可能?中找到。 - Bergi
在浏览器中查看并使用浏览器开发者工具(例如Chrome中的元素选项卡)? - Ronnie Royston
10个回答

91

由于当前这个问题没有一个轻量级(非库),跨浏览器兼容的答案,我尝试提供一个:

function css(el) {
    var sheets = document.styleSheets, ret = [];
    el.matches = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector 
        || el.msMatchesSelector || el.oMatchesSelector;
    for (var i in sheets) {
        var rules = sheets[i].rules || sheets[i].cssRules;
        for (var r in rules) {
            if (el.matches(rules[r].selectorText)) {
                ret.push(rules[r].cssText);
            }
        }
    }
    return ret;
}

JSFiddle: http://jsfiddle.net/HP326/6/

调用 css(document.getElementById('elementId')) 将返回一个数组,其中包含与传递的元素匹配的每个CSS规则的元素。 如果您想获取有关每个规则的更具体信息,请查看CSSRule对象文档。


2
这一行代码定义了 a.matchesa.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector。这意味着,如果已经存在一个 DOM 节点的标准“matches”方法,它将使用该方法,否则它将尝试使用 Webkit 特定的方法(webkitMatchesSelector),然后是 Mozilla、Microsoft 和 Opera 的方法。您可以在以下链接中了解更多信息:https://developer.mozilla.org/en/docs/Web/API/Element/matches - S.B.
6
很遗憾,我认为这个替代方案无法探测到从父元素级联到子元素的所有CSS规则。示例代码:http://jsfiddle.net/t554xo2L/ 在这种情况下,UL规则(应用于元素)不会被匹配到if (a.matches(rules[r].selectorText))的保护条件中。 - funforums
4
我从未声称它会列出/继承/ CSS 规则 - 它只会列出与传递元素匹配的 CSS 规则。如果您还想获取该元素继承的规则,可能需要向上遍历DOM,并在每个父元素上调用“css()”。 - S.B.
3
我知道 :-) 我只是想指出这一点,因为可能会查看这个问题的人会认为它获取“应用于一个元素的所有CSS规则”,正如问题标题所说的那样,但事实并非如此。 - funforums
3
如果您想获取当前应用于该元素的所有规则,包括继承的规则,则应使用getComputedStyle。因此,我认为这个答案是正确的,并且不应包含从父元素继承的样式(例如分配给父元素的文本颜色)。但是,它不包括通过媒体查询有条件地应用的规则。 - tremby
显示剩余8条评论

27

简短版2017年4月12日

挑战者出现。

(Note: 保留了HTML标签)
var getMatchedCSSRules = (el, css = el.ownerDocument.styleSheets) => 
    [].concat(...[...css].map(s => [...s.cssRules||[]])) /* 1 */
    .filter(r => el.matches(r.selectorText));            /* 2 */

代码中的第一行/* 1 */创建了一个扁平数组包含所有规则。

第二行/* 2 */将不匹配的规则丢弃。

该方法基于同一页面上@S.B.的函数css(el)

示例1

var div = iframedoc.querySelector("#myelement");
var rules = getMatchedCSSRules(div, iframedoc.styleSheets);
console.log(rules[0].parentStyleSheet.ownerNode, rules[0].cssText);

示例2

var getMatchedCSSRules = (el, css = el.ownerDocument.styleSheets) => 
    [].concat(...[...css].map(s => [...s.cssRules||[]]))
    .filter(r => el.matches(r.selectorText));

function Go(big,show) {
    var r = getMatchedCSSRules(big);
PrintInfo:
    var f = (dd,rr,ee="\n") => dd + rr.cssText.slice(0,50) + ee;
    show.value += "--------------- Rules: ----------------\n";
    show.value += f("Rule 1:   ", r[0]);
    show.value += f("Rule 2:   ", r[1]);
    show.value += f("Inline:   ", big.style);
    show.value += f("Computed: ", getComputedStyle(big), "(…)\n");
    show.value += "-------- Style element (HTML): --------\n";
    show.value += r[0].parentStyleSheet.ownerNode.outerHTML;
}

Go(...document.querySelectorAll("#big,#show"));
.red {color: red;}
#big {font-size: 20px;}
<h3 id="big" class="red" style="margin: 0">Lorem ipsum</h3>
<textarea id="show" cols="70" rows="10"></textarea>

缺点

  • 没有媒体处理,没有@import@media
  • 无法访问从跨域样式表加载的样式。
  • 不能按选择器“特异性”(重要性顺序)排序。
  • 没有从父级继承的样式。
  • 可能无法使用旧版或基本浏览器。
  • 不确定它如何处理伪类和伪选择器,但似乎还可以。

也许有一天我会解决这些缺点。

长版本2018年8月12日

这是一个来自某人的GitHub页面的更全面的实现(从这个原始代码进行fork,经由Bugzilla)。为Gecko和IE编写,但据传也能与Blink一起工作。

2017年5月4日:特异性计算器存在关键错误,现已修复。(我无法通知作者,因为我没有GitHub帐户。)

2018年8月12日:最近的Chrome更新似乎已将对象范围(this)与分配给独立变量的方法分离。因此,调用matcher(selector)停止工作。通过将其替换为matcher.call(el, selector)来解决。

// polyfill window.getMatchedCSSRules() in FireFox 6+
if (typeof window.getMatchedCSSRules !== 'function') {
    var ELEMENT_RE = /[\w-]+/g,
            ID_RE = /#[\w-]+/g,
            CLASS_RE = /\.[\w-]+/g,
            ATTR_RE = /\[[^\]]+\]/g,
            // :not() pseudo-class does not add to specificity, but its content does as if it was outside it
            PSEUDO_CLASSES_RE = /\:(?!not)[\w-]+(\(.*\))?/g,
            PSEUDO_ELEMENTS_RE = /\:\:?(after|before|first-letter|first-line|selection)/g;
        // convert an array-like object to array
        function toArray(list) {
            return [].slice.call(list);
        }

        // handles extraction of `cssRules` as an `Array` from a stylesheet or something that behaves the same
        function getSheetRules(stylesheet) {
            var sheet_media = stylesheet.media && stylesheet.media.mediaText;
            // if this sheet is disabled skip it
            if ( stylesheet.disabled ) return [];
            // if this sheet's media is specified and doesn't match the viewport then skip it
            if ( sheet_media && sheet_media.length && ! window.matchMedia(sheet_media).matches ) return [];
            // get the style rules of this sheet
            return toArray(stylesheet.cssRules);
        }

        function _find(string, re) {
            var matches = string.match(re);
            return matches ? matches.length : 0;
        }

        // calculates the specificity of a given `selector`
        function calculateScore(selector) {
            var score = [0,0,0],
                parts = selector.split(' '),
                part, match;
            //TODO: clean the ':not' part since the last ELEMENT_RE will pick it up
            while (part = parts.shift(), typeof part == 'string') {
                // find all pseudo-elements
                match = _find(part, PSEUDO_ELEMENTS_RE);
                score[2] += match;
                // and remove them
                match && (part = part.replace(PSEUDO_ELEMENTS_RE, ''));
                // find all pseudo-classes
                match = _find(part, PSEUDO_CLASSES_RE);
                score[1] += match;
                // and remove them
                match && (part = part.replace(PSEUDO_CLASSES_RE, ''));
                // find all attributes
                match = _find(part, ATTR_RE);
                score[1] += match;
                // and remove them
                match && (part = part.replace(ATTR_RE, ''));
                // find all IDs
                match = _find(part, ID_RE);
                score[0] += match;
                // and remove them
                match && (part = part.replace(ID_RE, ''));
                // find all classes
                match = _find(part, CLASS_RE);
                score[1] += match;
                // and remove them
                match && (part = part.replace(CLASS_RE, ''));
                // find all elements
                score[2] += _find(part, ELEMENT_RE);
            }
            return parseInt(score.join(''), 10);
        }

        // returns the heights possible specificity score an element can get from a give rule's selectorText
        function getSpecificityScore(element, selector_text) {
            var selectors = selector_text.split(','),
                selector, score, result = 0;
            while (selector = selectors.shift()) {
                if (matchesSelector(element, selector)) {
                    score = calculateScore(selector);
                    result = score > result ? score : result;
                }
            }
            return result;
        }

        function sortBySpecificity(element, rules) {
            // comparing function that sorts CSSStyleRules according to specificity of their `selectorText`
            function compareSpecificity (a, b) {
                return getSpecificityScore(element, b.selectorText) - getSpecificityScore(element, a.selectorText);
            }

            return rules.sort(compareSpecificity);
        }

        // Find correct matchesSelector impl
        function matchesSelector(el, selector) {
          var matcher = el.matchesSelector || el.mozMatchesSelector || 
              el.webkitMatchesSelector || el.oMatchesSelector || el.msMatchesSelector;
          return matcher.call(el, selector);
        }

        //TODO: not supporting 2nd argument for selecting pseudo elements
        //TODO: not supporting 3rd argument for checking author style sheets only
        window.getMatchedCSSRules = function (element /*, pseudo, author_only*/) {
            var style_sheets, sheet, sheet_media,
                rules, rule,
                result = [];
            // get stylesheets and convert to a regular Array
            style_sheets = toArray(window.document.styleSheets);

            // assuming the browser hands us stylesheets in order of appearance
            // we iterate them from the beginning to follow proper cascade order
            while (sheet = style_sheets.shift()) {
                // get the style rules of this sheet
                rules = getSheetRules(sheet);
                // loop the rules in order of appearance
                while (rule = rules.shift()) {
                    // if this is an @import rule
                    if (rule.styleSheet) {
                        // insert the imported stylesheet's rules at the beginning of this stylesheet's rules
                        rules = getSheetRules(rule.styleSheet).concat(rules);
                        // and skip this rule
                        continue;
                    }
                    // if there's no stylesheet attribute BUT there IS a media attribute it's a media rule
                    else if (rule.media) {
                        // insert the contained rules of this media rule to the beginning of this stylesheet's rules
                        rules = getSheetRules(rule).concat(rules);
                        // and skip it
                        continue
                    }

                    // check if this element matches this rule's selector
                    if (matchesSelector(element, rule.selectorText)) {
                        // push the rule to the results set
                        result.push(rule);
                    }
                }
            }
            // sort according to specificity
            return sortBySpecificity(element, result);
        };
}

已解决的问题

  • = 匹配+= 匹配
  • return re ? re.length : 0;return matches ? matches.length : 0;
  • _matchesSelector(element, selector)matchesSelector(element, selector)
  • matcher(selector)matcher.call(el, selector)

在 getSheetRules 中,我不得不添加 if(stylesheet.cssRules === null) { return [] } 才能让它对我起作用。 - Gwater17
测试了“长版本”。对我来说有效。太糟糕了,getMatchedCSSRules()从未被浏览器标准化。 - colin moock
这个如何处理具有相同特异性的两个选择器,例如 h1 和 h1, div - 最后声明的应该被使用? - Stellan
也许我们可以在这里获取一些处理伪代码的想法?https://github.com/dvtng/jss/blob/master/jss.js - mr1031011

23

编辑:此答案已经过时,在Chrome 64+中不再起作用。仅保留作为历史参考。实际上,该错误报告将链接回此问题,以获取使用替代解决方案。


看起来我在另外一个小时的研究后成功回答了自己的问题。

它就是这么简单:

window.getMatchedCSSRules(document.getElementById("description"))

(在WebKit / Chrome中工作,可能也适用于其他浏览器)


4
如果它仅支持 Chrome 浏览器,那么这没什么用。它只能适用于不到 5% 的访问者(取决于人口统计数据)。 - Tomasi
5
截至2012年6月,Chrome使用率已增至超过32%(略高于IE使用率!)。http://gs.statcounter.com/ - Roy Tinker
6
getMatchedCSSRules方法并不能展示作用于元素的最终样式。它会按顺序返回所有适用的CSSStyleRule对象数组。如果你使用CSS媒体查询进行响应式网页设计或者加载多个样式表(比如一个针对IE浏览器的样式表),你仍需要遍历每个返回的样式,并计算出每个规则的CSS特异性,然后计算出适用的最终规则。你需要重新创建浏览器自然执行的行为。为了在你的示例中证明这一点,将"p {color: blue !important}"添加到你的样式声明的开头。 - mrbinky3000
27
此功能在Chrome 41中被废弃,请参见https://code.google.com/p/chromium/issues/detail?id=437569#c2。 - Daniel Darabos
5
这个问题最终在Chrome 63中被移除了(官方博客文章 - 指向了这个问题)。 - brichins
显示剩余7条评论

22
请查看这个库,它可以实现所需功能:http://www.brothercake.com/site/resources/scripts/cssutilities/ 它适用于所有现代浏览器,包括 IE6,可以像 Firebug 一样提供规则和属性集合(事实上比 Firebug 更准确),还可以计算任何规则的相对或绝对特异度。唯一的注意点是,虽然它能理解静态媒体类型,但不能理解媒体查询。

2
这个模块真的很棒,只希望它能得到更多作者的关注。 - mr1031011
3
这个库有没有维护的版本或者替代品? 目前这个库甚至都无法下载... - Adrian B
我该如何在Node中使用这个库? - Adrian B
1
无法下载此文件。 - Ismael

6

这里是我改写的 getMatchedCSSRules 函数,支持 @media 查询。

const getMatchedCSSRules = (el) => {
  let rules = [...document.styleSheets]
  rules = rules.filter(({ href }) => !href)
  rules = rules.map((sheet) => [...(sheet.cssRules || sheet.rules || [])].map((rule) => {
    if (rule instanceof CSSStyleRule) {
      return [rule]
    } else if (rule instanceof CSSMediaRule && window.matchMedia(rule.conditionText)) {
      return [...rule.cssRules]
    }
    return []
  }))
  rules = rules.reduce((acc, rules) => acc.concat(...rules), [])
  rules = rules.filter((rule) => el.matches(rule.selectorText))
  rules = rules.map(({ style }) => style)
  return rules
}

1
如何以及在哪里应该放置这段代码?我应该以哪种类型或格式将元素发送到函数? - Farhang Amaji
1
@FarhangAmaji 这是一个函数;只需在你想调用它的任何范围内定义它。这甚至可以包括将其粘贴到开发者控制台中,如果这是一个一次性任务的话。显然,它接受一个参数,该参数的类型是HTMLElement。(不清楚作者为什么将其定义为const箭头函数;当然,你可以毫无问题地将其转换为普通函数。) - undefined

4
这是 S.B. 回答的一个版本,它还返回匹配媒体查询中匹配规则。我已经移除了 *.rules || *.cssRules 的合并和 .matches 实现查找器;如果需要,可以添加 polyfill 或将这些行添加回去。
这个版本还返回 CSSStyleRule 对象而不是规则文本。我认为这样更有用,因为规则的具体细节可以更容易地通过编程方式探索。
咖啡:
getMatchedCSSRules = (element) ->
  sheets = document.styleSheets
  matching = []

  loopRules = (rules) ->
    for rule in rules
      if rule instanceof CSSMediaRule
        if window.matchMedia(rule.conditionText).matches
          loopRules rule.cssRules
      else if rule instanceof CSSStyleRule
        if element.matches rule.selectorText
          matching.push rule
    return

  loopRules sheet.cssRules for sheet in sheets

  return matching

JS:

function getMatchedCSSRules(element) {
  var i, len, matching = [], sheets = document.styleSheets;

  function loopRules(rules) {
    var i, len, rule;

    for (i = 0, len = rules.length; i < len; i++) {
      rule = rules[i];
      if (rule instanceof CSSMediaRule) {
        if (window.matchMedia(rule.conditionText).matches) {
          loopRules(rule.cssRules);
        }
      } else if (rule instanceof CSSStyleRule) {
        if (element.matches(rule.selectorText)) {
          matching.push(rule);
        }
      }
    }
  };

  for (i = 0, len = sheets.length; i < len; i++) {
    loopRules(sheets[i].cssRules);
  }

  return matching;
}

这个怎么改才能用在传递的“element”的子元素上呢? - Kragalon
2
你的使用情况是什么?我真的不知道那会有什么用,因为适用于子代的规则并不一定适用于父代。你最终只会得到一堆没有特定共同点的规则。如果你真的想要那样做,你可以对子代进行递归,并为每个子代运行此方法,并建立所有结果的数组。 - tremby
1
这个条件:如果(window.matchMedia(rule.conditionText).matches) {...}在我的情况下没有匹配,因为"rule.conditionText"未定义。没有它,它可以工作。您可以在https://news.ycombinator.com/上尝试并测试此功能。 "span.pagetop b"具有媒体查询规则,与您的函数不匹配。 - ayal gelles
1
Chrome不支持CSSMediaRule实例上的conditionText属性。 - Macil
1
我刚刚测试并同意。这可以解释@ayalgelles所看到的情况。还有其他问题,例如Firefox在尝试获取不同域上的sheet的cssRules时会抛出SecurityError; 在这种情况下,Chrome返回null(并且脚本会因此而中断)。这很容易解决。现在只要我能记得我在哪个项目上使用了它,我就可以看看我如何解决前面的问题...就目前而言,这只是一些示例代码。 - tremby
显示剩余5条评论

2

var GetMatchedCSSRules = (elem, css = document.styleSheets) => Array.from(css)
  .map(s => Array.from(s.cssRules).filter(r => elem.matches(r.selectorText)))
  .reduce((a,b) => a.concat(b));

function Go(paragraph, print) {
  var rules = GetMatchedCSSRules(paragraph);
PrintInfo:
  print.value += "Rule 1: " + rules[0].cssText + "\n";
  print.value += "Rule 2: " + rules[1].cssText + "\n\n";
  print.value += rules[0].parentStyleSheet.ownerNode.outerHTML;
}

Go(document.getElementById("description"), document.getElementById("print"));
p {color: red;}
#description {font-size: 20px;}
<p id="description">Lorem ipsum</p>
<textarea id="print" cols="50" rows="12"></textarea>


4
无意义的重复,是我旧版答案的一个副本。只是在污染页面。完整和最新版本请看这里:链接 - 7vujy0f0hy

1
我认为S.B.的答案现在应该被接受,但并不完全准确。有几次提到可能会漏掉一些规则。面对这种情况,我决定使用document.querySelectorAll而不是element.matches。唯一需要注意的是,您需要某种唯一标识元素来将其与要查找的元素进行比较。在大多数情况下,我认为通过将其id设置为唯一值来实现是可行的。这就是您可以识别匹配元素的方法。如果您能想出一种通用的方式来将document.querySelectorAll的结果与您要查找的元素匹配,那基本上就是getMatchedCSSRules的完整填充。
我检查了document.querySelectorAll的性能,因为它可能比element.matches慢,但在大多数情况下,这应该不是问题。我看到它大约需要0.001毫秒。
我还发现了一个名为CSSUtilities的库,它声称可以做到这一点,但我觉得它已经过时了,并且已经有一段时间没有更新了。看着它的源代码,让我想到可能会有一些遗漏的情况。

CSSUtilities非常古老,但它确实返回伪状态的规则(例如,它可以返回hover规则)。我在这里没有找到任何解决伪状态的答案。 - mr1031011
1
你不需要设置独特的ID,因为HTML节点是通过引用传递的,使用初始节点引用和由querySelectorAll返回的节点之间的简单三重等式检查,应该可以准确地返回你要查找的节点。 - Ismael

1

我编写了一个函数,可以确保在IE9+中计算所请求元素及其子元素的CSS,并在下面的片段中提供了将其保存到新className的可能性。

/**
  * @function getElementStyles
  *
  * Computes all CSS for requested HTMLElement and its child nodes and applies to dummy class
  *
  * @param {HTMLElement} element
  * @param {string} className (optional)
  * @param {string} extras (optional)
  * @return {string} CSS Styles
  */
function getElementStyles(element, className, addOnCSS) {
  if (element.nodeType !== 1) {
    return;
  }
  var styles = '';
  var children = element.getElementsByTagName('*');
  className = className || '.' + element.className.replace(/^| /g, '.');
  addOnCSS = addOnCSS || '';
  styles += className + '{' + (window.getComputedStyle(element, null).cssText + addOnCSS) + '}';
  for (var j = 0; j < children.length; j++) {
    if (children[j].className) {
      var childClassName = '.' + children[j].className.replace(/^| /g, '.');
      styles += ' ' + className + '>' + childClassName +
        '{' + window.getComputedStyle(children[j], null).cssText + '}';
    }
  }
  return styles;
}

使用方法

getElementStyles(document.getElementByClassName('.my-class'), '.dummy-class', 'width:100%;opaity:0.5;transform:scale(1.5);');

2
  1. 你可以通过 el => getComputedStyle(el).cssText 来替换整个 computeStyles 子程序。证明:fiddle
  2. '.' + element.className 是一个错误的构造,因为它假设只存在一个类名。有效的构造是 element.className.replace(/^| /g, '.')
  3. 你的函数忽略了除类之外的其他 CSS 选择器的可能性。
  4. 你的递归仅限于一级(子代但不包括孙代)。
  5. 用法:没有 getElementByClassName,只有 getElementsByClassName(返回一个数组)。
- 7vujy0f0hy

1

由于这个链接的问题被关闭并认定为重复,所以我在这里添加一个答案。

未解决的第二部分:“一旦我找到计算样式,我想知道它来自哪里”

通过循环遍历document.styleSheets,并在您修改之前和之后查看getComputedStyle(),您可以检测正在使用的样式表。 这远非最佳方案,但至少可以检测出您正在查看的规则是否正在使用中。

以下是一个示例:

<html><head>
<title>CSS Test</title>
<style id="style-a">
li {color: #333; font-size: 20px !important;}
li.bb {color: #600; font-size: 10px;}
p {margin: 5px;}
p {margin-bottom: 10px;}
</style>
<script>
window.addEventListener('DOMContentLoaded', async () => {
    const selector = 'li';
    // const selector = 'li.bb';
    const exempleValues = {
        'color': ['rgb(0, 0, 0)', 'rgb(255, 255, 255)'],
        'font-size': ['10px', '12px'],
    };
    const delay = (t) => new Promise((k, e) => {setTimeout(k, t)});

    for(const element of document.querySelectorAll(selector)) {
        const elementCss = document.defaultView.getComputedStyle(element);
        for(const sheet of document.styleSheets) {
            for(const rule of sheet.cssRules) {
                if(rule.selectorText !== selector) {
                    continue;
                }
                for(const properyName of rule.style) {
                    const currentValue = rule.style[properyName];
                    const priority = rule.style.getPropertyPriority(properyName)
                    if(!exempleValues[properyName]) {
                        console.warn('no exemple values for', properyName);
                        continue;
                    }
                    const exempleValue = exempleValues[properyName][exempleValues[properyName][0] === currentValue ? 1 : 0];
                    rule.style.setProperty(properyName, exempleValue, priority);
                    await delay(100);
                    if(exempleValue === elementCss[properyName]) {
                        console.log(selector, properyName, currentValue, priority || false, true, 'in use', element, sheet.ownerNode);
                    } else {
                        console.log(selector, properyName, currentValue, priority || false, false, 'overrided', element);
                    }
                    rule.style.setProperty(properyName, currentValue, priority);
                    await delay(100);
                }
            }
        }
    }
}, {once: true});
</script>
</head><body>
<h1>CSS Test</h1>
<p>html-file for testing css</p>
<ul>
    <li>AAAA</li>
    <li class="bb">BBBB</li>
    <li>CCCC</li>
</ul>
</body></html>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接