Как получить все события для всех элементов DOM на странице, которую посещает Puppeteer — в основном getEventListeners
Я работаю над аналитикой веб-сайтов на основе Puppeteer, и мне действительно нужно перечислить все события на странице.
Это легко сделать с «нормальным» JavaScript, поэтому я подумал, что могу просто оценить его в Puppeteer и перейти к другим задачам.
Ну, это не так просто, например, "getEventListeners" не работает. Итак, этот код ниже не работает (но если я возьму оцениваемый код, скопирую его в консоль браузера и запущу - он работает хорошо);
exports.getDomEventsOnElements = function (page) {
return new Promise(async (resolve, reject) => {
try {
let events = await page.evaluate(() => {
let eventsInDom = [];
const elems = document.querySelectorAll('*');
for(i = 0; i < elems.length; i++){
const element = elems[i];
const allEventsPerEl = getEventListeners(element);
if(allEventsPerEl){
const filteredEvents = Object.keys(allEventsPerEl).map(function(k) {
return { event: k, listeners: allEventsPerEl[k] };
})
if(filteredEvents.length > 0){
eventsInDom.push({
el: element,
ev: filteredEvents
})
}
}
}
return eventsInDom;
})
resolve(events);
} catch (e) {
reject(e);
}
})
}
Я исследовал дальше, и похоже, что это не будет работать в Puppeteer, и даже пробовал со старым добрым JQuery.
const events = $._data( element[0], 'events' );
но это тоже не работает.
Затем я наткнулся на Chrome DevTools Protocol (CDP), и там должно быть возможно получить его, заранее определив один элемент;
const cdp = await page.target().createCDPSession();
const INCLUDE_FN = true;
const { result: {objectId} } = await cdp.send('Runtime.evaluate', {
expression: 'foo',
objectGroup: INCLUDE_FN ?
'provided' : // will include fn
'' // wont include fn
});
const listeners = await cdp.send('DOMDebugger.getEventListeners', { objectId });
console.dir(listeners, { depth: null });
(источник: https://github.com/puppeteer/puppeteer/issues/3349)
Но это выглядит слишком сложно, когда я хочу проверить каждый элемент DOM на наличие событий и добавить их в массив. Я подозреваю, что есть лучший способ, чем зацикливание элементов страницы и запуск CDP для каждого из них. Или лучше сказать - надеюсь :)
Любые идеи?
Я просто хотел бы иметь массив всех элементов с (JS) событиями, например:
let allEventsOnThePage : [
{el: "blutton", events : ["click"]},
{el: "input", events : ["click", "blur", "focus"]},
/* you get the picture */
];
1 ответ
Мне было любопытно, поэтому я решил расширить тот пример CDP, который вы нашли, и придумал следующее:
async function describe (session, selector = '*') {
// Unique value to allow easy resource cleanup
const objectGroup = 'dc24d2b3-f5ec-4273-a5c8-1459b5c78ca0';
// Evaluate query selector in the browser
const { result: { objectId } } = await session.send('Runtime.evaluate', {
expression: `document.querySelectorAll("${selector}")`,
objectGroup
});
// Using the returned remote object ID, actually get the list of descriptors
const { result } = await session.send('Runtime.getProperties', { objectId });
// Filter out functions and anything that isn't a node
const descriptors = result
.filter(x => x.value !== undefined)
.filter(x => x.value.objectId !== undefined)
.filter(x => x.value.className !== 'Function');
const elements = [];
for (const descriptor of descriptors) {
const objectId = descriptor.value.objectId;
// Add the event listeners, and description of the node (for attributes)
Object.assign(descriptor, await session.send('DOMDebugger.getEventListeners', { objectId }));
Object.assign(descriptor, await session.send('DOM.describeNode', { objectId }));
elements.push(descriptor);
}
// Clean up after ourselves
await session.send('Runtime.releaseObjectGroup', { objectGroup });
return elements;
}
Он вернет массив объектов, каждый из которых (как минимум)
node
а также
listeners
атрибуты и могут быть использованы следующим образом:
/** Helper function to turn a flat array of key/value pairs into an object */
function parseAttributes (array) {
const result = [];
for (let i = 0; i < array.length; i += 2) {
result.push(array.slice(i, i + 2));
}
return Object.fromEntries(result);
}
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://chromedevtools.github.io/devtools-protocol', { waitUntil: 'networkidle0' });
const session = await page.target().createCDPSession();
const result = await describe(session);
for (const { node: { localName, attributes }, listeners } of result) {
if (listeners.length === 0) { continue; }
const { id, class: _class } = parseAttributes(attributes);
let descriptor = localName;
if (id !== undefined) { descriptor += `#${id}`; }
if (_class !== undefined) { descriptor += `.${_class}`; }
console.log(`${descriptor}:`);
for (const { type, handler: { description } } of listeners) {
console.log(` ${type}: ${description}`);
}
}
await browser.close();
})();
который вернет что-то вроде:
button.aside-close-button:
click: function W(){I.classList.contains("shown")&&(I.classList.remove("shown"),P.focus())}
main:
click: function W(){I.classList.contains("shown")&&(I.classList.remove("shown"),P.focus())}
button.menu-link:
click: e=>{e.stopPropagation(),I.addEventListener("transitionend",()=>{O.focus()},{once:!0}),I.classList.add("shown")}