获取DOM元素的CSS路径

35
我有一个用于获取css路径的函数:

I got this function to get a cssPath :

var cssPath = function (el) {
  var path = [];

  while (
    (el.nodeName.toLowerCase() != 'html') && 
    (el = el.parentNode) &&
    path.unshift(el.nodeName.toLowerCase() + 
      (el.id ? '#' + el.id : '') + 
      (el.className ? '.' + el.className.replace(/\s+/g, ".") : ''))
  );
  return path.join(" > ");
}
console.log(cssPath(document.getElementsByTagName('a')[123]));

但是我得到的结果是这样的:

html > body > div#div-id > div.site > div.clearfix > ul.choices > li

但要完全正确,它应该是这样的:

html > body > div#div-id > div.site:nth-child(1) > div.clearfix > ul.choices > li:nth-child(5)

有人知道如何用 JavaScript 简单实现吗?


2
如果你想要一个CSS选择器,那么最好使用:eq(1)或者:nth-child(2)而不是[1] - Andy E
或者只需使用JavaScript为元素分配唯一的ID?我可以理解cssPath可能作为FireBug插件等工具很有用,但对于常规代码而言,引入ID是最有效的。 - BGerrissen
事实上,我确实相信有一个FireBug插件可以从名为FireFinder的元素获取cssPath。 - BGerrissen
是的,安迪,你说得对。这个语法看起来像是CSS选择器和XPath的糟糕混合。我应该修复它。 - jney
1
可能是一个重复的问题,参考获取元素的CSS选择器(当它没有id) - msangel
1
simmer.js 看起来是一个很好的库,可以用于这个目的。 - Tom Pohl
8个回答

42

以上答案实际上存在一个错误——当它遇到非元素节点(例如文本节点)时,while循环会提前中断,导致CSS选择器不正确。

这是一个改进版本,修复了该问题,并且:

  • 在遇到第一个具有分配给它的ID的祖先元素时停止
  • 使用nth-of-type()使选择器更易读
    var cssPath = function(el) {
        if (!(el instanceof Element)) 
            return;
        var path = [];
        while (el.nodeType === Node.ELEMENT_NODE) {
            var selector = el.nodeName.toLowerCase();
            if (el.id) {
                selector += '#' + el.id;
                path.unshift(selector);
                break;
            } else {
                var sib = el, nth = 1;
                while (sib = sib.previousElementSibling) {
                    if (sib.nodeName.toLowerCase() == selector)
                       nth++;
                }
                if (nth != 1)
                    selector += ":nth-of-type("+nth+")";
            }
            path.unshift(selector);
            el = el.parentNode;
        }
        return path.join(" > ");
     }

:nth-of-type():nth-child() 的工作方式不同 - 有时候不能简单地用一个替换另一个。 - BoltClock
3
如果(nth!= 1)不好,为了拥有超级具体的路径,即使是1,您也应该始终使用child。 - Sych
@Sych,为什么?它似乎工作正常,并且例如在“html”中添加nth-of-type是行不通的。 - WispyCloud
3
@jtblin,例如,.container span将捕获.container内的所有span,但是.container span:nth-of-type(1)只会捕获第一个,这可能是预期的行为。 - Sych
5
我们可以使用 if (el.previousElementSibling != null || el.nextElementSibling != null) 代替 if (nth != 1),这样它就能够为元素集中的第一个元素添加 nth-of-type(1),但如果它是唯一的元素,则不会添加。 - kremuwa

20
为了始终获取正确的元素,你需要对于无法唯一标识元素的选择器使用 :nth-child():nth-of-type()。因此,请尝试以下代码:

要始终获取正确的元素,您需要在无法唯一标识元素的选择器中使用:nth-child():nth-of-type()。所以请尝试这样做:

var cssPath = function(el) {
    if (!(el instanceof Element)) return;
    var path = [];
    while (el.nodeType === Node.ELEMENT_NODE) {
        var selector = el.nodeName.toLowerCase();
        if (el.id) {
            selector += '#' + el.id;
        } else {
            var sib = el, nth = 1;
            while (sib.nodeType === Node.ELEMENT_NODE && (sib = sib.previousSibling) && nth++);
            selector += ":nth-child("+nth+")";
        }
        path.unshift(selector);
        el = el.parentNode;
    }
    return path.join(" > ");
}

你可以添加一个例程来检查其相应上下文中的唯一元素(如TITLE, BASE, CAPTION等)。


是的,看起来很不错。它也兼容IE吗? - jney
@jney:如果你指的是:nth-child()选择器,那么不是。 - Gumbo

7

另外两个提供的答案有一些关于浏览器兼容性的假设,我遇到了这个问题。下面的代码不会使用nth-child,并且还具备previousElementSibling检查。

function previousElementSibling (element) {
  if (element.previousElementSibling !== 'undefined') {
    return element.previousElementSibling;
  } else {
    // Loop through ignoring anything not an element
    while (element = element.previousSibling) {
      if (element.nodeType === 1) {
        return element;
      }
    }
  }
}
function getPath (element) {
  // False on non-elements
  if (!(element instanceof HTMLElement)) { return false; }
  var path = [];
  while (element.nodeType === Node.ELEMENT_NODE) {
    var selector = element.nodeName;
    if (element.id) { selector += ('#' + element.id); }
    else {
      // Walk backwards until there is no previous sibling
      var sibling = element;
      // Will hold nodeName to join for adjacent selection
      var siblingSelectors = [];
      while (sibling !== null && sibling.nodeType === Node.ELEMENT_NODE) {
        siblingSelectors.unshift(sibling.nodeName);
        sibling = previousElementSibling(sibling);
      }
      // :first-child does not apply to HTML
      if (siblingSelectors[0] !== 'HTML') {
        siblingSelectors[0] = siblingSelectors[0] + ':first-child';
      }
      selector = siblingSelectors.join(' + ');
    }
    path.unshift(selector);
    element = element.parentNode;
  }
  return path.join(' > ');
}

7

反向CSS选择器查找是一件本质上棘手的事情。我通常遇到两种解决方案:

  1. 沿着DOM树向上组装选择器字符串,由元素名称、类和idname属性的组合构成。这种方法的问题在于它可能会导致选择器返回多个元素,如果我们要求它们只选择一个唯一的元素,则无法达到要求。

  2. 使用nth-child()nth-of-type()组装选择器字符串,这可能会导致非常长的选择器。在大多数情况下,选择器越长,具体性就越高,具体性越高,当DOM结构发生变化时,它就越容易被破坏。

以下解决方案是尝试解决这两个问题的混合方法。它是一种混合方法,输出一个唯一的CSS选择器(即,document.querySelectorAll(getUniqueSelector(el))应该始终返回一个单项数组)。虽然返回的选择器字符串不一定是最短的,但它是以CSS选择器效率为重点,同时平衡特异性的选择,通过将nth-of-type()nth-child()优先级放在最后。

您可以通过更新aAttr数组来指定要合并到选择器中的属性。最低浏览器要求为IE 9。

function getUniqueSelector(elSrc) {
  if (!(elSrc instanceof Element)) return;
  var sSel,
    aAttr = ['name', 'value', 'title', 'placeholder', 'data-*'], // Common attributes
    aSel = [],
    // Derive selector from element
    getSelector = function(el) {
      // 1. Check ID first
      // NOTE: ID must be unique amongst all IDs in an HTML5 document.
      // https://www.w3.org/TR/html5/dom.html#the-id-attribute
      if (el.id) {
        aSel.unshift('#' + el.id);
        return true;
      }
      aSel.unshift(sSel = el.nodeName.toLowerCase());
      // 2. Try to select by classes
      if (el.className) {
        aSel[0] = sSel += '.' + el.className.trim().replace(/ +/g, '.');
        if (uniqueQuery()) return true;
      }
      // 3. Try to select by classes + attributes
      for (var i=0; i<aAttr.length; ++i) {
        if (aAttr[i]==='data-*') {
          // Build array of data attributes
          var aDataAttr = [].filter.call(el.attributes, function(attr) {
            return attr.name.indexOf('data-')===0;
          });
          for (var j=0; j<aDataAttr.length; ++j) {
            aSel[0] = sSel += '[' + aDataAttr[j].name + '="' + aDataAttr[j].value + '"]';
            if (uniqueQuery()) return true;
          }
        } else if (el[aAttr[i]]) {
          aSel[0] = sSel += '[' + aAttr[i] + '="' + el[aAttr[i]] + '"]';
          if (uniqueQuery()) return true;
        }
      }
      // 4. Try to select by nth-of-type() as a fallback for generic elements
      var elChild = el,
        sChild,
        n = 1;
      while (elChild = elChild.previousElementSibling) {
        if (elChild.nodeName===el.nodeName) ++n;
      }
      aSel[0] = sSel += ':nth-of-type(' + n + ')';
      if (uniqueQuery()) return true;
      // 5. Try to select by nth-child() as a last resort
      elChild = el;
      n = 1;
      while (elChild = elChild.previousElementSibling) ++n;
      aSel[0] = sSel = sSel.replace(/:nth-of-type\(\d+\)/, n>1 ? ':nth-child(' + n + ')' : ':first-child');
      if (uniqueQuery()) return true;
      return false;
    },
    // Test query to see if it returns one element
    uniqueQuery = function() {
      return document.querySelectorAll(aSel.join('>')||null).length===1;
    };
  // Walk up the DOM tree to compile a unique selector
  while (elSrc.parentNode) {
    if (getSelector(elSrc)) return aSel.join(' > ');
    elSrc = elSrc.parentNode;
  }
}

1
我想说的一点是,虽然id属性应该是唯一的,但并不一定是静态的,因为有些网站使用动态id,在刷新之间会发生变化。 - Tom

3

2

由于无必要的变异,我发现所有的实现都不易阅读。这里提供我的ClojureScript和JS实现:

(defn element? [x]
  (and (not (nil? x))
      (identical? (.-nodeType x) js/Node.ELEMENT_NODE)))

(defn nth-child [el]
  (loop [sib el nth 1]
    (if sib
      (recur (.-previousSibling sib) (inc nth))
      (dec nth))))

(defn element-path
  ([el] (element-path el []))
  ([el path]
  (if (element? el)
    (let [tag (.. el -nodeName (toLowerCase))
          id (and (not (string/blank? (.-id el))) (.-id el))]
      (if id
        (element-path nil (conj path (str "#" id)))
        (element-path
          (.-parentNode el)
          (conj path (str tag ":nth-child(" (nth-child el) ")")))))
    (string/join " > " (reverse path)))))

Javascript:

const isElement = (x) => x && x.nodeType === Node.ELEMENT_NODE;

const nthChild = (el, nth = 1) => {
  if (el) {
    return nthChild(el.previousSibling, nth + 1);
  } else {
    return nth - 1;
  }
};

const elementPath = (el, path = []) => {
  if (isElement(el)) {
    const tag = el.nodeName.toLowerCase(),
          id = (el.id.length != 0 && el.id);
    if (id) {
      return elementPath(
        null, path.concat([`#${id}`]));
    } else {
      return elementPath(
        el.parentNode,
        path.concat([`${tag}:nth-child(${nthChild(el)})`]));
    }
  } else {
    return path.reverse().join(" > ");
  }
};

1
function cssPath (e, anchor) {
    var selector;

    var parent = e.parentNode, child = e;
    var tagSelector = e.nodeName.toLowerCase();

    while (anchor && parent != anchor || !anchor && parent.nodeType === NodeTypes.ELEMENT_NODE) {
        var cssAttributes = ['id', 'name', 'class', 'type', 'alt', 'title', 'value'];
        var childSelector = tagSelector;
        if (!selector || parent.querySelectorAll (selector).length > 1) {
            for (var i = 0; i < cssAttributes.length; i++) {
                var attr = cssAttributes[i];
                var value = child.getAttribute(attr);
                if (value) {
                    if (attr === 'id') {
                        childSelector = '#' + value;
                    } else if (attr === 'class') {
                        childSelector = childSelector + '.' + value.replace(/\s/g, ".").replace(/\.\./g, ".");
                    } else { 
                        childSelector = childSelector + '[' + attr + '="' + value + '"]';
                    }
                }
            }

            var putativeSelector = selector? childSelector + ' ' + selector: childSelector;             

            if (parent.querySelectorAll (putativeSelector).length > 1) {
                var siblings = parent.querySelectorAll (':scope > ' + tagSelector);
                for (var index = 0; index < siblings.length; index++)
                    if (siblings [index] === child) {
                        childSelector = childSelector + ':nth-of-type(' + (index + 1) + ')';
                        putativeSelector = selector? childSelector + ' ' + selector: childSelector;             
                        break;
                    }
            }

            selector = putativeSelector;
        }
        child = parent;
        parent = parent.parentNode;
    }

    return selector;
};      

0
迟到总比不到好:我来到这个问题并尝试使用所选答案,但在我的情况下,它没有起作用,因为它对我的情况不是非常具体。所以我决定编写自己的解决方案 - 希望它能帮助一些人。
这个解决方案是这样的:tag.class#id[name][type]:nth-child(?),并且使用>进行定位。
function path(e) {
    let a = [];
    while (e.parentNode) {
        let d = [
            e.tagName.toLowerCase(),
            e.hasAttribute("class") ? e.getAttribute("class") : "",
            e.hasAttribute("id") ? e.getAttribute("id") : "",
            e.hasAttribute("name") ? e.getAttribute("name") : "",
            e.hasAttribute("type") ? e.getAttribute("type") : "",       
            0                                                       // nth-child
        ];

        // Trim
        for (let i = 0; i < d.length; i++) d[i] = typeof d[i] == "string" ? d[i].trim() : d[i];

        if (d[1] != "") d[1] = "."+d[1].split(" ").join(".");
        if (d[2] != "") d[2] = "#"+d[2];
        if (d[3] != "") d[3] = '[name="'+d[3]+'"]';
        if (d[4] != "") d[4] = '[type="'+d[4]+'"]';
        // Get child index...
        let s = e;
        while (s) {
            d[5]++;
            s = s.previousElementSibling;
        }
        d[5] = d[5] != "" ? ":nth-child("+d[5]+")" : ":only-child";
        // Build the String
        s = "";
        for (let i = 0; i < d.length; i++) s += d[i];
        a.unshift(s);

        // Go to Parent
        e = e.parentNode;
    }
    return a.join(">");
}

我知道它不是很易读(我在我的代码中使用它时很凌乱),但它会给你确切的元素,你可以试试。


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