Next.js 14服务器缓存导致SSG预渲染MongoDB连接异常问题
我正在开发一个需要国际化的厨师网站,使用SSG为每个语言环境预渲染所有页面。构建阶段遇到了问题:只有当后端数据被缓存时才会报错,禁用缓存、不使用SSG或开发模式下一切正常。缓存会阻塞数据库请求,抛出MongoNotConnectedError: Client must be connected before running operations。
已经确认不是数据库本身的问题,禁用缓存后运行正常,但这样就失去了SSG的意义——页面会每次都从数据库动态获取数据重新生成。
Menu页面代码(app目录)
SSG语言环境在主布局级别设置:
import { getAllProducts } from '@/database/dbProducts'; import XDraggableList from '../XDraggableList'; import MenuItem from './MenuItem'; import { IMenuItem } from '@/interfaces/IMenuItem'; const Menu = async () => { const products: IMenuItem[] = await getAllProducts(); // ... 剩余代码 }; export default Menu;
数据库数据获取代码
import { db } from '.'; import { IMenuItem } from '@/interfaces/IMenuItem'; import Product from '@/models/Product'; import { unstable_noStore } from 'next/cache'; export const getAllProducts = async (): Promise<IMenuItem[]> => { // TODO: 为什么不禁用缓存时构建会报错 // unstable_noStore(); // 如果不使用unstable_noStore(),构建时就会出现MongoNotConnectedError // 但禁用缓存后,请求会一直访问数据库,失去缓存作用 await db.connect(); const products = await Product.find().sort({ name: 1 }).lean(); await db.disconnect(); return JSON.parse(JSON.stringify(products)); };
构建错误信息
$ yarn build yarn run v1.22.19 $ next build ▲ Next.js 14.0.3 - Environments: .env ✓ Creating an optimized production build ✓ Compiled successfully ✓ Linting and checking validity of types ✓ Collecting page data Generating static pages (24/32) [ ==] Connected to MongoDb: MongoNotConnectedError: Client must be connected before running operations at executeOperationAsync (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\operations\execute_operation.js:23:19) at C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\operations\execute_operation.js:12:45 at maybeCallback (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\utils.js:269:21) at executeOperation (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\operations\execute_operation.js:12:38) at FindCursor._initialize (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\find_cursor.js:55:73) at [kInit] (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\abstract_cursor.js:450:38) at next (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\abstract_cursor.js:510:32) at FindCursor.next (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\abstract_cursor.js:226:16) at [Symbol.asyncIterator] (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\abstract_cursor.js:160:45) at AsyncGenerator.next (<anonymous>) { [Symbol(errorLabels)]: Set(0) {} }
错误重复7次后,各语言页面的预渲染都会报错。禁用缓存能解决问题,但会让SSG失效,请问该如何处理?
问题根源
Next.js在SSG构建时会缓存数据获取函数的结果,当缓存命中时,不会重新执行函数体内的代码——包括await db.connect()和await db.disconnect()。第一次执行函数时,连接建立后又被断开,后续缓存复用结果时,不会重新建立连接,但MongoDB驱动会尝试复用之前的断开连接,导致报错。
简单说:缓存让getAllProducts只执行一次,断开连接后,后续复用缓存的场景下(多语言页面预渲染),没有重新建立连接就直接执行查询,触发错误。
修复方案
方案1:修改MongoDB连接逻辑,避免重复断开连接
不要在每次数据请求后都断开连接,而是复用全局连接。MongoDB驱动本身会维护连接池,频繁断开/重连反而影响性能。
修改db模块的连接逻辑:
// db/index.ts import mongoose from 'mongoose'; const MONGODB_URI = process.env.MONGODB_URI!; if (!MONGODB_URI) { throw new Error('Please define the MONGODB_URI environment variable'); } // 全局缓存连接实例 let cached = global.mongoose; if (!cached) { cached = global.mongoose = { conn: null, promise: null }; } export async function connect() { if (cached.conn) { return cached.conn; } if (!cached.promise) { const opts = { bufferCommands: false, }; cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { return mongoose; }); } cached.conn = await cached.promise; return cached.conn; } // 移除disconnect逻辑,不需要每次请求后断开
然后修改getAllProducts:
export const getAllProducts = async (): Promise<IMenuItem[]> => { await db.connect(); const products = await Product.find().sort({ name: 1 }).lean(); // 不再调用disconnect return JSON.parse(JSON.stringify(products)); };
这样连接会被全局复用,即使Next.js缓存数据获取函数,后续执行时也能使用已建立的连接(或者自动重新连接),不会出现断开的情况。
方案2:使用revalidate控制缓存,而非禁用缓存
如果必须保持断开连接的逻辑,可以在页面或数据函数中指定revalidate,让Next.js在构建时正确处理多语言场景的缓存,同时保留SSG能力。
在Menu页面添加generateStaticParams和revalidate:
import { getAllProducts } from '@/database/dbProducts'; // ... 其他导入 export async function generateStaticParams() { // 返回所有语言环境参数,比如['en', 'es', 'fr'] return [{ lang: 'en' }, { lang: 'es' }, /* ...其他语言 */]; } // 设置缓存重新验证时间,这里设置为永久(SSG),或者按需设置 export const revalidate = false; // 等同于SSG,永久缓存 const Menu = async ({ params }: { params: { lang: string } }) => { const products: IMenuItem[] = await getAllProducts(); // ... 剩余代码 }; export default Menu;
同时修改getAllProducts,确保连接逻辑在缓存时也能正确执行:
export const getAllProducts = async (): Promise<IMenuItem[]> => { // 不需要unstable_noStore await db.connect(); try { const products = await Product.find().sort({ name: 1 }).lean(); return JSON.parse(JSON.stringify(products)); } finally { // 使用finally确保即使查询出错也能断开连接 await db.disconnect(); } };
不过这种方案还是不如方案1高效,因为频繁断开重连会增加数据库负担。
方案3:在构建时预取所有数据,避免多语言页面重复请求
在generateStaticParams中预取数据,然后传递给页面组件,这样所有语言页面复用同一份数据,避免多次调用getAllProducts:
export async function generateStaticParams() { const products = await getAllProducts(); return ['en', 'es', 'fr'].map(lang => ({ lang, props: { products }, // 传递预取的数据 })); } const Menu = async ({ params, props }: { params: { lang: string }, props: { products: IMenuItem[] } }) => { const { products } = props; // ... 剩余代码 }; export default Menu;
这种方式下,getAllProducts只执行一次,构建时所有语言页面复用这份数据,既保留SSG缓存,又避免多次连接数据库。
内容的提问来源于stack exchange,提问作者jm3

