【问题标题】:How to get all events on all DOM elements of a page visited by Puppeteer - basically getEventListeners如何获取 Puppeteer 访问的页面的所有 DOM 元素上的所有事件 - 基本上是 getEventListeners
【发布时间】:2021-04-08 21:40:40
【问题描述】:

我正在处理一些 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 协议 (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 });

(src: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 */
];

【问题讨论】:

标签: javascript puppeteer cdp


【解决方案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;
}

它将返回一个对象数组,每个对象(至少)具有nodelisteners属性,可以按如下方式使用:

/** 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")}

【讨论】:

  • 看起来很有前途,会做一些测试看看它的性能如何(这将在数百或数千个 URL 上运行)
  • 谢谢,这很完美,我很惊讶它的速度有多快。需要更多测试,但也许你可以将它公关给 Puppeteer :) CDP 有多快令人惊讶。也将使用复杂页面进行测试
猜你喜欢
  • 2011-08-16
  • 2011-02-03
  • 2020-09-24
  • 2011-05-14
  • 1970-01-01
  • 1970-01-01
  • 2015-10-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多