执行反向 CSS 选择器查找本质上是一件棘手的事情。我通常遇到两种类型的解决方案:
沿着 DOM 树向上走,从元素名称、类和id
orname
属性的组合中组合出选择器字符串。这种方法的问题在于它可能导致选择器返回多个元素,如果我们要求它们只选择一个唯一的元素,它不会削减它。
使用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;
}
}