面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月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 个并发,避免同时发起数百请求被限流或打挂目标服务器。写段代码// 流式解析大文件,内存恒定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); });
服务端阅读 05月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 版本。写段代码// 表格转对象数组插件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(); };};
服务端阅读 05月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 拿不到但浏览器能看到,就是动态渲染。写段代码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();}
服务端阅读 05月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 和事件绑定。写段代码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();
服务端阅读 05月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) 在回调中可写任意判断逻辑,更灵活。写段代码const $ = cheerio.load(html);const $list = $('#article-list');const titles = $list.find('.title').map((i, el) => $(el).text().trim()).get();
服务端阅读 05月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 },但后者会把换行也合并成空格,按需选择。写段代码// 正确处理 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);
服务端阅读 05月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 内容时更安全。写段代码const cheerio = require('cheerio');const $ = cheerio.load('<root><item>值</item></root>', { xmlMode: true, decodeEntities: false});console.log($('item').text()); // 值
服务端阅读 05月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 被封。写段代码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;}
服务端阅读 05月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 可能要几分钟。写段代码// 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());});
服务端阅读 05月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。写段代码const cheerio = require('cheerio');const $ = cheerio.load('<ul><li>A</li><li>B</li></ul>');$('li').each((i, el) => console.log($(el).text()));
前端阅读 1035月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 的只能在浏览器跑。一个实用的判断函数: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 模块直接白屏。
服务端阅读 05月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 之间。
服务端阅读 05月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。# Deno 权限示例deno run --allow-net --allow-read server.ts2026 年选型建议选 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 导入替代
前端阅读 965月27日 01:16

Koa.js 如何实现文件上传的断点续传?

断点续传的本质是"客户端记住传到哪了,服务端知道从哪继续接"。核心流程:上传前计算文件 hash(MD5/SHA1),作为文件唯一标识发请求到服务端查"这个文件的哪些分片你有了"(返回已上传分片索引)客户端只上传缺失的分片服务端暂存每个分片全部分片上完后,服务端合并分片为完整文件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 库做这些操作,底层是流式的不会一次性加载整个文件到内存。
服务端阅读 985月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 做反向代理
服务端阅读 1002024年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. 不稳定的APINode.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 处理大规模网络服务时的性能优势。
前端阅读 662024年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密集型操作进行优化的,特别是网络和文件系统操作。
服务端阅读 1082024年6月24日 16:43

nodejs 如何使用 DllPlugin 动态链接库?

Node.js 中的 DllPlugin 及相关的 DllReferencePlugin 主要是用于改善构建时间和实现代码分离,在 Webpack 构建过程中使用。DllPlugin 用来打包出一个个独立的动态链接库文件,而 DllReferencePlugin 则用于在主应用程序中引用这些动态链接库。以下是使用 DllPlugin 的具体步骤:步骤 1: 创建 DLL 文件首先,你需要在项目中创建一个 webpack 配置文件专门用于构建 DLL。// webpack.dll.config.jsconst 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 文件。// webpack.config.jsconst 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 文件。例如:<!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 提高了开发效率,尤其是在大型项目和频繁构建的环境中,它可以显著减少构建时间并提升开发体验。