NextJS 13中间件中Singleton构造函数多次执行的问题与解决
Next.js 中单例类构造函数重复执行的问题分析与解决
问题重现
定义了如下单例类 Paths:
export default class Paths { private static _instance: Paths; private constructor() { console.log('paths constructor'); } public static get Instance() { console.log(this._instance); return this._instance || (this._instance = new this()); } }
在Next.js中间件中使用该单例:
import { NextRequest, NextResponse } from 'next/server'; import Paths from './lib/features/paths'; export default async function middleware(req: NextRequest) { const paths = Paths.Instance; return NextResponse.next(); }
刷新页面时,服务器控制台重复输出:
undefined paths constructor
说明Paths的构造函数被多次执行,单例逻辑失效。
原因分析
- 开发模式热重载影响:Next.js开发服务器(
next dev)默认启用Fast Refresh特性,当相关文件变更(甚至部分无文件变更的场景),模块会被重新编译加载,导致Paths类的静态属性_instance被重置为undefined,每次请求都会触发新实例创建。 - Edge Runtime上下文隔离:Next.js中间件默认运行在Edge Runtime环境,该环境为保证请求隔离,可能会在不同执行上下文加载模块,导致类的静态属性无法跨请求共享。
- 模块缓存机制失效:尽管Node.js默认缓存模块,但Next.js开发模式或Edge Runtime的特殊处理,可能绕过模块缓存,使得
Paths模块被多次加载,每次加载都会重新初始化类的静态成员。
解决方案
方案1:改用模块级单例(推荐)
将单例逻辑从类的静态属性改为模块导出的单例对象,利用模块缓存机制保证单例唯一性:
// paths.ts class Paths { constructor() { console.log('paths constructor'); } // 在此添加类的业务方法 } const pathsInstance = new Paths(); export default pathsInstance;
中间件中直接导入使用:
import { NextRequest, NextResponse } from 'next/server'; import paths from './lib/features/paths'; export default async function middleware(req: NextRequest) { // 直接使用已初始化的paths实例 return NextResponse.next(); }
模块级导出的实例会被缓存,只要缓存生效,无论模块被加载多少次,都只会创建一次实例。
方案2:利用全局对象存储实例(仅Node.js环境适用)
若中间件运行在Node.js Runtime(而非默认Edge Runtime),可将实例存储在全局对象中,避免模块重载导致的重置:
export default class Paths { private static _instance: Paths; private constructor() { console.log('paths constructor'); } public static get Instance() { // 检查全局对象中是否存在实例 if (globalThis._pathsInstance) { return globalThis._pathsInstance as Paths; } this._instance = new this(); globalThis._pathsInstance = this._instance; return this._instance; } } // 补充全局类型定义(可选) declare global { var _pathsInstance: Paths | undefined; }
注意:该方案不适用于Edge Runtime,因为Edge环境限制了全局对象的使用。
方案3:关闭开发模式Fast Refresh(不推荐)
可在next.config.js中关闭Fast Refresh避免模块重载,但会牺牲开发体验:
/** @type {import('next').NextConfig} */ const nextConfig = { fastRefresh: false, }; module.exports = nextConfig;
补充说明
生产环境下,Next.js会对模块进行打包优化,模块缓存机制正常工作,基于类的静态单例可能恢复正常,但仍推荐使用模块级单例保证跨环境一致性。
内容的提问来源于stack exchange,提问作者Nicola Scionti
相关产品推荐
相关产品推荐

