npm package: @libeilong/lazy-singleton
Description: 一个零依赖、仅 ~500 字节的微型工具库，用于创建延迟初始化的单例。它可以从根本上解决 JavaScript/TypeScript 项目中的循环依赖问题，并提供一流的开发体验，包括对 Vite 和 Webpack 的状态保留热重载（HMR）支持。
Author: lblblong
Latest version: 0.1.0

# lazy-singleton

一个零依赖、仅 ~500 字节的微型工具库，用于创建延迟初始化的单例。它可以从根本上解决 JavaScript/TypeScript 项目中的循环依赖问题，并提供一流的开发体验，包括对 Vite 和 Webpack 的状态保留热重载（HMR）支持。

## 😩 您是否曾陷入这样的困境？

在大型项目中，模块间的依赖关系变得复杂：stores 依赖 api，api 依赖 request，而 request 又需要在出错时调用 stores 的方法。

```typescript
// 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 等操作均符合预期。

📦 **零依赖，体积微小**：干净、轻量，可无负担地添加到任何项目中。

## 🚀 安装

```bash
npm install lazy-singleton
# or
yarn add lazy-singleton
# or
pnpm add lazy-singleton
```

## 💡 基本用法

只需用 `lazy()` 函数包裹您的实例创建逻辑即可。

**之前 (有循环依赖风险):**

```typescript
// logger.ts
class LoggerService {
  constructor() {
    console.log('Logger initialized!')
  }
  log(message: string) {
    /*...*/
  }
}
export const logger = new LoggerService()
```

**之后 (安全且延迟加载):**

```typescript
// 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` 对象，用于开启高级功能。

```typescript
lazy<T>(initializer: () => T, options?: Options): T

interface Options {
  /**
   * 用于 HMR 状态保留的唯一键。
   * 如果提供，将在 Vite/Webpack 热更新之间尝试保留实例。
   */
  hmrKey?: string;

  /**
   * 是否在控制台打印初始化和 HMR 日志，便于调试。
   */
  debug?: boolean;
}
```

## 🔥 状态保留热重载 (HMR)

这是 lazy-singleton 的王牌功能。在前端开发中，我们希望修改代码后，应用的状态（如用户登录信息）能够被保留。

使用 `hmrKey` 来开启此功能：

```typescript
// 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 之间的复杂依赖关系。

```typescript
// 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 中，它同样可以解决服务间的循环依赖，并能延迟初始化昂贵的资源，如数据库连接。

```typescript
// 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 实现的。