服务端5月29日 00:52
Cheerio 性能怎么优化?大文件和高并发场景怎么处理?Cheerio 性能优化抓住三个方向:选择器、内存、并发。选择器方面:用 `.find()` 配合具体 class 替代深层后代选择器,缓存 `$container` 后链式调用避免重复查询。内存方面:大文件用 stream 分块解析代替一次 load,批量 DOM 操作先拼字符串再一次性 `.html()` 插入,用完的 `$` 引用及时置空触发 GC。并发方面:多 URL 用 `Promise.all` 并行请求 + 逐个解析,超大数据集用 Worker 线程分片处理。load 选项中 `decodeEntities: false` 和 `withDomLvl1: false` 也能减少不必要的解析开销。
## 追问
**为什么 .find() 比层级选择器快?**
`$('.container .item .title')` 每次都从根节点全量匹配三层;`$('.container').find('.item').find('.title')` 先锁定容器再在子集中查找,搜索范围逐层缩小。差距在元素数量大时(万级以上)才明显。
**大文件怎么避免内存溢出?**
不要 `cheerio.load(wholeFile)`,改用 stream 按 `</item>` 等边界标签分割,每块单独 load 解析后立即释放。内存占用从 O(n) 降到 O(chunk),10MB 文件也不会爆。
**批量插入 DOM 为什么不能逐个 append?**
每次 `.append()` 都触发内部 DOM 树重建,1000 次就是 1000 次重建。正确做法是先用数组拼 HTML 字符串,最后 `.html(str)` 一次性写入,从 O(n) 次操作降到 1 次。
**load 选项哪些影响性能?**
`decodeEntities: false` 跳过 HTML 实体解码(不需要中文转义时关闭);`withDomLvl1: false` 跳过 DOM Level 1 兼容处理;`normalizeWhitespace: false` 跳过空白合并。三个都关掉可提速 15-20%。
**多 URL 并发爬取怎么做?**
用 p-limit 或手动分批 `Promise.all`:`for (let i = 0; i < urls.length; i += 5)` 每批 5 个并发,避免同时发起数百请求被限流或打挂目标服务器。
## 写段代码
```javascript
// 流式解析大文件,内存恒定
const results = [];
let buf = '';
fs.createReadStream('big.html')
.on('data', chunk => {
buf += chunk;
const matches = buf.match(/<item[\s\S]*?<\/item>/g) || [];
matches.forEach(m => {
const $ = cheerio.load(m);
results.push($('name').text());
});
buf = buf.slice(buf.lastIndexOf('</item>') + 7);
});
```标签
NodeJS
Node 是一个 Javascript 运行环境(runtime)。实际上它是对 Google V8 引擎(应用于 Google Chrome 浏览器)进行了封装。V8 引擎执行 Javascript 的速度非常快,性能非常好。Node 对一些特殊用例进行了优化,提供了替代的 API,使得 V8 在非浏览器环境下运行得更好。例如,在服务器环境中,处理二进制数据通常是必不可少的,但 Javascript 对此支持不足,因此,V8.Node 增加了 Buffer 类,方便并且高效地 处理二进制数据。因此,Node 不仅仅简单的使用了 V8,还对其进行了优化,使其在各环境下更加给力。

服务端5月29日 00:51
如何开发 Cheerio 插件?有哪些实用插件模式?Cheerio 插件的核心模式是扩展 `cheerio.prototype`(即 `$.fn`),给选择器结果集添加自定义方法。基本写法:`module.exports = function(cheerio) { cheerio.prototype.myMethod = function() { return this; } }`,然后 `cheerio.use(pluginFn)` 加载。插件方法内部通过 `this` 访问当前选中的元素集合,用 `cheerio(el)` 包装后即可调用所有原生方法。返回 `this` 支持链式调用,返回 `.get()` 则输出普通数组。
## 追问
**插件方法怎么访问当前选中的元素?**
方法内 `this` 就是 cheerio 对象,`this.each((i, el) => ...)` 遍历,`cheerio(el)` 包装单个元素。`this.length` 获取匹配数量,`this[i]` 直接取原生节点。
**怎么让插件支持配置参数?**
导出一个工厂函数而非直接函数:`module.exports = (options) => (cheerio) => { ... }`,调用时 `cheerio.use(myPlugin({ trim: true }))`。内部用默认参数合并:`const opts = { trim: true, ...options }`。
**有哪些实用的插件场景?**
最常见三类:①文本清洗——批量 removeTags/cleanText,去掉 script/style/注释;②结构化提取——tableToArray 把表格转二维数组,tableToObjects 按表头生成对象数组;③URL 标准化——resolveUrls 把相对路径转绝对路径,处理懒加载 data-src。
**插件方法和独立工具函数怎么选?**
需要操作 DOM 节点、支持链式调用、依赖当前选择器上下文时用插件;纯数据转换、不涉及 DOM 操作时用普通函数。插件的优势是和 cheerio API 风格统一,缺点是污染原型。
**开发插件要注意什么?**
方法名加业务前缀避免冲突(如 `seo_extractLinks` 而非 `extractLinks`);返回 `this` 保持链式;用 try-catch 包裹核心逻辑防止异常中断;在 peerDependencies 声明 cheerio 版本。
## 写段代码
```javascript
// 表格转对象数组插件
module.exports = function(cheerio) {
cheerio.prototype.tableToObjects = function() {
const headers = this.find('th').map((i, el) => cheerio(el).text().trim()).get();
return this.find('tbody tr').map((i, tr) => {
const obj = {};
cheerio(tr).find('td').each((j, td) => {
obj[headers[j]] = cheerio(td).text().trim();
});
return obj;
}).get();
};
};
```服务端5月29日 00:51
Cheerio 爬取动态页面为什么拿不到数据?怎么解决?Cheerio 只是 HTML 解析器,不执行 JavaScript,所以通过 JS 动态渲染的内容(SPA、AJAX 加载、无限滚动)用 Cheerio 直接解析会拿到空壳 HTML。解决方案有三条路径:1) 用 Puppeteer/Playwright 先渲染页面拿到完整 HTML 再交给 Cheerio 解析;2) 拦截网络请求直接找到数据 API 端点,用 axios 请求 JSON;3) 分析页面源码中的内联数据(如 window.__INITIAL_STATE__)直接提取。
## 追问
**方案 2(直接请求 API)相比方案 1 有什么优势?**
速度快、资源消耗低、无需启动浏览器。缺点是 API 可能有签名/鉴权,需要逆向分析请求参数。
**Puppeteer 和 Playwright 选哪个?**
Playwright API 更现代,原生支持多浏览器,自动等待机制更智能;Puppeteer 生态更成熟、Chrome 专项优化更好。新项目推荐 Playwright。
**如何优化 Puppeteer + Cheerio 方案的性能?**
拦截非必要资源(图片/字体/CSS)不加载,复用浏览器实例而非每次新建,设置合理的 waitForSelector 超时而非固定等待。
**怎么判断页面是动态渲染还是静态 HTML?**
curl 获取 HTML 后检查目标元素是否存在,如果 curl 拿不到但浏览器能看到,就是动态渲染。
## 写段代码
```javascript
const puppeteer = require('puppeteer');
const cheerio = require('cheerio');
async function scrape(url) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const $ = cheerio.load(await page.content());
await browser.close();
return $('.item').map((i, el) => $(el).text()).get();
}
```服务端5月29日 00:51
Cheerio 有哪些 DOM 操作方法?修改和读取分别怎么用?Cheerio 的 DOM 操作分两大类:读取和修改。读取用 `.text()` `.html()` `.attr()` `.val()` 获取内容;修改用 `.append()` `.prepend()` `.after()` `.before()` 插入节点,`.remove()` `.empty()` 删除,`.replaceWith()` 替换。关键是区分内部插入(append/prepend,插入子节点)和外部插入(after/before,插入兄弟节点)。遍历用 `.find()` `.children()` `.parent()` `.closest()` `.siblings()` 在节点树中移动。
## 追问
**append 和 after 有什么区别?**
`.append()` 是内部插入,把新节点加为目标元素的最后一个子节点;`.after()` 是外部插入,把新节点放在目标元素后面作为兄弟节点。记忆方式:append 是往肚子里塞,after 是站在旁边。
**remove 和 empty 有什么区别?**
`.remove()` 连元素本身一起删除;`.empty()` 只清空内容,元素标签保留。爬虫场景中 `.empty()` 常用于清空某个容器再重新填入清洗后的数据。
**怎么批量提取数据并转成数组?**
用 `.map()` + `.get()` 组合:`$('.item').map((i, el) => $(el).text()).get()`。`.map()` 返回 cheerio 对象,`.get()` 转为普通数组,否则无法用 Array 方法。
**.find() 和 .children() 有什么区别?**
`.find()` 搜索所有后代元素(含孙子节点),`.children()` 只找直接子元素。性能上 `.children()` 更快,层级确定时优先使用。
**怎么克隆一个节点并插入到其他位置?**
`.clone()` 深拷贝节点,返回独立的 cheerio 对象,可链式调用 `.appendTo()` 或 `.insertAfter()` 插入。注意 clone 不会复制 data 和事件绑定。
## 写段代码
```javascript
const $ = cheerio.load(html);
// 读取:提取所有文章标题和链接
const articles = $('.post').map((i, el) => ({
title: $(el).find('h2').text(),
link: $(el).find('a').attr('href')
})).get();
// 修改:给外链加 target 并移除广告
$('a[href^="http"]').attr('target', '_blank');
$('.ad').remove();
```服务端5月29日 00:50
Cheerio 中如何高效使用选择器提取数据?Cheerio 支持 CSS 选择器 + jQuery 扩展伪类,核心选择器分四类:基本(元素/ID/类)、层级(后代/子元素/兄弟)、属性(= ^= $= *=)、伪类(:first/:eq(n)/:contains()/:has())。高效用法的关键是缩小搜索范围:先选父容器再 .find(),缓存 $ 变量避免重复查找,用具体选择器替代通配符。
## 追问
**.find() 和层级选择器 $('div p') 哪个更快?**
性能差异极小,但 .find() 可读性更好且支持链式调用,推荐优先使用。已缓存的父元素上 .find() 还能跳过全局搜索。
**如何获取元素的文本、属性和 HTML?**
.text() 取文本(含子元素文本拼接),.attr('href') 取属性值,.html() 取内部 HTML,.val() 取表单值。注意 .text() 会递归合并所有子节点文本。
**.each() 和 .map() 有什么区别?**
.each() 遍历不返回值(适合副作用操作),.map() 返回数组(适合数据提取)。.map() 需链式调用 .get() 才能得到原生数组。
**:contains() 和 .filter() 如何选择?**
:contains('text') 在选择器阶段过滤,写法简洁但不支持正则;.filter(fn) 在回调中可写任意判断逻辑,更灵活。
## 写段代码
```javascript
const $ = cheerio.load(html);
const $list = $('#article-list');
const titles = $list.find('.title').map((i, el) =>
$(el).text().trim()
).get();
```服务端5月29日 00:50
Cheerio 使用中有哪些常见坑?怎么解决?最常见的五大坑:①中文乱码——需要用 iconv-lite 按实际编码解码,别依赖 axios 自动处理;②选择器找不到元素——先 `$.html()` 检查 HTML 是否完整加载,再逐步缩小选择器范围调试;③拿不到动态内容——Cheerio 不执行 JS,要么换 Puppeteer 要么直接调后端 API;④大文件内存溢出——分批处理或流式解析,别一次全 load 进来;⑤XML 解析报错——必须加 `{ xmlMode: true }` 选项,否则自闭合标签和命名空间都会出问题。
## 追问
**中文乱码具体怎么处理?**
axios 设置 `responseType: 'arraybuffer'` 拿到原始字节,先从 Content-Type 或 meta 标签检测编码,再用 `iconv.decode(buf, charset)` 转码。GBK 页面不转码必乱。
**选择器没匹配到元素,排查步骤是什么?**
第一步 `console.log($.html())` 看 HTML 是否完整;第二步从最外层选择器开始逐步缩小;第三步用 `:contains()` 按文本内容定位;第四步确认元素不在 iframe 或 shadow DOM 中。
**处理大文件怎么避免内存泄漏?**
分批 `.slice(i, i+batchSize)` 处理,每批处理完置空引用;更优方案是用 stream 按 `</item>` 分割,逐块 `cheerio.load` 解析,内存恒定。
**XML 模式和 HTML 模式有什么区别?**
`xmlMode: true` 会保留大小写(HTML 模式全部转小写)、保留自闭合标签、不补全缺失的闭合标签。解析 RSS/SVG/配置文件必须开启,否则数据丢失。
**提取的文本满是空白字符怎么办?**
`.text()` 结果链式调用 `.replace(/\s+/g, ' ').trim()`,或在 load 时加 `{ normalizeWhitespace: true }`,但后者会把换行也合并成空格,按需选择。
## 写段代码
```javascript
// 正确处理 GBK 编码页面
const resp = await axios.get(url, { responseType: 'arraybuffer' });
const charset = resp.headers['content-type']?.match(/charset=([^;]+)/i)?.[1] || 'utf-8';
const html = charset.toLowerCase() === 'utf-8'
? resp.data.toString()
: iconv.decode(Buffer.from(resp.data), charset);
const $ = cheerio.load(html);
```服务端5月29日 00:50
Cheerio 加载 HTML 有哪几种方式?核心方法是 cheerio.load(html, options),支持三种输入源:HTML 字符串、fs.readFileSync 读取的文件内容、Buffer。第二个参数控制解析行为:xmlMode 用于 XML 文档,decodeEntities 控制是否解码 HTML 实体(如 & → &),withDomLvl1 决定是否遵循 DOM Level1 规范。对大文件可用流式读取拼接后再 load。
## 追问
**xmlMode 什么时候必须开启?**
解析 XML/RSS/Atom feed 时必须开启,否则标签大小写会被统一转小写,XML 的自闭合标签也会被错误处理。
**decodeEntities 设为 false 有什么用?**
保留原始 HTML 实体不解码,适合需要原样输出 HTML 的场景,如内容编辑器。默认 true 会把 & 转成 &。
**能直接加载 URL 吗?**
不能。Cheerio 是纯解析库,没有网络能力。需先用 axios/fetch 获取 HTML 字符串,再传入 load()。
**cheerio.load() 返回的 $ 是什么?**
返回一个函数,既可当选择器 $(selector) 使用,也挂载了 .html()、.xml() 等静态方法,本质是包装后的 DOM 操作入口。
**Buffer 输入和字符串输入有区别吗?**
功能一致,Buffer 会被内部自动转字符串。用 Buffer 可避免编码问题,尤其处理非 UTF-8 内容时更安全。
## 写段代码
```javascript
const cheerio = require('cheerio');
const $ = cheerio.load('<root><item>值</item></root>', {
xmlMode: true,
decodeEntities: false
});
console.log($('item').text()); // 值
```服务端5月29日 00:50
如何用 Cheerio 实现网页爬虫和数据抓取?核心流程三步走:用 axios/fetch 获取 HTML → cheerio.load() 加载 → 选择器提取数据。Cheerio 只负责解析,HTTP 请求需搭配其他库。关键要注意设置 User-Agent、处理相对 URL 转绝对路径、以及加入请求延迟避免被封禁。
## 追问
**如何处理分页爬取?**
循环拼接页码参数逐页请求,每页之间加 1-2 秒延迟,收集数据后统一存储。注意检测是否有下一页(如"下一页"按钮或总页数标识)来决定终止条件。
**爬取的数据如何清洗?**
用 .trim() 去空白,正则移除 HTML 标签和特殊字符,parseFloat/parseInt 转数字类型,过滤空值和无效条目。
**如何实现请求重试机制?**
封装 fetchWithRetry 函数,失败后指数退避等待重试(1s→2s→4s),超过最大次数抛出异常。
**并发爬取如何控制?**
用 p-limit 等并发控制库限制同时请求数,或手动实现队列,避免瞬间大量请求导致 IP 被封。
## 写段代码
```javascript
const axios = require('axios');
const cheerio = require('cheerio');
async function scrape(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
const items = [];
$('.item').each((i, el) => {
items.push({ title: $(el).find('h2').text().trim() });
});
return items;
}
```服务端5月29日 00:50
Cheerio 和 Puppeteer 有什么区别?爬虫场景怎么选?Cheerio 是纯 HTML 解析器,不执行 JavaScript,解析速度比 Puppeteer 快 100 倍以上,内存占用极低;Puppeteer 启动真实 Chromium,能执行 JS、处理动态渲染、模拟用户交互,但资源消耗大、速度慢。选择依据很简单:目标页面内容是否由 JS 动态生成——查看网页源码(Ctrl+U)能看到数据就用 Cheerio,看不到就用 Puppeteer。
## 追问
**Puppeteer 能不能用 Cheerio 解析页面?**
可以,这是常见的混合模式:Puppeteer 负责加载动态页面拿到渲染后的 HTML,再传给 Cheerio 做数据提取,兼顾动态能力与解析效率。
**怎么判断一个网站是否需要 Puppeteer?**
右键查看网页源码,搜索目标数据。如果源码中没有,说明是 JS 动态加载,需要 Puppeteer;源码中已有,直接 Cheerio 即可。也可以用 `curl` 请求看返回的原始 HTML。
**Cheerio 能否处理需要登录的页面?**
Cheerio 本身只做解析,登录请求通过 axios/fetch 带 cookie 发送即可,不需要 Puppeteer。只有登录过程涉及 JS 渲染或验证码交互时才需要浏览器。
**两者的资源消耗差异有多大?**
Cheerio 解析一个页面通常 <10ms、内存 <50MB;Puppeteer 启动浏览器就要 500ms+、内存 100-300MB。批量爬取时差异是数量级的,100 个页面 Cheerio 几秒搞定,Puppeteer 可能要几分钟。
## 写段代码
```javascript
// Puppeteer 拿动态页面 + Cheerio 解析
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const html = await page.content();
await browser.close();
const $ = cheerio.load(html);
$('.item').each((i, el) => {
console.log($(el).text());
});
```服务端5月29日 00:50
Cheerio 和 jQuery 有什么区别?Cheerio 是 Node.js 端的 HTML 解析库,实现了 jQuery 核心 API,但剥离了浏览器依赖——不渲染 DOM、不处理 CSS 布局、不执行 JavaScript,因此比 jQuery 轻量且解析速度快 8 倍以上。jQuery 运行在浏览器中操作真实 DOM,Cheerio 则在服务端解析 HTML 字符串构建虚拟 DOM 树,只做数据提取与结构操作。
## 追问
**Cheerio 为什么比 jsdom 快?**
jsdom 会构建完整的浏览器环境(CSSOM、布局计算等),Cheerio 仅构建 DOM 树,跳过了渲染管线,所以解析速度快一个数量级。
**Cheerio 能替代 jQuery 做前端开发吗?**
不能。Cheerio 没有 render 树,修改 DOM 后不会触发页面重绘,也无法绑定事件,它只适合服务端数据提取场景。
**Cheerio 的底层解析器是什么?**
默认使用 htmlparser2,也可切换到 parse5。htmlparser2 速度更快,parse5 则更严格遵循 HTML 规范。
**什么场景下必须用 jQuery 而非 Cheerio?**
需要事件监听、CSS 动画、视口计算、用户交互等浏览器原生能力时,必须用 jQuery。
## 写段代码
```javascript
const cheerio = require('cheerio');
const $ = cheerio.load('<ul><li>A</li><li>B</li></ul>');
$('li').each((i, el) => console.log($(el).text()));
```前端5月28日 03:16
如何判断 JS 文件是 Node.js 环境还是浏览器环境?看三个层面:模块语法、全局对象、环境 API,基本够用。
**模块语法**是最直观的线索——用了 `require`/`module.exports` 的基本是 Node.js(CommonJS),但这不是充分条件,因为浏览器端打包工具也能处理 CJS。反过来,纯 `import`/`export`(ESM)两边都能跑,不能用来判断。
**全局对象**更可靠:访问 `process`、`__dirname`、`__filename`、`global` 的是 Node.js;访问 `window`、`document`、`navigator`、`localStorage` 的是浏览器。但要注意,SSR 框架(Next.js)里两者可能同时存在。
**环境 API** 是最终判据:调了 `fs`、`child_process`、`net`、`crypto`(非 Web Crypto 子集)等 Node 核心模块的只能在 Node 跑;用了 DOM API(`document.querySelector`、`addEventListener`)、`WebSocket`、`WebRTC` 的只能在浏览器跑。
一个实用的判断函数:
```js
function detectEnv() {
if (typeof process !== 'undefined' && process.versions?.node) return 'node';
if (typeof window !== 'undefined' && typeof document !== 'undefined') return 'browser';
return 'unknown';
}
```
这个函数够用但不完美——Web Worker 里有 `self` 没有 `window`,Electron 里两个都有。
## 追问
### Webpack 的 target 配置和这个问题有什么关系?
`target: 'node'` 时 Webpack 不会 polyfill `fs`/`path` 等 Node 模块,`target: 'web'` 时会。如果源码用了 Node API 但打包目标是浏览器,构建会报错或打出空模块。所以看 `webpack.config.js` 的 `target` 也能反推这个文件的预期运行环境。
### TypeScript 怎么区分这两种环境的类型?
`tsconfig.json` 里 `"lib": ["DOM"]` 会注入浏览器类型(`document`、`window`),不加就没有。`"types": ["node"]` 会注入 Node 类型(`process`、`__dirname`)。编译时 TS 就能帮你揪出混用的情况——比如在 `lib` 不含 `DOM` 的配置下写了 `document.getElementById`,会直接报类型错误。
### 实际项目中踩过什么坑?
Next.js 里最常见——组件里直接用 `window` 做判断,SSR 阶段 `window` 不存在就炸了。正确做法是把浏览器 API 调用放进 `useEffect` 或 `typeof window !== 'undefined'` 守卫里。另一个坑:库的 `package.json` 没配 `exports` 字段,Node 和浏览器拿到同一个入口文件,结果浏览器端 import 了 Node 模块直接白屏。服务端5月27日 23:41
Cheerio 和 jsdom 有什么区别?如何选择使用?Cheerio 和 jsdom 是 Node.js 中处理 HTML 的两种常见方案,核心区别在于:Cheerio 是轻量解析器,只做 DOM 遍历和数据提取;jsdom 是完整浏览器环境模拟器,能执行 JavaScript 并提供 window、localStorage 等浏览器 API。选错工具会导致性能浪费或功能缺失,以下是关键对比。## 核心架构差异Cheerio 基于 htmlparser2 构建,实现了一套精简的 DOM 模型,仅保留节点遍历、属性读写和 CSS 选择器能力。它不解析 CSS、不执行脚本、不渲染页面,因此体积小、速度快。jsdom 基于 WHATWG DOM 标准实现,构造了完整的 `window` 对象,包括 `document`、`location`、`localStorage`、`fetch` 等 API。它内置 JavaScript 引擎,可通过 `runScripts: 'dangerously'` 执行页面脚本,还能加载外部资源。简单说:Cheerio 把 HTML 当字符串解析,jsdom 把 HTML 当浏览器渲染。## 功能与性能对比| 维度 | Cheerio | jsdom ||------|---------|-------|| 解析速度 | 快(约 5-10ms/万节点) | 慢(约 100-500ms/万节点) || 内存占用 | 低 | 高(约 8-10 倍) || CSS 选择器 | jQuery 风格,支持链式调用 | 标准 querySelector/querySelectorAll || JavaScript 执行 | 不支持 | 支持 || 浏览器 API | 无 | window/document/localStorage/fetch/Canvas || 事件系统 | 无 | 完整 DOM 事件冒泡机制 || HTML 容错性 | 高(htmlparser2 宽容解析) | 低(严格按标准解析) |Cheerio 的性能优势在批量处理时尤为明显。解析同一份万级节点的 HTML,Cheerio 通常比 jsdom 快 8 倍以上,内存占用低一个数量级。## 选择决策**选 Cheerio 的场景:**- 爬虫抓取静态页面数据(标题、链接、正文)- 批量处理 HTML 文档(清洗标签、提取字段)- 服务端模板渲染后的 HTML 后处理- Serverless 等资源受限环境**选 jsdom 的场景:**- 前端组件单元测试(模拟 DOM 环境)- 服务端渲染(SSR)需要执行客户端脚本- 处理依赖 JavaScript 动态渲染的页面- 需要浏览器 API 的 Node.js 代码(如 window.matchMedia)**一个实用判断标准:** 如果你只需要 `querySelector` + `textContent`,用 Cheerio;如果你需要 `window` 对象,用 jsdom。## 常见坑点**Cheerio 陷阱:** 静态抓取 SPA 页面会拿到空壳 HTML。此时需要搭配 Puppeteer 等无头浏览器先渲染,再用 Cheerio 解析结果,而非换用 jsdom——jsdom 执行 JS 的能力有限,对复杂 SPA 支持不完善。**jsdom 陷阱:** 默认不执行脚本(需手动开启 `runScripts`),且开启后存在安全风险,不要用 jsdom 执行不可信来源的 HTML。另外 jsdom 不支持 `requestAnimationFrame`、`IntersectionObserver` 等部分现代 API,Jest 等测试框架通常会补充 polyfill。## 代码示例Cheerio 快速提取数据:```javascriptconst cheerio = require('cheerio');const $ = cheerio.load(html);// jQuery 风格 APIconst title = $('h1').text();const links = $('a').map((i, el) => $(el).attr('href')).get();const cleaned = (() => { $('script, style').remove(); return $.html();})();```jsdom 模拟浏览器环境:```javascriptconst { JSDOM } = require('jsdom');const dom = new JSDOM(html, { runScripts: 'dangerously' });const document = dom.window.document;const title = document.querySelector('h1').textContent;// 访问浏览器 APIconst storage = dom.window.localStorage;const location = dom.window.location.href;```两者也可以组合使用:先用 Puppeteer 或 jsdom 获取 JS 执行后的 HTML,再用 Cheerio 高效提取数据。## 追问方向- **Cheerio 如何处理编码问题?** `cheerio.load(html, { decodeEntities: false })` 可避免中文乱码。- **jsdom 如何模拟用户交互?** 通过 `dom.window.dispatchEvent` 或 `fireEvent` 库触发事件。- **还有其他选择吗?** node-html-parser 更轻量,parse5 更标准,linkedom 性能介于 Cheerio 和 jsdom 之间。服务端5月27日 20:26
什么是 Deno?它和 Node.js 有什么区别?## Deno 的核心设计理念
Deno 由 Node.js 创始人 Ryan Dahl 在 2018 年发起,动机是他公开承认 Node.js 有几个设计决策无法在不破坏兼容性的前提下修复:默认不安全的执行环境、混乱的 node_modules 机制、以及 CommonJS 与 ES Modules 并存的模块系统。Deno 从零开始,用 Rust 重写了底层,试图给出更干净的答案。
## Deno 和 Node.js 的关键区别
**安全模型**:Deno 默认沙箱执行,脚本无法读写文件、访问网络或读取环境变量,必须通过 `--allow-read`、`--allow-net` 等标志显式授权。Node.js 默认完全信任脚本,没有权限墙。
**模块与包管理**:Deno 用 URL 直接导入模块,不依赖 package.json 和 node_modules。Deno 2.0 起 `npm:` 前缀已稳定支持,可以 `import express from "npm:express"` 直接使用 npm 包。Node.js 仍以 npm + package.json 为核心。
**TypeScript**:Deno 原生执行 .ts 文件,零配置。Node.js 22 虽已实验性支持,但生产环境仍需配置转译。
**API 风格**:Deno 全部采用 Promise/async-await,Node.js 保留了大量回调风格的旧 API。
**底层实现**:Deno 基于 Rust + V8,Node.js 基于 C++ + V8 + libuv。
```bash
# Deno 权限示例
deno run --allow-net --allow-read server.ts
```
## 2026 年选型建议
- **选 Node.js**:已有大型项目、依赖 npm 生态深度、团队招聘池大
- **选 Deno**:新项目优先安全(如跑不可信代码)、边缘部署(Deno Deploy)、TypeScript-first
- **考虑 Bun**:CI/CD 追求极致速度、高吞吐 HTTP 场景
## 追问方向
- Deno 的权限模型能防范供应链攻击吗?-- 能限制恶意包的文件和网络访问,但 `--allow-all` 等于没有限制
- Deno 2.0 的 npm 兼容性有没有坑?-- 大部分主流包可用,但依赖原生 C++ 模块的包可能失败
- 为什么 Ryan Dahl 认为node_modules是个错误?-- 它导致幽灵依赖、磁盘浪费、安装慢,Deno 用全局缓存 + URL 导入替代前端5月27日 01:16
Koa.js 如何实现文件上传的断点续传?断点续传的本质是"客户端记住传到哪了,服务端知道从哪继续接"。
**核心流程**:
1. 上传前计算文件 hash(MD5/SHA1),作为文件唯一标识
2. 发请求到服务端查"这个文件的哪些分片你有了"(返回已上传分片索引)
3. 客户端只上传缺失的分片
4. 服务端暂存每个分片
5. 全部分片上完后,服务端合并分片为完整文件
**Koa 侧关键点**:
- 用 `@koa/multer` 或直接读 stream 接收分片
- 分片命名规则:`{hash}-{index}`,便于按 hash 查找和按 index 排序
- 合并分片前校验每个分片的大小是否正确
- 合并完后校验完整文件的 hash 是否和客户端一致
## 追问
### 分片大小怎么定?
一般 1-5MB。太小请求次数多(HTTP 开销),太大断点续传意义不大了。网速好的用户可以用更大的分片。
### 并发上传多个分片好还是串行好?
并发上传更快,浏览器对同一域名的 HTTP/1.1 最大并发是 6 个(HTTP/2 不受限)。注意并发数不能太大——文件 I/O 是性能瓶颈,服务端同时写入大量分片会 IO 打满。
### 合并完大文件后内存会炸吗?
不会,用 `fs.createWriteStream`(流式写入)和 `fs.createReadStream`(流式读取)顺序追加。Koa 生态有 `fs-extra` 库做这些操作,底层是流式的不会一次性加载整个文件到内存。
服务端5月27日 01:12
Node.js 如何开启多进程?进程之间如何通讯?Node.js 用 `child_process` 模块创建子进程,用 `cluster` 模块做多核利用:
**child_process**:
- `spawn(command, args)`:启动一个新进程,返回流(适合长时间运行、大量输出的进程)
- `exec(command, callback)`:启动 shell 执行命令,缓存输出后回调(适合短命令)
- `fork(modulePath)`:特殊 spawn,创建 Node.js 子进程,自带 IPC 通道
**cluster**:基于 `fork` 封装,能创建多个共享同一端口的 worker 进程(常见于 HTTP 服务利用多核)。
**进程通讯**:`fork` 创建的父子进程间有 IPC 通道,用 `process.send(msg)` 和 `process.on('message')` 通信。底层实现:libuv 管道(pipe)。
## 追问
### cluster 怎么实现多进程共享端口?
主进程监听端口,将接收到的连接通过 Round-Robin 分发给 worker 进程。worker 不直接监听端口,而是接收主进程分配的连接句柄。Linux 上也可用 `SO_REUSEPORT` 内核级别的分发。
### fork 和 spawn 的区别?
fork 是 spawn 的特殊版——专门 fork Node.js 进程,自动建立 IPC 通道。spawn 启动任何命令,流式处理输出,适合与外部程序交互。
### PM2 的 cluster 模式和 fork 模式有什么区别?
- cluster:PM2 用 Node.js cluster 模块,多实例共享端口,自动负载均衡
- fork:PM2 只是用 child_process.fork 启动多个实例,需要不同端口或用 Nginx 做反向代理
服务端2024年6月24日 16:43
nodejs 的优点和缺点?### Node.js 的优点
#### 1. 高性能
Node.js 使用 V8 引擎,这是 Google Chrome 的 JavaScript 运行时,它将 JavaScript 代码编译成机器代码。这意味着 Node.js 能够提供高性能的网络应用。由于其非阻塞 I/O 和事件驱动架构,Node.js 特别适合处理大量并发连接,这对于实时应用程序(如游戏、聊天服务)和高流量服务是非常有利的。
#### 2. 单一语言开发
使用 Node.js,开发人员可以使用 JavaScript 编写前端和后端代码。这简化了开发流程,因为只需掌握一种语言和一套代码库即可。这也有助于前后端的高效协作。
#### 3. 强大的生态系统
Node.js 有一个庞大的生态系统,npm(Node.js 包管理器)是世界上最大的软件注册表。开发人员可以轻松地找到和共享各种库和工具,这有助于加快开发速度并减少重复造轮子的需要。
#### 4. 易于学习
由于 JavaScript 是最受欢迎的编程语言之一,许多开发人员已经熟悉它。这使得 Node.js 相对容易学习,尤其是对于那些已经有 JavaScript 经验的前端开发人员。
#### 5. 跨平台
Node.js 可以在多种平台上运行,包括 Windows、macOS、Linux,甚至在 Docker 容器中也能良好运作,这使得它非常灵活。
### Node.js 的缺点
#### 1. 单线程
虽然 Node.js 的单线程模型有助于处理高并发和简化开发,但它也意味着所有 I/O 密集型操作可能会阻塞事件循环,影响应用程序的整体性能。对于计算密集型任务,Node.js 可能不是最佳选择。
#### 2. 不稳定的API
Node.js 核心 API 的频繁变动曾经是一个问题,尽管现在已经相对稳定了。但开发者仍然需要留意 API 变动对项目的影响。
#### 3. 异步编程模型
Node.js 大量依赖异步代码,虽然这有助于提高性能,但也可能导致回调地狱(callback hell),使得代码难以理解和维护。尽管现在有 Promise 和 async/await 这样的解决方案,但对于新手来说,异步编程仍然可能是一个挑战。
#### 4. 性能瓶颈
Node.js 的性能虽然在处理 I/O 密集型任务时很出色,但在 CPU 密集型任务上可能就不那么理想。虽然可以通过创建子进程等方式来缓解这个问题,但这增加了复杂性。
#### 5. 年轻的工具
尽管 npm 生态系统非常庞大,但一些库和工具相对于其他语言的生态系统而言可能还不够成熟。这可能意味着更多的漏洞和不稳定性。
#### 实例
以性能为例,LinkedIn 将其后端服务从 Ruby on Rails 迁移到 Node.js,据报道提升了应用程序的性能,并显著减少了服务器需求。这展示了 Node.js 处理大规模网络服务时的性能优势。前端2024年6月24日 16:43
[Event Loop] 浏览器和nodejs事件循环有什么区别?在浏览器和Node.js中,事件循环是实现非阻塞I/O操作的核心机制,尽管它们在高层面上非常相似,但具体实现上有几个主要区别。以下是我将回顾的几点关键差异及其例子:
### 1. 任务源和处理方式
**浏览器:**
浏览器的事件循环主要处理来自Web API的任务,这些可以是DOM事件、Ajax回调、setTimeout等。它使用了宏任务(macro tasks)和微任务(micro tasks)的概念。宏任务包括script(整体代码)、setTimeout、setInterval和I/O,而微任务主要包括Promise.then、MutationObserver。在一个事件循环中,每次只会从宏任务队列中取出一个任务执行,然后执行所有可用的微任务。
**Node.js:**
Node.js的事件循环由libuv库实现,包括了多个阶段,如timers、I/O callbacks、poll、check、close callbacks等。Node.js中处理任务更为复杂,各个阶段几乎都有自己的队列。timers阶段处理setTimeout和setInterval回调,poll阶段负责I/O事件回调,而setImmediate的回调会在check阶段执行。
**例子:**
在浏览器中,`Promise.resolve().then()`会在当前宏任务完成后立即执行,因为微任务总是在宏任务之后清空。
在Node.js中,由于事件循环的阶段性,可能会在执行微任务时插入其他类型的任务,例如,如果在I/O操作完成后添加了一个setImmediate,邑可能在当前阶段的微任务和下一阶段的微任务之间执行。
### 2. 定时器的精度
**浏览器:**
浏览器的定时器(如setTimeout和setInterval)的精度相对较低,早期定时器至少有4ms的延迟(根据HTML5标准规定),而现代浏览器偶尔会有更高的延迟,以帮助减少后台标签页的能耗。
**Node.js:**
Node.js定时器的精度通常更高,因为服务器端的环境对实时性和性能有更高的要求。Node.js的事件循环可以精确到毫秒。
**例子:**
在浏览器中设置 `setTimeout(fn, 1)`可能实际上在4ms后才执行回调,而在Node.js中,相同的设置会尽量接近1ms执行回调。
### 3. 默认行为和扩展性
**浏览器:**
浏览器的事件循环通常是不可见和不可控制的,由浏览器内核管理。
**Node.js:**
Node.js的事件循环可以通过C++插件和核心模块进行扩展,给开发者提供了更多控制。例如,使用libuv库,开发者能够接触到底层的事件循环机制。
**例子:**
Node.js的开发者可以编写本地插件,通过直接与libuv交互来修改或增强事件循环的行为,而这在浏览器端是做不到的。
### 4. 性能和优化
**浏览器:**
浏览器的事件循环是为了优化用户界面和用户互动设计的,因此,许多优化都是围绕用户体验和界面响应性进行的。
**Node.js:**
Node.js的事件循环是针对I/O密集型操作进行优化的,特别是网络和文件系统操作。
服务端2024年6月24日 16:43
nodejs 如何使用 DllPlugin 动态链接库?Node.js 中的 `DllPlugin` 及相关的 `DllReferencePlugin` 主要是用于改善构建时间和实现代码分离,在 Webpack 构建过程中使用。`DllPlugin` 用来打包出一个个独立的动态链接库文件,而 `DllReferencePlugin` 则用于在主应用程序中引用这些动态链接库。
以下是使用 `DllPlugin` 的具体步骤:
### 步骤 1: 创建 DLL 文件
首先,你需要在项目中创建一个 webpack 配置文件专门用于构建 DLL。
```javascript
// webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
vendor: ['lodash', 'react'] // 假设我们希望将 lodash 和 react 打包成一个 DLL
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js', // 输出的文件名
library: '[name]_library' // 全局变量名,其他模块会从此变量上获取到里面的模块
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'dist', '[name]-manifest.json'),
name: '[name]_library'
})
]
};
```
这个配置会将 `lodash` 和 `react` 打包成一个名为 `vendor.dll.js` 的文件,并生成一个 `vendor-manifest.json` 文件。
### 步骤 2: 在主配置中引用 DLL
然后,在你的主 `webpack` 配置文件中,你需要使用 `DllReferencePlugin` 来引用上一步中生成的 `vendor-manifest.json` 文件。
```javascript
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
// ...你的其他配置
plugins: [
// ...你的其他插件
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dist/vendor-manifest.json') // 引入 DLL 的 manifest 文件
})
],
// ...其余配置
};
```
### 步骤 3: 在 HTML 中引入 DLL 文件
最后,你需要确保在应用程序加载前,先在 HTML 文件中引入这些 DLL 文件。例如:
```html
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<script src="./dist/vendor.dll.js"></script>
<!-- 接下来是你应用程序的其它脚本 -->
</body>
</html>
```
### 使用例子
假设你有一个大型项目,每次构建都需要很长时间,因为第三方库例如 `React`、`Vue` 或 `Lodash` 并不经常更改,但它们每次都会被重新编译。通过使用 DLL,你可以将这些库预编译成静态资源,以便在开发过程中重复使用,从而减少了构建时需要处理的工作量,并加快了构建速度。
使用 DLL 时需要注意的是,当你更新了 DLL 中的依赖项时,你需要重新构建 DLL 文件。同时,应当确保在生产环境构建中不包含 DLL 的引用,或者确保 DLL 是最新的,以避免因为版本不一致带来的问题。
总的来说,`DllPlugin` 提高了开发效率,尤其是在大型项目和频繁构建的环境中,它可以显著减少构建时间并提升开发体验。