为何必须使用Array.map()才能正确获取链接属性?如何移除Array.map()并优化代码?
嘿,咱们来一步步拆解你遇到的这两个问题,先搞懂为啥去掉map()就报错,再给你说不用map()的办法,还有合并代码的小技巧~
一、为什么去掉map()会报错?
这其实是Puppeteer里一个非常容易踩的「上下文隔离」坑:浏览器的运行环境和你的Node.js代码环境是完全分开的!
page.evaluate()的作用是把一段代码发到浏览器里执行,执行完之后,只能把「可以被序列化的普通JavaScript对象」传回Node这边。而document.querySelectorAll('a')返回的是DOM元素节点——这些节点是浏览器专属的复杂对象,带有一堆浏览器内部的属性和方法,根本没法被序列化成Node能识别的数据。
当你直接返回[...document.querySelectorAll('a')]时,Puppeteer只能把这些DOM元素转换成空的“壳子对象”(或者丢失了核心属性的对象)传到Node端。这时候你再去访问a.href、a.innerText,自然就会拿到undefined,调用trim()时就触发了TypeError。
而你用map()的时候,是把每个DOM元素里需要的href和innerText提取出来,组装成了普通的键值对对象——这种对象是标准的可序列化结构,能完整传到Node端,所以就能正常调用trim()啦。
二、能不能不用map()直接操作原数组?
当然可以!咱们换个思路:把遍历处理的逻辑全放到浏览器上下文里执行,根本不用把DOM元素传到Node端。
比如你可以直接在page.evaluate()里遍历链接并输出,不用提取任何属性到Node端:
// 先监听浏览器的console输出,不然Node控制台看不到结果 page.on('console', msg => console.log(msg.text())); await page.evaluate(() => { // 直接操作浏览器里的原DOM元素数组 document.querySelectorAll('a').forEach(a => { console.log(`<a href="${a.href}">${a.innerText.trim()}</a>`); }); });
这里要注意:默认情况下,浏览器里的console.log不会输出到你的Node控制台,所以咱们要先监听page的console事件,把浏览器的输出转发到Node端。这样既不用map()复制属性,也直接操作了原DOM元素数组,完美解决你的需求。
三、关于合并两行代码的问题
你之前报错是因为page.evaluate()返回的是一个Promise,而Promise对象本身没有forEach()方法,所以直接链式调用forEach肯定会报错。要合并代码,有两种可行的思路:
- 用
then()链式处理Promise的结果:
await page.evaluate(() => { return [...document.querySelectorAll('a')].map(({href, innerText}) => ({href, innerText})); }).then(links => { links.forEach(a => console.log(`<a href="${a.href}">${a.innerText.trim()}</a>`)); });
- 或者干脆像上面那样,把遍历逻辑全塞到
page.evaluate()里,核心逻辑就一行(算上监听事件是两行,但不用在Node端处理数组)。
最后总结一下
- 不能直接返回DOM元素数组到Node端:因为浏览器和Node上下文隔离,DOM元素无法序列化传递,要么用
map()提取成普通可序列化对象,要么直接在浏览器上下文里处理元素 - 合并代码报错的核心:
page.evaluate()返回的是Promise,没有forEach()方法,要么用then()链式处理结果,要么把遍历逻辑移入evaluate()内部
备注:内容来源于stack exchange,提问作者Danny Beckett

