面试题手册

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

服务端阅读 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 });
服务端阅读 05月29日 01:08

Mongoose 事务怎么用?乐观锁和悲观锁有什么区别?

Mongoose 事务依赖 MongoDB 副本集(单机不支持),通过 session 实现多文档原子操作。核心流程:startSession() 创建会话,session.startTransaction() 开启事务,操作时每条 CRUD 必须传 { session } 参数,最后 commitTransaction() 或 abortTransaction(),endSession() 释放资源。推荐用 withTransaction() 辅助方法自动提交/回滚。常见坑:忘记在操作中传 session 参数导致操作不在事务内、事务超时默认 60 秒需控制时长。并发控制方面,Mongoose 内置乐观锁:Schema 的 __v 字段在 save() 时自动递增,若文档已被修改则抛 VersionError;悲观锁需自行实现,通常用字段锁标记 + findOneAndUpdate 原子设置。追问withTransaction() 和手动管理事务有什么区别? withTransaction(fn) 自动处理 commit/abort,fn 返回时自动提交,fn 抛异常自动回滚,遇到 TransientTransactionError 还会自动重试。手动管理则需自己 try/catch 并调 commit/abort。哪些操作不支持事务? 创建集合、创建索引等 DDL 操作在 4.4 之前不能在事务中执行。事务内也不能操作 config、admin 等系统数据库的集合。乐观锁的 __v 在什么场景下会失效? 使用 updateOne/findOneAndUpdate 等不经过 save() 的操作时 __v 不会自动检查。需要手动在条件中加入版本号:{ _id, __v: currentVersion }。事务中读写同一个文档会死锁吗? 可能。两个事务同时修改同一文档会产生写冲突,后提交的事务会抛 WriteConflict 错误。解决:控制事务粒度、按固定顺序访问文档、加重试逻辑。写段代码const session = await mongoose.startSession();await session.withTransaction(async () => { await Account.updateOne( { _id: fromId, balance: { $gte: amount } }, { $inc: { balance: -amount } }, { session } ); await Account.updateOne( { _id: toId }, { $inc: { balance: amount } }, { session } );});session.endSession();
服务端阅读 05月29日 01:08

JSON 的语法规则有哪些?哪些是容易踩坑的?

JSON 语法看似简单,但有几条规则与 JavaScript 对象字面量不同,容易踩坑。核心规则:键名必须用双引号包裹("name" 不能写成 name);字符串也只能用双引号,单引号非法;最后一个键值对后不能有尾逗号;不支持注释;值只能是 string、number、boolean、null、object、array 六种类型,不支持 undefined、Date、Function、RegExp。数字不支持前导零(01 非法)和 NaN/Infinity。字符串中的特殊字符需转义,如 \n、\"、\\。大小写敏感:只有小写的 true、false、null 合法,TRUE 或 Null 都会解析失败。追问{"a": 1,} 在 JS 中合法但 JSON 非法,为什么 JSON 要禁止尾逗号?JSON.parse('{"a": undefined}') 会怎样?undefined 在对象和数组中的序列化行为有何不同?JSON 数字为什么不区分整数和浮点数?1e3 在 JSON 中合法吗?如何在 JSON 中表示二进制数据?Base64 编码有什么局限?JSON.parse('123') 合法吗?JSON.parse('"hello"') 呢?最外层必须是对象吗?写段代码// 合法 JSONconst a = JSON.parse('{"name":"Alice","age":25}');// 非法:单引号键名// JSON.parse("{'name':'Alice'}"); // SyntaxError// 非法:尾逗号// JSON.parse('{"name":"Alice",}'); // SyntaxError// 合法:最外层不是对象JSON.parse('123'); // 123JSON.parse('"hi"'); // "hi"
服务端阅读 05月29日 01:08

Mongoose 子文档和引用怎么选?

子文档是嵌套在父文档中的 Schema,分单个子文档和子文档数组两种形式。子文档与父文档同生同灭、原子更新,适合总是一起访问且数量有限的数据。当数据需要独立查询、数量可能很大时,改用 ObjectId 引用。追问子文档的验证是何时触发的?父文档保存时,子文档的验证会自动级联触发。如果子文档校验失败,整个保存操作回滚,无需手动调用子文档验证。子文档怎么访问父文档?通过子文档的 $parent 属性:comment.$parent 可拿到所属的 post 文档,便于在子文档方法中读取父级上下文。向子文档数组添加元素有哪些方式?push/unshift 追加到数组,Mongoose 会自动标记数组为 modified。也可用 $push + $each 原子操作批量添加,避免先查再改的两步操作。删除子文档用什么方法?pull 按条件删除:post.comments.pull({ author: 'Alice' });splice 按索引删除;也可用 $pull 原子操作。pull 更安全,不会因索引偏移误删。子文档数组的单文档大小限制是什么?MongoDB 单文档上限 16MB,子文档数组过大不仅逼近限制,还导致查询和更新性能下降。经验值:数组元素超过几百条就该考虑拆分为独立集合。写段代码const commentSchema = new Schema({ text: String, author: String});const postSchema = new Schema({ title: String, comments: [commentSchema]});// 添加 + 删除子文档const post = await Post.findById(id);post.comments.push({ text: 'nice', author: 'Tom' });post.comments.pull({ author: 'Tom' });await post.save();
服务端阅读 05月29日 01:08

Mongoose 中间件的 pre 和 post 钩子怎么用?

Mongoose 中间件分两类:文档中间件(作用于 save/validate/remove,this 指向文档实例)和查询中间件(作用于 find/findOne/updateOne/deleteOne,this 指向 Query 对象)。pre 钩子在操作前执行,可修改数据或通过 next(err) 中止操作;post 钩子在操作后执行,接收结果参数,不能中止但适合日志和通知。异步中间件用 async function 即可,不需要手动调 next(),抛异常自动中止。经典场景:pre save 哈希密码、post save 写审计日志、pre find 过滤软删除记录。注意 findOneAndUpdate 不触发 save 钩子,需要单独注册 findOneAndUpdate 的 pre/post。追问pre 钩子中 next() 和 async/await 能混用吗? 不建议。选一种即可:传统写法调 next(err),async 写法直接抛异常或正常返回。混用可能导致钩子执行两次或跳过后续中间件。查询中间件里怎么修改查询条件? 通过 this(Query 对象)调用 .where()、.skip() 等方法,例如 this.where({ deleted: false })。不要直接修改 this._conditions,应使用 Query API。post 钩子能拿到修改前的值吗? post save 中 this 是保存后的文档,doc 参数也是。如需旧值,在 pre save 中用 this.modifiedPaths() 记录变更字段,或用 this.$__original() 在 Mongoose 7+ 获取原始值。remove 中间件在 Mongoose 7+ 有什么变化? remove 钩子已废弃,改为 deleteOne。文档级的 this.deleteOne() 触发文档中间件,Model.deleteOne() 触发查询中间件,两者 this 含义不同。写段代码userSchema.pre('save', async function() { if (!this.isModified('password')) return; this.password = await bcrypt.hash(this.password, 10);});userSchema.post('save', function(doc) { AuditLog.create({ action: 'save', docId: doc._id });});
服务端阅读 05月29日 01:08

Mongoose 数据验证有哪些类型,自定义验证器怎么写?

Mongoose 验证分内置和自定义两类。内置验证器包括:required(必填)、min/max(数值范围)、minlength/maxlength(字符串长度)、enum(枚举值)、match(正则匹配)。自定义验证通过字段的 validate 选项实现,支持同步和异步两种方式。验证默认在 save() 时触发,也可显式调用 validate() 或 validateSync()。追问unique 是验证器吗?它和 required 有什么本质区别?unique 不是验证器,是索引声明器。它在数据库层创建唯一索引,Mongoose 验证阶段不检查唯一性,只有 MongoDB 写入时才报错。required 才是真正的应用层验证器,在保存前就拦截。所以 unique 的错误是 MongoDB E11000,不是 Mongoose ValidationError。异步验证器有什么使用场景和注意事项?典型场景:验证邮箱/用户名是否已存在(需查库)。注意异步验证器中 this 指向当前文档实例,用 this.constructor.findOne() 避免硬编码 Model 名;更新操作默认不触发验证,需设置 runValidators: true。update 操作的验证和 save 有什么区别?save 触发完整验证链(内置+自定义+pre validate钩子);updateOne/updateById 默认跳过验证,需手动传 { runValidators: true },且更新验证只能校验即将修改的字段,无法访问完整文档上下文。validateBeforeSave 什么时候该设为 false?仅在数据已通过其他可信渠道验证时使用(如内部迁移脚本、测试数据填充)。生产业务代码不建议跳过,否则脏数据会直接写入数据库。写段代码const userSchema = new Schema({ email: { type: String, required: true, validate: { validator: async function(v) { const doc = await this.constructor.findOne({ email: v }); return !doc || doc._id.equals(this._id); }, message: '邮箱已被注册' } }, role: { type: String, enum: ['admin', 'user'], default: 'user' }});
服务端阅读 05月29日 01:08

JSON 是什么?为什么它能取代 XML 成为数据交换的主流格式?

JSON(JavaScript Object Notation)是一种基于文本的轻量级数据交换格式,采用键值对和数组的结构来组织数据。它脱胎于 JavaScript 对象字面量语法,但已被所有主流编程语言支持,成为 Web API 响应的事实标准。相比 XML,JSON 去掉了冗余的闭合标签,数据体积更小、解析更快,且天然映射为编程语言的内置数据结构(对象和数组),无需额外的 DOM 解析层。JSON 的主要用途包括:客户端与服务端之间的数据传输、配置文件(如 package.json、tsconfig.json)、NoSQL 数据库存储(如 MongoDB),以及跨语言的数据序列化。追问JSON 和 JavaScript 对象字面量有什么区别?哪些合法的 JS 对象在 JSON 中是非法的?为什么 JSON 不支持注释?这在配置场景下会带来什么问题,社区有哪些变通方案?JSON 中的数字类型为什么不区分整数和浮点数?这会导致什么精度问题?如果要在跨语言场景中使用 JSON 传递日期,有哪些常见的约定做法?JSON5 和 JSON 的关系是什么?为什么不推荐在生产环境使用 JSON5?写段代码const data = { name: "Alice", scores: [95, 87, 92], active: true};const json = JSON.stringify(data);const parsed = JSON.parse(json);console.log(parsed.scores[0]); // 95
服务端阅读 05月29日 01:08

Mongoose Populate 如何实现文档关联查询?

Mongoose 通过 ObjectId 引用(ref)建立文档关联,再用 .populate() 将引用自动替换为完整文档。基本流程:Schema 中用 ref 声明引用,查询时链式调用 .populate('field') 即可获取关联数据,支持多字段、嵌套和条件填充。追问populate 的深层嵌套怎么写?使用对象参数嵌套 populate:.populate({ path: 'comments', populate: { path: 'user' } }),可无限层级嵌套,但超过 3 层说明数据模型需要重新设计。如何只填充关联文档的部分字段?在 populate 选项中加 select:.populate({ path: 'author', select: 'name email' }),减少数据传输量。Virtual Populate 和普通 populate 有什么区别?Virtual Populate 通过 schema.virtual() 定义,关联字段不存入数据库,查询时动态填充,适合反向关联(如通过 author 查其所有 books),不占用存储空间。populate 的 N+1 问题怎么解决?populate 本质是额外查询,列表场景下每条记录都会触发一次查询。替代方案:用 aggregation 的 $lookup 在数据库端完成关联,一条聚合语句搞定,性能更优。条件 populate 怎么用?.match 过滤关联文档:.populate({ path: 'author', match: { status: 'active' } }),不匹配时该字段返回 null,需在业务层处理。写段代码const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' }});// 基本 populate + 选择字段const book = await Book.findById(id) .populate({ path: 'author', select: 'name email' });// 嵌套 populateconst post = await Post.findById(id) .populate({ path: 'comments', populate: { path: 'user' } });
服务端阅读 05月29日 01:08

Mongoose 查询构建器的链式方法和性能优化有哪些?

Mongoose 查询构建器支持两种写法:对象语法 User.find({ age: { $gte: 18 } }) 和链式语法 User.find().where('age').gte(18)。链式方法包括 .where() 设置字段、.gt()/.lt()/.gte()/.lte() 范围筛选、.in() 匹配数组、.sort() 排序、.limit().skip() 分页、.select() 投影字段、.populate() 关联填充。性能优化关键三点:.lean() 跳过文档封装返回纯 JS 对象,查询速度提升约 30%;.select() 只取必要字段减少传输量;确保查询字段有索引。链式调用时方法顺序不影响查询结果,但可读性上建议先筛选再排序分页。追问链式查询和对象查询能混用吗? 可以,两者等价,Mongoose 内部会将链式调用合并为同一个查询条件对象。但混用时不建议对同一字段既用对象又用链式,容易产生冲突。lean() 返回的对象为什么不能调用 save()? lean() 跳过了 Mongoose 文档实例化,返回的是普通对象,没有 $__ 内部状态和实例方法。需要修改再保存时应去掉 lean()。populate() 的性能问题怎么解决? populate 本质是额外查询,N 个文档关联会产生 N+1 次查询。替代方案:用 $lookup 聚合在数据库端完成关联,或手动批量查询减少往返次数。如何调试慢查询? 开启 mongoose.set('debug', true) 打印实际查询语句,或对查询链调用 .explain('executionStats') 查看索引命中和扫描行数。写段代码const users = await User.find({ status: 'active' }) .where('age').gte(18).lte(60) .sort({ createdAt: -1 }) .select('name email age') .populate('department', 'name') .lean() .limit(20);
服务端阅读 05月29日 01:07

Mongoose Schema 怎么定义,有哪些常用配置项?

Schema 是 Mongoose 的文档结构蓝图,定义字段类型、验证规则、默认值和索引。它本身不操作数据库,需通过 mongoose.model() 编译为 Model 后使用。核心配置项包括:类型声明(String/Number/Date/ObjectId/Array)、验证器(required/min/max/enum/match)、修饰符(trim/lowercase/uppercase)、默认值(default)和索引(unique/index/sparse)。追问Schema 的 strict 选项有什么作用?strict 默认为 true,未在 Schema 中声明的字段会被自动忽略,不会写入数据库。设为 false 允许存储任意字段,但不推荐,会丧失结构约束的意义。虚拟字段怎么定义,和普通字段有什么区别?通过 schema.virtual('name').get() 定义,不存入数据库、不占存储、不能用于查询筛选。适合拼接展示字段(如 fullName),不适合业务排序或过滤。Schema 怎么添加实例方法和静态方法?实例方法用 schema.methods.xxx = function(),通过文档实例调用,this 指向文档;静态方法用 schema.statics.xxx = function(),通过 Model 类调用,this 指向 Model。注意不要用箭头函数,否则 this 绑定错误。嵌套文档和引用(ref)该选哪个?嵌套文档适合一对一或少量子文档,查询简单但文档体积大;引用(ref + ObjectId)适合多对多或大数据集,需 populate 联查但文档精简。频繁一起读取用嵌套,独立生命周期用引用。写段代码const postSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true }, author: { type: Schema.Types.ObjectId, ref: 'User', required: true }, tags: [{ type: String, enum: ['js', 'node', 'db'] }], views: { type: Number, default: 0, min: 0 }}, { timestamps: true });
服务端阅读 05月29日 01:07

Mongoose 是什么,为什么要用它而不是原生 MongoDB 驱动?

Mongoose 是 MongoDB 的 ODM(对象文档建模)库,在 Node.js 和 MongoDB 之间提供结构化抽象层。核心价值:通过 Schema 强制文档结构、内置数据验证、支持中间件钩子(pre/post)、自动类型转换和虚拟字段,让开发者以面向对象方式操作 MongoDB,显著降低数据不一致风险。追问Mongoose 的 Schema 和 Model 是什么关系?Schema 定义文档结构(字段类型、默认值、验证规则),Model 是 Schema 的编译产物,即操作数据库的构造函数。一个 Schema 可编译出多个 Model。pre 和 post 中间件分别适合做什么?pre 常用于密码加密(save 前 hash)、数据格式化、权限校验;post 常用于日志记录、缓存清理、触发异步通知。pre 可通过 next() 或 async/await 控制流程中断。Mongoose 的类型转换(casting)会带来什么隐患?查询时 Mongoose 自动将字符串 "123" 转为数字 123,可能掩盖数据错误。生产环境建议配合严格验证,或使用 strictQuery 选项控制行为。虚拟字段和真实字段有什么区别?虚拟字段不存入数据库,由 get/set 计算得出,无法用于查询和排序。适合组合展示(如 fullName = firstName + lastName),不适合业务筛选。写段代码const userSchema = new mongoose.Schema({ name: { type: String, required: true, trim: true }, email: { type: String, required: true, lowercase: true }, age: { type: Number, min: 0, default: 0 }});userSchema.virtual('info').get(() => `${this.name}, ${this.age}岁`);const User = mongoose.model('User', userSchema);
服务端阅读 05月29日 00:52

如何在 Cypress 中用 cy.request() 测试 API 接口?

cy.request() 是 Cypress 内置的 HTTP 请求方法,直接在网络层发送请求,无需经过浏览器渲染,适合独立验证后端逻辑。典型场景:绕过 UI 直接测试 API 返回值和状态码、在 UI 测试前通过 API 预置测试数据(如登录获取 token)、验证认证头和权限控制。最佳实践包括:用 Cypress.env() 管理环境变量避免硬编码 URL、用 failOnStatus: false 测试错误响应、结合 cy.intercept() 模拟后端异常、用 response.duration 断言接口性能。注意 cy.request() 默认 4xx/5xx 会导致测试失败,测试异常场景时必须设置 failOnStatusCode: false。追问cy.request() 和 cy.intercept() 有什么区别?什么时候该配合使用?如何用 cy.request() 实现 UI 测试前的数据预置?比通过 UI 操作准备数据有什么优势?测试需要鉴权的 API 时,如何自动获取并注入 Bearer Token?cy.request() 发送的请求会走浏览器的 CORS 限制吗?为什么?如何对 cy.request() 的响应做 Schema 校验而不只是断言个别字段?写段代码// API 测试:登录 + 鉴权 + 错误场景it('returns 401 with wrong password', () => { cy.request({ url: '/api/login', method: 'POST', body: { user: 'admin', pass: 'wrong' }, failOnStatusCode: false }).its('status').should('eq', 401);});
服务端阅读 05月29日 00:52

Cypress 如何处理异步操作?命令链和自动等待机制是什么?

Cypress 的命令不是立即执行,而是入队后按序串行执行。每个命令返回 chainable 对象,后续命令挂载到链条上形成命令队列,Cypress 依次取出执行并自动等待前置条件满足。自动等待指每个命令内建重试机制:cy.get() 会反复查询 DOM 直到元素存在且可见,cy.request() 会等待响应返回,默认超时 4 秒。开发者无需写 sleep 或显式等待,Cypress 在命令间自动处理异步时序。追问命令队列和 Promise 链有什么区别?命令队列在 .then() 之前不会执行,是同步入队异步执行;Promise 链是立即执行。所以不能把 Cypress 命令赋值给变量:const el = cy.get('#btn') 拿到的是 chainable 不是元素,必须用 .then() 回调取值。什么时候需要用 .then()?需要访问命令返回值或混合同步逻辑时。比如从响应中提取 ID 再构造下一个请求。注意 .then() 内部的 cy 命令会重新入队,不会立即执行。自动等待超时了怎么办?可通过 { timeout: 10000 } 单独设置,或在 cypress.config.js 中配置 defaultCommandTimeout 全局调整。超时后命令失败,测试中断并截图。应优先用 should() 断言替代加大超时。cy.wait() 和自动等待什么时候用?自动等待覆盖 DOM 和 XHR 场景,一般够用。但 cy.intercept() 拦截请求后需 cy.wait('@alias') 确保请求完成再断言响应,这是显式等待的典型场景。为什么不能在 .then() 外用 async/await?Cypress 命令不在 Promise 上运行,await 一个 chainable 不会等命令执行完。混用 async/await 会导致时序错乱,Cypress 官方明确不推荐在命令链中使用 async/await。写段代码cy.intercept('GET', '/api/users').as('users');cy.visit('/dashboard');cy.wait('@users').its('response.statusCode').should('eq', 200);cy.get('#user-list').should('be.visible');
服务端阅读 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:52

Cypress 和 Selenium 有什么区别?何时选择 Cypress?

核心区别在架构:Cypress 运行在与应用同源的浏览器内,通过 Chrome DevTools Protocol 直接操作 DOM,内置自动等待和重试机制;Selenium 通过外部 WebDriver 进程与浏览器通信,需显式编写等待逻辑。这意味着 Cypress 调试体验远优于 Selenium(可视化 Test Runner、时间旅行),且代码更简洁,但仅支持 Chromium 内核和 JavaScript;Selenium 跨浏览器覆盖全面(Chrome/Firefox/Safari),支持多语言(Java/Python/C#),适合需要兼容性测试的团队。选择 Cypress 的场景:前端 SPA 项目为主、团队用 JavaScript、追求快速反馈和低维护成本。选 Selenium 的场景:必须覆盖多浏览器、团队非 JS 技术栈、需测试非 Web 应用。追问Cypress 的同源架构为什么无法测试跨域场景?有什么变通方案?Selenium 的显式等待(WebDriverWait)和隐式等待(implicit wait)有什么区别?各自的风险是什么?Cypress 的 cy.intercept() 如何模拟后端响应?与 Selenium 的 Mock Server 方案相比优劣如何?大型项目中 Cypress 测试执行变慢,如何优化?Playwright 与 Cypress 相比有哪些改进?是否正在取代 Cypress?写段代码// Cypress: 自动等待,无需 sleepcy.visit('/login');cy.get('#user').type('admin');cy.get('#pass').type('1234');cy.get('#submit').click();cy.url().should('include', '/dashboard');// Selenium (Python): 必须显式等待from selenium.webdriver.support.ui import WebDriverWaitelem = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, 'submit')))elem.click()
服务端阅读 05月29日 00:51

Elasticsearch 的 suggest 功能如何实现自动补全?

ES 提供四种 suggester:Completion Suggester 基于内存 FST(有限状态转换器)做前缀匹配,延迟极低,适合自动补全;Term Suggester 基于编辑距离做拼写纠错;Phrase Suggester 在 Term 基础上加 n-gram 语言模型优化整句建议;Context Suggester 为 Completion 增加分类/地理上下文过滤。自动补全场景用 Completion Suggester:索引时将建议词存入 completion 类型字段构建 FST,查询时用 prefix 参数匹配,毫秒级返回。追问Completion Suggester 为什么快?它将建议词构建为 FST 结构常驻内存,前缀匹配是 O(k) 复杂度(k 为前缀长度),不涉及倒排索引扫描和评分计算,所以延迟在毫秒级。Completion 和 searchasyou_type 有什么区别?Completion 是独立建议通道,返回建议词而非文档;searchasyoutype 是特殊 text 子类型,本质还是全文检索返回文档。Completion 更适合输入框补全,searchasyoutype 更适合边输边搜文档内容。拼写纠错用哪个?Term Suggester。它基于倒排索引中的词项做编辑距离计算,返回相似词建议。需要指定 suggest_mode:missing(仅缺词建议)、popular(热门词优先)、always(始终建议)。如何给建议词加权重?在 completion 字段中设置 weight 属性,权重高的建议优先返回。常用于热门搜索词提权:"suggest": {"input": "laptop", "weight": 100}。FST 内存开销大吗?FST 是高度压缩的有向无环图,百万级建议词通常只占几十 MB。但建议词总量达亿级时需评估内存,可用 context 过滤减少单次匹配范围。写段代码PUT /search-suggest{ "mappings": { "properties": { "title": { "type": "keyword" }, "suggest": { "type": "completion", "analyzer": "standard" } } }}GET /search-suggest/_search{ "suggest": { "auto": { "prefix": "elas", "completion": { "field": "suggest", "size": 5 } } }}