@libeilong/lazy-singleton
v0.1.0
Published
一个零依赖、仅 ~500 字节的微型工具库,用于创建延迟初始化的单例。它可以从根本上解决 JavaScript/TypeScript 项目中的循环依赖问题,并提供一流的开发体验,包括对 Vite 和 Webpack 的状态保留热重载(HMR)支持。
Readme
lazy-singleton
一个零依赖、仅 ~500 字节的微型工具库,用于创建延迟初始化的单例。它可以从根本上解决 JavaScript/TypeScript 项目中的循环依赖问题,并提供一流的开发体验,包括对 Vite 和 Webpack 的状态保留热重载(HMR)支持。
😩 您是否曾陷入这样的困境?
在大型项目中,模块间的依赖关系变得复杂:stores 依赖 api,api 依赖 request,而 request 又需要在出错时调用 stores 的方法。
// stores/index.ts
import { api } from '../api'
// ...
// api/index.ts
import { request } from '../request'
// ...
// request.ts
import { stores } from '../stores' // <-- 循环依赖!这会导致在模块加载时,其中一个模块拿到的是 undefined,最终在浏览器中抛出致命错误:Uncaught TypeError: Cannot read properties of undefined。
lazy-singleton 通过将模块的初始化推迟到第一次实际使用时,优雅地打破了这个死结。
✨ 核心特性
🌀 轻松解决循环依赖:通过延迟加载,从根本上消除模块加载时的初始化冲突。
⚡️ 延迟初始化 (Lazy):只在第一次访问实例时才执行初始化代码,可以减少应用的启动时间。
📦 单例保证:确保在应用的整个生命周期中,一个实例只被创建一次。
🔥 状态保留热重载 (HMR):可选地在 Vite 和 Webpack 的热更新之间保留实例状态,提供极致的开发体验。
🌐 环境无关:可在 Node.js、Vite、Webpack 和浏览器中无缝工作。
💪 健壮透明:基于原生 Proxy 实现,自动处理 this 绑定,instanceof 和 Object.keys 等操作均符合预期。
📦 零依赖,体积微小:干净、轻量,可无负担地添加到任何项目中。
🚀 安装
npm install lazy-singleton
# or
yarn add lazy-singleton
# or
pnpm add lazy-singleton💡 基本用法
只需用 lazy() 函数包裹您的实例创建逻辑即可。
之前 (有循环依赖风险):
// logger.ts
class LoggerService {
constructor() {
console.log('Logger initialized!')
}
log(message: string) {
/*...*/
}
}
export const logger = new LoggerService()之后 (安全且延迟加载):
// logger.ts
import { lazy } from 'lazy-singleton'
class LoggerService {
constructor() {
// 这段代码只会在第一次调用 `logger.log()` 时执行
console.log('Logger initialized!')
}
log(message: string) {
/*...*/
}
}
export const logger = lazy(() => new LoggerService())现在,即使其他模块与 logger.ts 存在循环依赖,应用也能正常工作。
🛠️ 高级 API
lazy 函数接受一个可选的 options 对象,用于开启高级功能。
lazy<T>(initializer: () => T, options?: Options): T
interface Options {
/**
* 用于 HMR 状态保留的唯一键。
* 如果提供,将在 Vite/Webpack 热更新之间尝试保留实例。
*/
hmrKey?: string;
/**
* 是否在控制台打印初始化和 HMR 日志,便于调试。
*/
debug?: boolean;
}🔥 状态保留热重载 (HMR)
这是 lazy-singleton 的王牌功能。在前端开发中,我们希望修改代码后,应用的状态(如用户登录信息)能够被保留。
使用 hmrKey 来开启此功能:
// src/stores/index.ts
import { lazy } from 'lazy-singleton'
import { UserStore } from './userStore'
import { AppStore } from './appStore'
export const stores = lazy(
() => {
return {
user: new UserStore(),
app: new AppStore(),
}
},
{
// 提供一个唯一的 key
hmrKey: 'stores',
// 开启调试日志
debug: true,
},
)现在,当您修改 UserStore.ts 或任何相关文件时:
- Vite/Webpack 会进行热模块替换。
- lazy-singleton 会自动保存旧模块的 stores 实例,并在新模块加载后恢复它。
- 您在控制台会看到
[lazy:stores] ✅ Instance restored from HMR.的日志。 - 您的应用状态得以保留,无需重新登录或刷新页面!
最佳实践:仅为需要保留状态的模块(如 stores)开启 hmrKey。对于无状态的模块(如 api 客户端、router 实例),不提供 hmrKey,让它们在热更新时重新创建以确保代码总是最新的。
🎯 场景示例
1. 前端应用 (Vite/React)
完美解决 stores, api, router 之间的复杂依赖关系。
// src/stores/index.ts
// 状态管理中心,需要保留 HMR 状态
export const stores = lazy(() => ({
user: new UserStore(),
app: new AppStore(),
}), { hmrKey: 'global-stores' });
// src/api/index.ts
import * as user from './user';
// API 客户端,无状态,总是希望使用最新的代码
export const API = lazy(() => ({
user,
// ...
}));
// src/router.ts
// 路由实例,无状态
export const router = lazy(() => createBrowserRouter(...));2. 后端应用 (Node.js/Express)
在 Node.js 中,它同样可以解决服务间的循环依赖,并能延迟初始化昂贵的资源,如数据库连接。
// src/services.ts
import { lazy } from 'lazy-singleton'
import { UserService } from './user.service'
import { AuthService } from './auth.service'
// AuthService 和 UserService 互相依赖
export const services = lazy(() => ({
user: new UserService(),
auth: new AuthService(),
}))
// src/database.ts
import { createPool } from 'mysql2/promise'
// 数据库连接池只在第一次数据库操作时才被创建
export const db = lazy(() => {
console.log('Creating database connection pool...')
return createPool({
/* config */
})
})🔧 它是如何工作的?
lazy-singleton 返回一个原生 JavaScript Proxy 对象。这个代理对象在内部持有一个尚未初始化的实例。当您第一次尝试访问代理对象的任何属性时(例如 stores.user),Proxy 会拦截该访问,执行您传入的 initializer 函数来创建原始实例,将其缓存,然后再将访问请求转发给这个真实的实例。后续的所有访问都将直接使用缓存的实例。
HMR 状态保留功能是通过 Vite (import.meta.hot) 和 Webpack (module.hot) 提供的 HMR API 实现的。
