【问题标题】:How to pass a function in Puppeteers .evaluate() method?如何在 Puppeteers .evaluate() 方法中传递函数?
【发布时间】:2021-07-25 09:53:59
【问题描述】:

每当我尝试传递一个函数时,像这样:

var myFunc = function() { console.log("lol"); };

await page.evaluate(func => {
 func();
 return true;
}, myFunc);

我明白了:

(node:13108) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Evaluation failed: TypeError: func is not a function
at func (<anonymous>:9:9)
(node:13108) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

为什么?如何正确操作?

谢谢!

€:让我澄清一下:我这样做是因为我想先找到一些 DOM 元素并在该函数中使用它们,更像这样(简化):

var myFunc = function(element) { element.innerHTML = "baz" };

await page.evaluate(func => {
  var foo = document.querySelector('.bar');
  func(foo);
  return true;
}, myFunc);

【问题讨论】:

标签: javascript node.js google-chrome puppeteer


【解决方案1】:

您不能将函数直接传递给page.evaluate(),但您可以调用另一个特殊方法(page.exposeFunction),它将您的函数公开为全局函数(也可作为页面window 对象的属性使用) ,所以当你在page.evaluate()里面时可以调用它:

var myFunc = function() { console.log("lol"); };
await page.exposeFunction("myFunc", myFunc);

await page.evaluate(async () => {
   await myFunc();
   return true;
});

请记住page.exposeFunction() 将使您的函数返回一个Promise,然后,您需要使用asyncawait。发生这种情况是因为您的函数不是running inside your browser,而是在您的nodejs 应用程序中。

  1. exposeFunction() does not work after goto()
  2. Why can't I access 'window' in an exposeFunction() function with Puppeteer?
  3. How to use evaluateOnNewDocument and exposeFunction?
  4. exposeFunction remains in memory?
  5. Puppeteer: pass variable in .evaluate()
  6. Puppeteer evaluate function
  7. allow to pass a parameterized funciton as a string to page.evaluate
  8. Functions bound with page.exposeFunction() produce unhandled promise rejections
  9. exposed function queryseldtcor not working in puppeteer
  10. How can I dynamically inject functions to evaluate using Puppeteer?

【讨论】:

    【解决方案2】:

    木偶师issue讨论了类似的问题。

    有几种方法可以解决您的问题。第一条规则是保持简单。

    评估函数

    这是最快的处理方式,你只需传递函数并执行它。

    await page.evaluate(() => {
      var myFunc = function(element) { element.innerHTML = "baz" };
      var foo = document.querySelector('.bar');
      myFunc(foo);
      return true;
    });
    

    预先暴露函数

    您可以使用 page.evaluate 或 page.addScriptTag 预先公开该函数

    // add it manually and expose to window
    await page.evaluate(() => {
      window.myFunc = function(element) { element.innerHTML = "baz" };
    });
    
    // add some scripts
    await page.addScriptTag({path: "myFunc.js"});
    
    // Now I can evaluate as many times as I want
    await page.evaluate(() => {
      var foo = document.querySelector('.bar');
      myFunc(foo);
      return true;
    });
    

    使用元素句柄

    page.$(选择器)

    您可以将元素句柄传递给 .evaluate 并根据需要进行更改。

    const bodyHandle = await page.$('body');
    const html = await page.evaluate(body => body.innerHTML, bodyHandle);
    

    page.$eval

    您可以定位一个元素并根据需要进行更改。

    const html = await page.$eval('.awesomeSelector', e => {
    e.outerHTML = "whatever"
    });
    

    诀窍是read the docs 并保持简单。

    【讨论】:

    • 第一个解决方案有什么意义,你可以传递函数并执行它。?如果我们想在多个evaluate 调用中使用相同的myFunc 怎么办?
    • 就像下面的答案所说,你不能像使用 page.evaluate 的变量那样将函数传递到页面中。例如:await page.evaluate(async func =&gt;{ /* ... */ }, myFunc)
    【解决方案3】:

    带参数的传递函数

    // 手动添加并暴露到窗口

     await page.evaluate(() => {
          window.myFunc = function(element) { element.innerHTML = "baz" };
        });
    

    //然后调用上面声明的函数

     await page.evaluate((param) => {
             myFunc (param);
        }, param);
    

    【讨论】:

    • 我不应该在page.evaluate(...) 中调用window.myFunc 而不是myFuncmyFunc 分配给window.myFunc 后如何进入全局命名空间?
    • 你的答案与投票最多的答案有何不同?
    【解决方案4】:

    抛出错误是因为你执行了func();func 不是一个函数。我更新我的答案以回答您更新的问题:

    选项 1:在页面上下文中执行您的函数:

    var myFunc = function(element) { element.innerHTML = "baz" };
    await page.evaluate(func => {
      var foo = document.querySelector('.bar');
      myFunc(foo);
      return true;
    });
    

    选项 2:将元素句柄作为参数传递

    const myFunc = (element) => { 
        innerHTML = "baz";
        return true;
    }
    const barHandle = await page.$('.bar');
    const result = await page.evaluate(myFunc, barHandle);
    await barHandle.dispose();
    

    `

    【讨论】:

    • 不是真的,我想在 .evaluate 函数中评估 myFunc。这样我就可以在浏览器上下文中做一些事情,并使用它在浏览器上下文中调用 myFunc:
    • 之前能够做其他事情并将其传递给函数:await page.evaluate(func => { /* 在这里做事情 / func( / 使用这些事情在这里 */ ); return true; }, myFunc);
    • 所以你可以这样做:` var myFunc = function(element) { element.innerHTML = "baz" };等待 page.evaluate(() => { var foo = document.querySelector('.bar'); myFunc(foo); return true; }); `
    • 这是错误的。您不能将测试中的全局范围函数传递到 page.evaluate 中。
    【解决方案5】:
    //  External function to run inside evaluate context
    function getData() {
            return document.querySelector('title').textContent;
        }
    
    function mainFunction(url, extractFunction){
        let browser = await puppeteer.launch({});
        let page = await browser.newPage();
    
        await page.goto(url);
    
        let externalFunction = Object.assign(extractFunction);
    
        let res = await this.page.evaluate(externalFunction)
    
        console.log(res);
    }
        
    
    // call it here
    mainFunction('www.google.com',getData);
    

    【讨论】:

      【解决方案6】:

      创建了一个包裹page.evaluate的辅助函数:

      const evaluate = (page, ...params) => browserFn => {
          const fnIndexes = [];
          params = params.map((param, i) => {
              if (typeof param === "function") {
                  fnIndexes.push(i);
                  return param.toString();
              }
              return param;
          });
          return page.evaluate(
              (fnIndexes, browserFnStr, ...params) => {
                  for (let i = 0; i < fnIndexes.length; i++) {
                      params[fnIndexes[i]] = new Function(
                          " return (" + params[fnIndexes[i]] + ").apply(null, arguments)"
                      );
                  }
                  browserFn = new Function(
                      " return (" + browserFnStr + ").apply(null, arguments)"
                  );
                  return browserFn(...params);
              },
              fnIndexes,
              browserFn.toString(),
              ...params
          );
      };
      
      export default evaluate;
      

      获取所有参数并将函数转换为字符串。
      然后在浏览器上下文中重新创建函数。
      https://github.com/puppeteer/puppeteer/issues/1474

      你可以像这样使用这个函数:

      const featuredItems = await evaluate(page, _getTile, selector)((get, s) => {
          const items = Array.from(document.querySelectorAll(s));
          return items.map(node => get(node));
      });
      

      【讨论】:

      • 你在这里用这些代码做什么?怎么用?
      • 这让我可以为浏览器上下文创建模块化函数,将它们导入我的脚本并将它们传递给评估。 IE。 _getTile 可以访问文档,从它自己的 Javascript 文件中导入,然后传递给评估方法。通过对函数进行字符串化然后在浏览器上下文中重新创建,您可以将函数传递给 Puppeteer .evaluate() 方法,如问题中所述。
      【解决方案7】:
      function stringifyWithFunc(obj) {
        const str = JSON.stringify(obj, function(key, val) {
            if (typeof val === "function") {
              return val + "";
              return val;
            });
          return str;
        }
      
        function parseWithFunction(str) {
          const obj = JSON.parse(str, function(key, val) {
            if (typeof val === 'string' && val.includes("function")) {
              return eval(`(${val})`);
            }
            return val;
          });
          return obj;
        }
      
        function testFunc() {
          console.log(123);
        };
      
        const params = {
          testFunc,
          a: 1,
          b: null
        }
      
        await page.exposeFunction("parseWithFunction", parseWithFunction);
      
        await pageFrame.$eval(".category-content", (elem, objStr) => {
            const params = parseWithFunction(objStr);
            params.testFunc()
          },
          stringifyWithFunc(params)
        );
      

      【讨论】:

        猜你喜欢
        • 2020-09-26
        • 1970-01-01
        • 1970-01-01
        • 2010-12-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多