面试题手册

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

服务端阅读 05月29日 01:09

Mongoose 聚合管道有哪些常用阶段?$match 为什么放最前面?

Mongoose 聚合通过 Model.aggregate([...stages]) 执行管道,常用阶段:$match 过滤文档、$group 分组统计($sum 计数/求和、$avg 均值、$push 收集数组)、$project 投影和计算新字段、$sort 排序、$limit/$skip 分页、$lookup 关联其他集合(类似 SQL JOIN)、$unwind 展开数组为多条记录、$facet 并行执行多个子管道。$match 必须放最前面,因为它能利用索引减少进入后续阶段的数据量,管道是顺序执行的,越早过滤性能越好。$lookup 是左外连接,localField/foreignField 匹配或 pipeline 子查询方式,结果以数组形式放入 as 指定字段。追问$lookup 的 pipeline 子查询方式和 localField/foreignField 有什么区别? pipeline 方式支持在关联时加 $match、$project 等阶段过滤和塑形关联数据,更灵活且减少返回量;localField/foreignField 只做等值连接,无法过滤。pipeline 还支持 let 变量引用主文档字段。$unwind 会导致数据膨胀怎么办? 对大数组 unwind 再 group 会产生大量中间文档。替代方案:用 $reduce 或 $map 在数组上直接聚合,或在 unwind 前用 $project 只保留需要的字段减少每条文档体积。$facet 有什么限制? 各子管道不能引用其他子管道的结果,也不能包含 $out 或 $merge 阶段。facet 内每个子管道独立处理同一份输入数据,内存消耗是各子管道之和。聚合结果超过 100MB 内存限制怎么办? 加 allowDiskUse: true 选项让中间结果写入临时文件,或优化管道尽早 $match + $project 缩减数据量。写段代码const stats = await Order.aggregate([ { $match: { createdAt: { $gte: new Date('2025-01-01') } } }, { $group: { _id: '$productId', total: { $sum: '$amount' }, count: { $sum: 1 } } }, { $sort: { total: -1 } }, { $limit: 10 }]);
服务端阅读 05月29日 01:09

如何配置 Cypress 测试报告和 CI/CD 集成?

Cypress 测试报告配置分两步:选 reporter、配参数。最常用的是 Mochawesome,在 cypress.config.js 中设 reporter 为 'mochawesome',通过 reporterOptions 指定 reportDir、overwrite: false、html: true、chart: true。如需合并多个 spec 的报告,搭配 mochawesome-merge 工具合并 JSON 再生成单份 HTML。CI/CD 集成的关键是:用 npx cypress run --reporter mochawesome 在无头模式执行;通过 --parallel 参数配合 Cypress Cloud 实现并行测试加速;用 actions/upload-artifact 收集报告和失败时的截图/视频;在 workflow 触发条件中绑定 push/pull_request 事件。失败截图和视频默认保存在 cypress/screenshots 和 cypress/videos 目录,CI 中应作为 artifact 上传以便排查。追问mochawesome-merge 的作用是什么?为什么多个 spec 会生成多份报告?Cypress 的 --parallel 参数如何工作?不使用 Cypress Cloud 能实现并行吗?如何在 CI 中只在测试失败时才上传视频和截图?Allure 报告和 Mochawesome 相比各有什么优劣?什么场景该选 Allure?如何在 GitHub Actions 中设置定时跑 Cypress 测试(cron 触发)?写段代码# .github/workflows/cypress.ymlname: Cypresson: [push]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx cypress run --reporter mochawesome - uses: actions/upload-artifact@v4 if: always() with: name: report path: cypress/results
服务端阅读 05月29日 01:09

Mongoose 鉴别器 Discriminator 怎么用?

Discriminator 是 Mongoose 的单集合继承机制,通过 Event.discriminator('Conference', confSchema) 在同一集合中存储不同结构的文档,用 discriminatorKey 字段区分类型。所有鉴别器共享基础 Schema 的字段和索引,各自扩展独有字段,适合多态数据模型。追问discriminatorKey 的作用是什么?它是 MongoDB 文档中用于标识类型的字段,默认叫 __t,可在基础 Schema 选项中自定义(如 kind、role)。创建鉴别器文档时自动写入,查询时 Mongoose 据此实例化正确的模型。基础 Schema 的中间件会作用于鉴别器吗?会。基础 Schema 的 pre/post 中间件对所有鉴别器生效,鉴别器自身的中间件只作用于该类型。利用这点可在基础 Schema 统一处理公共逻辑(如日志),鉴别器处理特有校验。嵌套鉴别器怎么实现?在子文档数组上调用 .discriminator():batchSchema.discriminator('ProductBatch', productBatchSchema),数组中的不同元素可以有不同的 Schema 结构,discriminatorKey 自动区分。鉴别器有什么局限?所有类型存在同一集合,无法对鉴别器特有字段建独立索引(索引会覆盖全集合);文档大小差异大时浪费存储;删除某类型文档需按 discriminatorKey 过滤;单集合数据量膨胀后影响所有类型的查询性能。鉴别器适合哪些场景?事件日志系统(Click/View/Purchase 共享时间戳基类)、内容管理(Article/Video/Image 共享标题和作者)、多角色用户(Admin/Customer 共享认证字段)。共同点:字段大量重叠、需要跨类型统一查询。写段代码const eventSchema = new Schema({ name: String, date: Date}, { discriminatorKey: 'kind' });const Event = mongoose.model('Event', eventSchema);const Conf = Event.discriminator('Conf', new Schema({ speakers: [String] }));await Conf.create({ name: 'JSConf', date: new Date(), speakers: ['Tom']});// MongoDB: { kind: 'Conf', name: 'JSConf', ... }
服务端阅读 05月29日 01:09

Mongoose Model 的 CRUD 方法有哪些,查询性能怎么优化?

Mongoose Model 常用 CRUD 方法:创建用 create() 和 insertMany();查询用 find()、findOne()、findById();更新用 updateOne()、findByIdAndUpdate(id, update, {new: true});删除用 deleteOne()、findByIdAndDelete()。链式查询支持 .select() 投影、.sort() 排序、.limit() 分页、.populate() 联查。findByIdAndUpdate 默认返回修改前文档,需传 {new: true} 获取更新后数据。追问find() 返回的结果和 lean() 有什么区别?find() 返回 Mongoose Document 实例,支持修改后 save()、虚拟字段、中间件;lean() 返回纯 JS 对象,内存占用低、查询速度快约 1.5-3 倍。只读展示场景务必用 lean(),需要修改并保存时才用 Document 实例。findByIdAndUpdate 能触发 save 中间件吗?不能。pre('save') 和 post('save') 只在 doc.save() 时触发。updateOne/findByIdAndUpdate 触发的是 pre('updateOne') 和 pre('findOneAndUpdate') 钩子。这是面试高频陷阱。批量操作用 insertMany 还是 bulkWrite?insertMany 只支持批量插入;bulkWrite 支持 insertOne/updateOne/deleteOne 混合操作,且底层走 MongoDB 的 bulk 协议,单次网络往返完成,性能更优。混合写操作场景优先 bulkWrite。countDocuments 和 estimatedDocumentCount 有什么区别?countDocuments 执行真实 COUNT 查询,支持过滤条件,结果精确;estimatedDocumentCount 读集合元数据估算,不支持条件,速度极快。无过滤条件的计数场景用 estimatedDocumentCount 更高效。写段代码// 批量混合操作await User.bulkWrite([ { updateOne: { filter: { name: 'Tom' }, update: { $inc: { age: 1 } } } }, { insertOne: { document: { name: 'Jerry', age: 20 } } }, { deleteOne: { filter: { name: 'Old' } } }]);// 只读查询用 leanconst users = await User.find({ age: { $gte: 18 } }) .select('name age').sort({ age: -1 }).lean();
前端阅读 05月29日 01:09

Web3 前端如何与后端服务协作?有哪些典型场景?

Web3 前端与后端的协作围绕链上和链下两条数据通路展开。链上交互通过钱包连接(window.ethereum)直接调用智能合约的 view/pure 方法读取状态、通过用户签名发送交易;链下交互则走传统 REST/GraphQL API,由后端代理聚合数据、管理会话、处理敏感逻辑。典型场景有五个:一是钱包身份验证——前端获取钱包地址并签名消息,后端验证签名后签发 JWT;二是读取合约状态——前端直接调用 view 函数或通过后端缓存聚合;三是发送交易——前端构造交易参数由用户在钱包确认签名,后端监听链上事件确认结果;四是事件监听——后端订阅合约事件(Transfer、Approval 等)通过 WebSocket 推送前端;五是链上数据索引——使用 The Graph 等索引服务将链上事件转为可查询的 GraphQL API,避免前端直接扫描区块。追问前端直接调用合约 view 函数和通过后端代理读取各有什么优劣?何时选哪种?用户签名消息的 EIP-712 标准是什么?比普通个人签名好在哪里?The Graph 的工作原理是什么?subgraph 如何定义和部署?如何处理后端服务宕机时前端的降级策略?能否直接切换到 RPC 节点?多链 DApp 中如何管理不同链的 provider 和合约实例?写段代码// 前端连接钱包并签名验证const accounts = await window.ethereum .request({ method: 'eth_requestAccounts' });const signer = new ethers.BrowserProvider( window.ethereum).getSigner();const signature = await signer .signMessage('login-nonce-123');// 将 address + signature 发给后端验证
服务端阅读 05月29日 01:09

Mongoose 插件怎么写?schema.plugin() 的原理是什么?

Mongoose 插件是一个接收 schema 和 options 参数的函数,通过 schema.plugin(pluginFn, options) 注册。插件内部可以调用 schema.add() 添加字段、schema.pre()/post() 注册中间件、schema.method() 添加实例方法、schema.static() 添加静态方法,本质就是对 Schema 的封装扩展。常用社区插件:mongoose-paginate-v2 分页、mongoose-delete 软删除、mongoose-unique-validator 唯一校验。全局注册用 mongoose.plugin(fn) 对之后创建的所有 Schema 生效。写自定义插件时注意:接受 options 参数提供可配置字段名;通过 schema.pre('find') 过滤而非覆盖查询;在插件中检查依赖字段是否存在避免隐性报错。追问插件执行顺序有影响吗? 有。plugin() 按调用顺序执行,后注册的中间件排后面。如果插件 A 在 pre save 中设置字段、插件 B 依赖该字段,A 必须先注册。全局插件对已有模型生效吗? 不生效。mongoose.plugin() 只对调用之后新创建的 Schema 生效,已编译的 Model 不会回溯应用。插件里能给 Schema 加虚拟属性吗? 可以,用 schema.virtual('name').get(fn)。但虚拟属性不持久化到数据库,且在 lean() 查询和 JSON 序列化时需显式配置 toJSON: { virtuals: true } 才能输出。mongoose-delete 插件的 overrideMethods 选项有什么用? 设为 true 后,find()、findOne() 等方法自动过滤已删除文档,无需手动加条件。本质是注册了查询中间件 pre('find') 添加 deleted: { $ne: true } 条件。写段代码function paginatePlugin(schema) { schema.statics.paginate = async function(query, { page = 1, limit = 10 }) { const docs = await this.find(query) .skip((page - 1) * limit).limit(limit); const total = await this.countDocuments(query); return { docs, total, pages: Math.ceil(total / limit) }; };}schema.plugin(paginatePlugin);
服务端阅读 05月29日 01:09

Mongoose 实例方法和静态方法有什么区别?

实例方法定义在 schema.methods 上,通过文档实例调用,this 指向当前文档,适合操作单条记录(如密码比对、计算总价)。静态方法定义在 schema.statics 上,通过模型类调用,this 指向模型本身,适合查询和批量操作(如按邮箱查找、统计数量)。核心区别:需不需要访问文档实例数据。追问this 上下文在两种方法中分别是什么?实例方法中 this 是当前 Document 实例,可访问 this.name、this.save() 等。静态方法中 this 是 Model 构造函数,等价于 User,可直接调用 this.find()、this.countDocuments()。什么时候该用实例方法?方法逻辑依赖文档自身数据时,比如 comparePassword 需要 this.password、getTotalPrice 需要 this.items。不依赖实例数据的方法放静态方法更合理。静态方法能替代普通查询吗?可以,静态方法本质是对查询的封装,让调用方不用关心查询细节。如 User.findByEmail('x') 比 User.findOne({ email: 'x' }) 语义更清晰,也方便统一修改查询逻辑。两种方法能用箭头函数定义吗?不能。箭头函数没有自己的 this,会捕获外层作用域的 this,导致无法访问文档实例或模型。必须用 function 关键字定义。静态方法返回值有什么讲究?返回 Query 对象可支持链式调用(.sort().limit()),返回 Promise 则终止链式。推荐静态方法返回 this.find() 的 Query,让调用方决定是否加排序或分页。写段代码// 实例方法:依赖文档数据userSchema.methods.comparePwd = function(pwd) { return bcrypt.compare(pwd, this.password);};// 静态方法:封装查询逻辑userSchema.statics.findByEmail = function(email) { return this.findOne({ email });};// 使用const user = await User.findByEmail('a@b.com');await user.comparePwd('123456');
服务端阅读 05月29日 01:08

JavaScript 中如何解析和序列化 JSON?有哪些高级用法?

JavaScript 通过 JSON.parse() 将 JSON 字符串转为 JS 值,通过 JSON.stringify() 将 JS 值转为 JSON 字符串。基本用法直观,但有几个关键细节:JSON.parse() 第二个参数 reviver 可在解析时转换值(如把日期字符串还原为 Date 对象);JSON.stringify() 第二个参数 replacer 可过滤或转换属性,第三个参数 space 控制缩进格式化输出。序列化时 undefined 和函数在对象中会被忽略、在数组中变为 null,Date 转为 ISO 字符串,RegExp 变为空对象,Symbol 键被丢弃。循环引用会抛出 TypeError。JSON.parse('123') 合法——JSON 的最外层不一定是对象。追问JSON.stringify() 遇到循环引用怎么办?如何实现一个安全的序列化函数?replacer 传数组 vs 传函数有什么区别?各自的典型使用场景是什么?JSON.stringify(NaN) 和 JSON.stringify(Infinity) 的结果是什么?为什么?BigInt 为什么不能被 JSON.stringify 序列化?有哪些变通方案?reviver 函数的执行顺序是怎样的?嵌套对象是从内到外还是从外到内?写段代码// replacer 过滤敏感字段const user = { name: 'Alice', token: 'secret', age: 25 };const safe = JSON.stringify(user, (k, v) => k === 'token' ? undefined : v);// {"name":"Alice","age":25}// reviver 还原日期const s = '{"date":"2025-01-01"}';const obj = JSON.parse(s, (k, v) => k === 'date' ? new Date(v) : v);// obj.date instanceof Date === true
服务端阅读 05月29日 01:08

Mongoose 查询性能怎么优化?

Mongoose 性能优化核心三板斧:.lean() 返回纯 JS 对象跳过文档 hydrated 过程提速 5-10 倍;.select() 投影只取需要的字段减少传输;合理建索引覆盖高频查询。三者组合使用效果最显著,读多写少的场景优先考虑 lean。追问.lean() 具体省掉了什么?Mongoose 默认将查询结果包装为 Document 实例,附带 change tracking、虚拟字段、方法等。lean() 跳过这些直接返回 POJO,丧失 save()、modify 等实例能力,但只读场景下性能提升显著。复合索引的 ESR 规则是什么?Equality → Sort → Range,索引字段按等值查询、排序、范围查询的顺序排列,让 MongoDB 最大化利用索引。如 { status: 1, createdAt: -1, age: 1 } 覆盖 WHERE status=? ORDER BY createdAt DESC WHERE age>?批量操作用什么 API?insertMany 批量插入、updateMany 批量更新、deleteMany 批量删除。单条循环操作每次都有网络往返,批量 API 一次请求完成,数量大时差距可达 10 倍以上。连接池怎么配?mongoose.connect uri 中设置 maxPoolSize(默认 100)和 minPoolSize。高并发短请求场景适当增大池,长连接场景注意 socketTimeoutMS 避免连接假死。连接应在应用启动时建立并复用,禁止请求内创建。为什么不建议滥用 populate?populate 每个引用字段都是一次额外查询,列表页查 20 条记录 populate 一个字段就变成 21 次查询。替代方案:$lookup 聚合在数据库端完成关联,或冗余存储常用字段减少关联需求。写段代码// lean + select + 索引 三板斧userSchema.index({ status: 1, createdAt: -1 });const users = await User .find({ status: 'active' }) .select('name email') .lean();// 批量插入替代循环await User.insertMany(usersToInsert);
服务端阅读 05月29日 01:08

Mongoose 和原生 MongoDB 驱动有什么区别,各适合什么场景?

核心区别在于抽象层级:Mongoose 是 ODM,通过 Schema 强制文档结构、内置验证、中间件钩子和类型转换;原生驱动是薄封装,直接操作集合,无结构约束,灵活但需手动处理验证和安全。Mongoose 适合需要数据一致性保障的业务系统,原生驱动适合高性能场景和非结构化数据处理。追问Mongoose 的性能开销主要来自哪里?三个来源:文档实例化(每条查询结果包装为 Mongoose Document)、Schema 验证和类型转换(save 前的校验链)、查询结果钩子(post hook 执行)。高读场景用 .lean() 跳过文档实例化,性能接近原生驱动。项目中能同时用两者吗?可以。Mongoose 底层连接可通过 mongoose.connection.db 获取原生 Db 实例。典型做法:需要验证的写操作走 Mongoose Model,批量聚合查询走原生 collection.aggregate(),各取所长。Mongoose 的 populate 和原生驱动的 $lookup 有什么区别?populate 是应用层联查,发两次查询再内存拼接;$lookup 是数据库层联查,一次聚合完成。大数据量时 $lookup 性能远优于 populate。populate 优势是自动按 Schema 解析和类型转换。什么情况下原生驱动反而更安全?当 Schema 定义错误导致类型转换异常时(如字符串 "123" 被静默转为数字 123),原生驱动不做隐式转换,数据原样存入,反而不会掩盖问题。对数据格式零容忍的场景,原生驱动更可控。写段代码// Mongoose: Schema 约束 + 验证const User = mongoose.model('User', new Schema({ name: { type: String, required: true }, age: { type: Number, min: 0 }}));await User.create({ name: 'Tom', age: 25 });// 原生驱动: 直接操作,无约束await db.collection('users').insertOne({ name: 'Tom', age: 25 });