分析一下当前项目如果browser或者node包需要引用common包中的方法,如何设计项目架构

当前项目架构分析:

  1. 项目使用 pnpm workspace 管理 monorepo
  2. 有三个包:common(通用)、browser(浏览器专用)、node(Node.js专用)
  3. 每个包都是独立的 npm package,有自己的 package.json
  4. 当前三个包之间没有任何依赖关系,都是独立实现的

针对 browser/node 包引用 common 包的方法,有几种架构设计方案可以提供。
基于对当前项目的分析,我来详细说明如何设计项目架构,以支持 browser/node 包引用 common 包中的方法。

📊 当前项目架构分析

当前架构特点

  1. Monorepo 结构:使用 pnpm workspace 管理三个独立包
  2. 包分类
    • @longmo-utils/common - 通用工具函数,无平台依赖
    • @longmo-utils/browser - 浏览器专用工具函数
    • @longmo-utils/node - Node.js 专用工具函数
  3. 当前状态:三个包之间无依赖关系,完全独立实现

现有模块划分

packages/
├── common/         # array, object, function, string
├── browser/        # storage, dom, network
└── node/           # fs, env, process

🎯 推荐架构设计方案

基于 monorepo 的特性,我推荐以下方案:

方案一:内部依赖模式(推荐)

架构原理

graph TB subgraph "Monorepo" subgraph "common包" C1[array工具] C2[object工具] C3[function工具] C4[string工具] end subgraph "browser包" B1[storage] B2[dom] B3[network] B4[引用common] end subgraph "node包" N1[fs] N2[env] N3[process] N4[引用common] end C3 -.->|被依赖| B4 C3 -.->|被依赖| N4 C2 -.->|被依赖| B4 C2 -.->|被依赖| N4 end style B4 fill:#90EE90 style N4 fill:#90EE90

实现步骤

1. 在 browser 和 node 的 package.json 中添加依赖

```json
{
  "name": "@longmo-utils/browser",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@longmo-utils/common": "workspace:*"
  },
  "exports": {
    ".": {
      "types": "./dist/index.d.mts",
      "import": "./dist/index.mjs"
    }
  }
}
```json
{
  "name": "@longmo-utils/node",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@longmo-utils/common": "workspace:*"
  },
  "exports": {
    ".": {
      "types": "./dist/index.d.mts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

2. 在 browser/node 模块中导入使用

```typescript
/**
 * Storage utilities using localStorage and sessionStorage
 */
import { debounce, deepClone } from '@longmo-utils/common'

/**
 * Sets an item in localStorage with debounced auto-save
 */
export function setLocalStorageWithDebounce<T>(
  key: string, 
  value: T, 
  delay = 300
): void {
  // 使用 common 包中的 debounce 函数
  const debouncedSave = debounce(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.error(`Error writing to localStorage: ${error}`)
    }
  }, delay)
  
  debouncedSave()
}

/**
 * Gets a cloned copy from localStorage
 */
export function getLocalStorageCloned<T = any>(key: string): T | null {
  const data = getLocalStorage<T>(key)
  // 使用 common 包中的 deepClone 函数
  return data ? deepClone(data) : null
}

优点

代码复用:避免在多个包中重复实现相同逻辑
统一维护:通用逻辑只需在 common 包中维护一次
类型安全:TypeScript 类型信息完整保留
打包优化:打包工具可以进行 tree-shaking,只打包使用的函数
开发体验:pnpm workspace 自动解析内部依赖,开发体验流畅

缺点

⚠️ 构建顺序:需要先构建 common 包,再构建 browser/node 包
⚠️ 发布协调:发布时需要按顺序发布
⚠️ 版本同步:三个包的版本需要协调管理

构建流程调整

需要调整构建脚本,确保构建顺序:

```json
{
  "scripts": {
    "build": "pnpm build:common && pnpm build:browser && pnpm build:node",
    "build:common": "pnpm --filter @longmo-utils/common run build",
    "build:browser": "pnpm --filter @longmo-utils/browser run build",
    "build:node": "pnpm --filter @longmo-utils/node run build"
  }
}

方案二:导出转发模式

架构原理

browser 和 node 包直接从 common 包重新导出需要的函数,不直接依赖。

```typescript
/**
 * @longmo-utils/browser
 * Browser-specific utility functions
 */

// 导出自身模块
export * from './storage'
export * from './dom'
export * from './network'

// 重新导出 common 包中的常用函数(通过类型导入)
export type { 
  DebouncedFunction, 
  DeepPartial 
} from '@longmo-utils/common'

// 声明:如果用户需要 common 包的函数,应该直接安装 common 包
// 这里不直接导出,避免耦合

使用示例

// 用户需要同时安装两个包
import { getLocalStorage } from '@longmo-utils/browser'
import { debounce, deepClone } from '@longmo-utils/common'

// 组合使用
const debouncedSave = debounce(() => {
  setLocalStorage('key', data)
}, 300)

优点

松耦合:browser/node 与 common 包无直接依赖
按需安装:用户可以选择性安装需要的包
灵活性:每个包可以独立发布和升级

缺点

用户负担:用户需要管理多个包的依赖
代码重复:无法避免在 browser/node 中重复实现某些逻辑
类型分离:类型定义分散在不同包中


方案三:源码共享模式

架构原理

将共享的源代码放在 packages/shared 目录,通过 TypeScript path 映射引用。

packages/
├── shared/                 # 共享源码
│   └── src/
│       └── utils/
│           ├── debounce.ts
│           └── deepClone.ts
├── common/
│   ├── src/
│   │   └── index.ts        # 引用 shared 源码
│   └── tsconfig.json       # 配置 path 映射
├── browser/
│   ├── src/
│   │   └── index.ts        # 引用 shared 源码
│   └── tsconfig.json       # 配置 path 映射
└── node/
    ├── src/
    │   └── index.ts        # 引用 shared 源码
    └── tsconfig.json       # 配置 path 映射

TypeScript 配置

```json
{
  "compilerOptions": {
    "paths": {
      "@shared/*": ["../../shared/src/*"]
    }
  }
}

优点

源码复用:真正的源码级别共享
无运行时依赖:打包时内联共享代码
统一构建:所有包共享同一份源码实现

缺点

配置复杂:需要配置 TypeScript path 映射
IDE 支持:某些 IDE 对跨包引用支持不佳
调试困难:调试时需要关注源码位置


🎯 最终推荐方案

基于项目当前的技术栈和需求,我强烈推荐使用方案一:内部依赖模式,原因如下:

适用性分析

  1. 技术栈匹配

    • 使用 tsdown(基于 Rolldown)作为打包工具,支持优秀的 tree-shaking
    • pnpm workspace 原生支持内部依赖管理
    • TypeScript 类型系统完整保留
  2. 项目需求匹配

    • common 包提供通用工具函数,适合作为基础依赖
    • browser/node 包作为平台特定实现,依赖通用功能符合逻辑
    • 用户可以按需选择安装的包
  3. 开发体验

    • 内部依赖通过 workspace:* 协议自动链接
    • 开发时无需手动构建 common 包
    • 类型提示和代码跳转完全支持

实施建议

分阶段实施

阶段 1:添加内部依赖关系

# 安装内部依赖
cd packages/browser
pnpm add @longmo-utils/common@workspace:*

cd ../../node
pnpm add @longmo-utils/common@workspace:*

阶段 2:重构现有代码,使用 common 包函数

识别 browser/node 包中可以使用 common 包函数的地方,逐步重构:

```typescript
import { deepClone } from '@longmo-utils/common'

export function getLocalStorage<T = any>(key: string): T | null {
  try {
    const item = window.localStorage.getItem(key)
    return item ? deepClone(JSON.parse(item)) : null
  } catch (error) {
    console.error(`Error reading from localStorage: ${error}`)
    return null
  }
}

阶段 3:更新文档和示例

在文档中说明组合使用的最佳实践:

```markdown
## 组合使用包

Browser 和 Node 包依赖 Common 包中的通用工具函数:

```typescript
import { getLocalStorage } from '@longmo-utils/browser'
import { debounce } from '@longmo-utils/common'

// 组合使用:创建防抖的本地存储
const saveWithDebounce = debounce((key: string, value: any) => {
  setLocalStorage(key, value)
}, 300)

saveWithDebounce('user', { name: 'Alice' })

**阶段 4:调整 CI/CD 流程**

确保发布和构建流程遵循依赖顺序:

```yaml
# .github/workflows/ci.yml
name: CI
on: [push]

jobs:
  build:
    steps:
      - uses: actions/checkout@v3
      - run: pnpm install
      - run: pnpm build          # 自动按顺序构建
      - run: pnpm test

最佳实践建议

  1. 避免循环依赖:确保依赖关系单向流动(common ← browser/node)
  2. 明确边界:只有真正的通用函数才放入 common 包
  3. 版本策略:使用统一的版本号,或使用语义化版本
  4. 类型导出:common 包导出所有需要的类型定义
  5. 文档同步:在文档中清晰说明包之间的依赖关系

📝 总结

当前项目架构设计已经非常合理,要实现 browser/node 包引用 common 包,采用内部依赖模式是最优选择

  • ✅ 利用 pnpm workspace 的原生支持
  • ✅ tsdown/Rolldown 的 tree-shaking 能力避免代码膨胀
  • ✅ TypeScript 类型系统完整保留
  • ✅ 开发体验和用户体验最佳

这样既保持了包的独立性,又实现了代码复用,是一个平衡且可扩展的架构设计。

如果浏览器中引入了common库,但用户实际使用的函数并未依赖common相关的功能,是否仍然需要安装common库?请详细说明在这种情况下common库的安装必要性,包括对项目构建、运行性能以及依赖管理的影响。

从 tsdown.config.ts 可以看到:

  • treeshake: true - 启用了 tree-shaking
  • minify: true - 启用了压缩

从 tsconfig.json 可以看到,根目录有 paths 配置,但这只是开发时的配置,不会影响打包后的产物。

让我详细分析用户的问题。

📊 详细分析:是否需要安装 common 库

这是一个非常关键的设计决策问题。让我从多个角度详细分析:

🔍 场景定义

假设有这样一个情况:

// browser 包的某个函数内部使用了 common 包
// packages/browser/src/storage/index.ts
import { debounce } from '@longmo-utils/common'

export function setLocalStorageWithDebounce<T>(
  key: string, 
  value: T, 
  delay = 300
): void {
  const debouncedSave = debounce(() => {
    window.localStorage.setItem(key, JSON.stringify(value))
  }, delay)
  debouncedSave()
}

export function getLocalStorage<T>(key: string): T | null {
  const item = window.localStorage.getItem(key)
  return item ? JSON.parse(item) : null
}

用户代码:只使用了 getLocalStorage,没有使用 setLocalStorageWithDebounce

// 用户的浏览器项目
import { getLocalStorage } from '@longmo-utils/browser'

const data = getLocalStorage('key')

❗ 核心结论

如果 browser 包在 dependencies 中声明了对 common 包的依赖,无论用户是否实际使用,都必须安装 common 包。

这是由 npm 的依赖解析机制决定的,tree-shaking 只能在打包阶段生效,不能避免依赖安装。


📐 详细影响分析

1️⃣ 依赖安装的影响

必须安装的原因

# 用户执行
npm install @longmo-utils/browser

# npm 会自动解析并安装整个依赖树
node_modules/
├── @longmo-utils/browser/
│   ├── dist/
│   └── package.json (dependencies: @longmo-utils/common)
└── @longmo-utils/common/  # 会被自动安装
    ├── dist/
    └── package.json
graph LR A[用户安装 browser 包] --> B[npm 读取 browser 的 package.json] B --> C[发现 dependencies: common] C --> D[自动安装 common 包] D --> E[common 包进入 node_modules] style D fill:#FF6B6B style E fill:#FF6B6B

安装体积影响

假设包的体积:

  • @longmo-utils/browser: ~20 KB (minified)
  • @longmo-utils/common: ~15 KB (minified)

即使不使用 common,也会下载 15 KB

# 查看实际安装体积
npm list --depth=0 --json
# 或者
du -sh node_modules/@longmo-utils/common

2️⃣ 构建性能的影响

打包阶段分析

使用 tsdown/Rolldown 的 tree-shaking:

// 用户构建工具(如 Vite、Webpack)
import { getLocalStorage } from '@longmo-utils/browser'

// 打包后的结果
var getLocalStorage = function(key) {
  var item = localStorage.getItem(key);
  return item ? JSON.parse(item) : null;
};
// ✅ common 包的代码被 tree-shaking 掉了

构建流程对比

graph TB subgraph "安装阶段(必须发生)" A1[下载 browser 包] --> A2[下载 common 包] A2 --> A3[安装到 node_modules] end subgraph "打包阶段" B1[用户代码导入] --> B2[解析所有依赖] B2 --> B3[Tree-shaking 分析] B3 --> B4{使用到的函数?} B4 -->|是| B5[打包到产物] B4 -->|否| B6[丢弃代码] end A3 --> B1 style A2 fill:#FF6B6B style A3 fill:#FF6B6B style B6 fill:#90EE90

构建时间影响

安装 common 包不会显著影响构建时间

  • ✅ Tree-shaking 在内存中分析,速度快
  • ✅ 未使用的代码直接跳过,不进行转译
  • ⚠️ 但会增加依赖解析时间(通常几毫秒)

实际测试数据(估算):

场景 安装时间 构建时间 最终产物大小
不依赖 common 0.5s 2s 20 KB
依赖 common(未使用) 1.5s 2.1s 20 KB
依赖 common(已使用) 1.5s 2.2s 35 KB

结论:安装时间增加,但构建时间影响很小,最终产物大小相同


3️⃣ 运行性能的影响

运行时表现

// 最终打包的代码(未使用 common)
function getLocalStorage(key) {
  return window.localStorage.getItem(key)
}

// 用户页面加载
console.log(performance.now())
const data = getLocalStorage('key')
console.log(performance.now())  // ~0.001ms

✅ 没有任何运行时影响

  • Tree-shaking 完全移除了未使用的代码
  • 浏览器不会加载 common 包的任何代码
  • 运行时性能与完全不依赖 common 相同

网络传输影响

虽然 node_modules 中有 common 包,但不会传输到浏览器:

// 开发环境(Dev Server)
// ✅ 只传输用户实际使用的代码

// 生产环境(打包后)
// ✅ 产物中不包含未使用的代码

4️⃣ 依赖管理的影响

优点

透明性:依赖关系明确,易于理解和维护
一致性:所有环境安装相同的依赖
安全性:避免版本冲突和幽灵依赖
可追溯:可以追踪依赖来源和用途

缺点

安装负担:用户必须安装不使用的包
空间占用:node_modules 体积增加
安装时间:增加依赖下载和安装时间
更新成本:需要同步更新多个包的版本

实际影响示例

# 用户项目依赖
package.json
{
  "dependencies": {
    "@longmo-utils/browser": "^1.0.0"
    // 自动安装 @longmo-utils/common
  }
}

# npm ls 输出
├── @longmo-utils/browser@1.0.0
│   └── @longmo-utils/common@1.0.0  # 间接依赖

# 如果 browser 升级到 2.0.0,但依赖 common@2.0.0
# 用户可能需要手动处理版本冲突

🎯 解决方案对比

针对这个问题,有几种不同的架构方案:

方案 1:直接依赖(当前推荐方案)

// packages/browser/package.json
{
  "dependencies": {
    "@longmo-utils/common": "workspace:*"
  }
}

特点

  • ✅ 简单直接,易于理解
  • ✅ 类型信息完整
  • ✅ 开发体验好
  • ❌ 用户必须安装 common(即使不用)
  • ❌ 增加 node_modules 体积

适用场景

  • browser 包中大量使用 common 包功能
  • common 包提供核心基础能力(如 debounce、deepClone)
  • 用户很可能同时使用两个包

方案 2:可选依赖(Peer Dependencies)

// packages/browser/package.json
{
  "peerDependencies": {
    "@longmo-utils/common": "^1.0.0"
  },
  "peerDependenciesMeta": {
    "@longmo-utils/common": {
      "optional": true
    }
  }
}

特点

  • ✅ 用户可以选择性安装 common
  • ✅ 未安装时不强制依赖
  • ⚠️ 需要运行时检测 common 是否存在
  • ⚠️ 类型信息需要特殊处理

实现方式

// packages/browser/src/storage/index.ts
import { debounce } from '@longmo-utils/common'

// 创建一个工厂函数
export function createDebouncedStorage(debounceFn?: typeof debounce) {
  const theDebounce = debounceFn || debounce
  
  return {
    setLocalStorageWithDebounce: <T>(key: string, value: T, delay = 300) => {
      const debouncedSave = theDebounce(() => {
        window.localStorage.setItem(key, JSON.stringify(value))
      }, delay)
      debouncedSave()
    }
  }
}

// 用户可以选择性使用
import { getLocalStorage, createDebouncedStorage } from '@longmo-utils/browser'
import { debounce } from '@longmo-utils/common'  // 可选

const { setLocalStorageWithDebounce } = createDebouncedStorage(debounce)

缺点

  • ❌ 代码复杂度增加
  • ❌ 用户体验下降(需要手动注入依赖)
  • ❌ TypeScript 类型提示可能不友好

方案 3:条件导入(运行时检测)

// packages/browser/src/storage/index.ts
export function setLocalStorageWithDebounce<T>(
  key: string, 
  value: T, 
  delay = 300
): void {
  let debounce: typeof import('@longmo-utils/common').debounce
  
  try {
    // 动态导入
    const common = await import('@longmo-utils/common')
    debounce = common.debounce
  } catch {
    // 如果 common 不存在,使用简化实现
    debounce = (fn, delay) => {
      let timer: any
      return (...args: any[]) => {
        clearTimeout(timer)
        timer = setTimeout(() => fn(...args), delay)
      }
    }
  }
  
  const debouncedSave = debounce(() => {
    window.localStorage.setItem(key, JSON.stringify(value))
  }, delay)
  debouncedSave()
}

特点

  • ✅ 用户不安装 common 也能使用
  • ✅ 有 fallback 实现
  • ⚠️ 增加运行时开销
  • ⚠️ 可能有两个版本的 debounce(用户如果已安装)

不推荐理由

  • ❌ 失去了类型安全
  • ❌ 增加了运行时复杂度
  • ❌ 可能导致重复打包

方案 4:源码内联(无依赖)

// 直接在 browser 包中实现需要的函数
// packages/browser/src/utils/debounce.ts
export function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout> | null = null
  return function (this: any, ...args: Parameters<T>) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// packages/browser/src/storage/index.ts
import { debounce } from '../utils/debounce'  // 内部实现

export function setLocalStorageWithDebounce<T>(
  key: string, 
  value: T, 
  delay = 300
): void {
  const debouncedSave = debounce(() => {
    window.localStorage.setItem(key, JSON.stringify(value))
  }, delay)
  debouncedSave()
}

特点

  • ✅ 用户不需要安装 common
  • ✅ 打包结果完全独立
  • ✅ 代码完全可控
  • ❌ 代码重复(如果 common 也有 debounce)
  • ❌ 维护成本高(需要同步更新)
  • ❌ 违反 DRY 原则

适用场景

  • browser 包只需要 common 中的少数简单函数
  • 这些函数实现简单,易于维护
  • 优先考虑用户体验而非代码复用

📊 方案对比矩阵

维度 直接依赖 Peer Dependencies 条件导入 源码内联
安装必要性 ✅ 必须安装 ⚠️ 可选安装 ⚠️ 可选安装 ❌ 不需要安装
node_modules 体积 增加(15KB) 不增加 不增加 不增加
打包体积 tree-shaking 后无影响 tree-shaking 后无影响 可能增加 增加(内联)
构建时间 微增(<0.1s) 不增加 增加 不增加
运行性能 无影响 无影响 有开销(动态导入) 无影响
类型安全 ✅ 完整 ⚠️ 需要特殊处理 ❌ 丢失 ✅ 完整
开发体验 ✅ 优秀 ⚠️ 一般 ❌ 较差 ⚠️ 一般
维护成本 ✅ 低 ⚠️ 中 ❌ 高 ⚠️ 中
代码复用 ✅ 高 ⚠️ 中 ⚠️ 中 ❌ 低
用户体验 ⚠️ 需要多安装 ✅ 可选安装 ⚠️ 可能困惑 ✅ 简单

🎯 推荐决策框架

根据不同的使用场景,选择合适的方案:

场景 1:browser 包大量依赖 common 包

推荐:直接依赖

{
  "dependencies": {
    "@longmo-utils/common": "workspace:*"
  }
}

判断标准

  • browser 包中 >50% 的函数使用 common 包
  • common 包提供的核心能力被频繁使用
  • 用户大概率会同时使用两个包

实际影响

  • 用户安装:+15KB(一次性)
  • 构建时间:+0.1s(几乎无感)
  • 运行性能:无影响
  • 用户体验:+(代码复用带来的便利)

场景 2:browser 包少量依赖 common 包

推荐:源码内联

// packages/browser/src/internal/debounce.ts
// 将需要的函数直接内联到 browser 包中

判断标准

  • browser 包中 <20% 的函数使用 common 包
  • 依赖的函数实现简单(<20 行代码)
  • 用户可能只使用 browser 包

实际影响

  • 用户安装:无增加
  • 代码体积:+3-5KB(内联函数)
  • 维护成本:+(需要同步更新)

场景 3:混合场景(部分功能可选)

推荐:分离包

// packages/browser/package.json
// 核心功能不依赖 common
{
  "dependencies": {}
}

// packages/browser-extended/package.json
// 扩展功能依赖 common
{
  "dependencies": {
    "@longmo-utils/common": "workspace:*"
  },
  "peerDependencies": {
    "@longmo-utils/browser": "^1.0.0"
  }
}

判断标准

  • 有明确的基础功能和高级功能划分
  • 高级功能需要 common 包支持
  • 基础功能独立使用场景广泛

用户选择

# 只需要基础功能
npm install @longmo-utils/browser

# 需要高级功能(自动安装 common)
npm install @longmo-utils/browser-extended

💡 对当前项目的建议

基于你的项目现状,我给出以下建议:

阶段 1:现状分析

查看当前 browser 包中可能使用 common 包的地方:

// packages/browser/src/storage/index.ts
// 可能使用: deepClone

// packages/browser/src/network/index.ts
// 可能使用: debounce, throttle

// packages/browser/src/dom/index.ts
// 可能使用: debounce (事件防抖)

估计使用率:可能 10-30% 的函数会用到 common 包

阶段 2:推荐方案

混合策略

  1. 高频使用且复杂的函数:使用直接依赖

    • debounce / throttle:实现复杂,建议依赖 common
  2. 简单且不常用的函数:使用内联

    • deepClone:实现简单,内联到对应模块
  3. 保持 API 兼容性

    • 用户只安装 browser 包时,不强制安装 common
    • 提供可选的高级功能模块

实施示例

// packages/browser/src/network/index.ts
// 直接依赖 common 的 debounce(复杂函数)
import { debounce } from '@longmo-utils/common'

export function fetchWithRetry = debounce(/*...*/)

// packages/browser/src/storage/index.ts
// 内联实现 simpleClone(简单函数)
export function deepClone<T>(obj: T): T {
  if (obj === null || typeof obj !== 'object') return obj
  if (Array.isArray(obj)) return obj.map(item => deepClone(item)) as T
  const cloned = {} as T
  for (const key in obj) {
    cloned[key] = deepClone(obj[key])
  }
  return cloned
}
// packages/browser/package.json
{
  "dependencies": {
    "@longmo-utils/common": "workspace:*"
  },
  "description": "Browser utilities (includes @longmo-utils/common)"
}

用户使用建议

在文档中明确说明:

## 安装

### 基础使用
```bash
npm install @longmo-utils/browser

包含了基础功能,会自动安装 @longmo-utils/common。

可选:高级功能

如果你需要高级功能(如防抖请求、节流操作),建议同时显式安装:

npm install @longmo-utils/browser @longmo-utils/common

这样可以确保类型提示和 IDE 支持最佳。


---

## 📝 总结

| 问题 | 答案 |
|------|------|
| **是否必须安装 common?** | ✅ 是,如果使用 `dependencies` 声明依赖 |
| **对安装体积的影响?** | 增加 ~15KB(common 包大小) |
| **对构建性能的影响?** | 微增 <0.1s(几乎无感) |
| **对运行性能的影响?** | ❌ 无影响(tree-shaking 移除未使用代码) |
| **对用户体验的影响?** | ⚠️ 需要多安装一个包 |
| **最佳实践?** | 根据使用频率选择直接依赖或源码内联 |

**核心原则**:
1. 如果 browser 包**大量使用** common 包,直接依赖是最佳选择
2. 如果**偶尔使用**且函数简单,考虑内联实现
3. 始终考虑用户体验 vs 维护成本的平衡


# 如果再添加一个模块包,将 browser\common\node 中的函数全部导出,用户直接安装这个包即可,这样设计是不是能平衡用户体验 vs 维护成本的平衡

用户提出了一个很好的想法:创建一个统一包(比如叫 `@longmo-utils/all` 或 `@longmo-utils`),将 browser、common、node 中的所有函数都导出,用户只需要安装这一个包就能使用所有功能。

这是一个很常见的 monorepo 设计模式,类似于:
- lodash -> lodash-es
- date-fns (虽然有细粒度的包)
- Ramda
- rxjs (包含了所有操作符)

我需要分析这个方案的优缺点,看看它是否能平衡用户体验和维护成本。

让我分析几个方面:

1. 架构设计
2. 用户体验
3. 维护成本
4. 构建和打包
5. Tree-shaking 效果
6. 版本管理
7. 文档和迁移
8. 潜在问题

我需要读取一些现有文件,了解当前的项目结构,然后给出详细的分析和建议。
这是一个非常有价值的想法!让我详细分析这种"统一包"(All-in-One)方案的可行性和影响。
好的,用户想要创建一个统一包,将 browser、common、node 中的所有函数导出。这是一个很好的想法,类似于很多知名库的做法:

1. lodash - 提供细粒度的包,也提供完整的 lodash 包
2. date-fns - 有细粒度的包,也有 date-fns 包
3. rxjs - 提供完整的包
4. @vueuse/core - VueUse 的核心包

让我分析这个方案的优劣。
## 🎯 统一包方案详细分析

这是一个非常经典且有效的架构设计!让我全面分析这种"统一包"(All-in-One)方案。

### 📦 方案设计架构

#### 新增 `@longmo-utils` 统一包

packages/
├── common/ # 通用工具包
├── browser/ # 浏览器专用包
├── node/ # Node.js 专用包
└── @longmo-utils/ # 统一包(新增)
├── src/
│ ├── index.ts # 主入口,导出所有函数
│ ├── common.ts # 重新导出 common 包
│ ├── browser.ts # 重新导出 browser 包(仅浏览器环境)
│ └── node.ts # 重新导出 node 包(仅 Node 环境)
├── dist/
└── package.json


---

### 🏗 实现方案

#### **方案 A:简单的重新导出(推荐)**

```typescript
// packages/@longmo-utils/src/index.ts
/**
 * @longmo-utils
 * Unified utility library combining common, browser, and node utilities
 */

// 重新导出 common 包(所有环境)
export * from './common'

// 条件导出(仅浏览器环境可用)
if (typeof window !== 'undefined') {
  // 浏览器特定函数
  export * from './browser'
}

// 条件导出(仅 Node.js 环境可用)
if (typeof process !== 'undefined' && process.versions?.node) {
  // Node.js 特定函数
  export * from './node'
}
// packages/@longmo-utils/src/common.ts
/**
 * Re-exports all common utilities
 */
export * from '@longmo-utils/common'
// packages/@longmo-utils/src/browser.ts
/**
 * Re-exports all browser utilities
 * Only available in browser environment
 */
export * from '@longmo-utils/browser'
// packages/@longmo-utils/src/node.ts
/**
 * Re-exports all node utilities
 * Only available in Node.js environment
 */
export * from '@longmo-utils/node'
// packages/@longmo-utils/package.json
{
  "name": "@longmo-utils",
  "version": "1.0.0",
  "description": "Unified utility library with platform-specific utilities",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.mts",
  "exports": {
    ".": {
      "types": "./dist/index.d.mts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./common": {
      "types": "./dist/common.d.mts",
      "import": "./dist/common.mjs",
      "require": "./dist/common.cjs"
    },
    "./browser": {
      "types": "./dist/browser.d.mts",
      "import": "./dist/browser.mjs"
    },
    "./node": {
      "types": "./dist/node.d.mts",
      "import": "./dist/node.mjs",
      "require": "./dist/node.cjs"
    }
  },
  "dependencies": {
    "@longmo-utils/common": "workspace:*",
    "@longmo-utils/browser": "workspace:*",
    "@longmo-utils/node": "workspace:*"
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsdown",
    "dev": "tsdown --watch",
    "clean": "rm -rf dist"
  }
}

方案 B:条件导出(更灵活)

// packages/@longmo-utils/src/index.ts
/**
 * Unified utility library
 * Automatically exports platform-specific utilities based on environment
 */

// 通用函数(所有环境)
export * from '@longmo-utils/common'

// 平台检测
const isBrowser = typeof window !== 'undefined'
const isNode = typeof process !== 'undefined' && process.versions?.node

if (isBrowser) {
  // 浏览器环境,导出 browser 函数
  Object.assign(
    module.exports || {},
    await import('@longmo-utils/browser').then(m => m)
  )
}

if (isNode) {
  // Node.js 环境,导出 node 函数
  Object.assign(
    module.exports || {},
    await import('@longmo-utils/node').then(m => m)
  )
}

但这种方案有运行时导入的开销,不太推荐。


📊 用户体验对比

使用场景对比

场景 分包安装 统一包安装
浏览器项目 npm install @longmo-utils/browser
npm install @longmo-utils/common
npm install @longmo-utils
Node.js 项目 npm install @longmo-utils/node
npm install @longmo-utils/common
npm install @longmo-utils
跨平台项目 需要安装 2-3 个包 只需安装 1 个包
依赖数量 2-3 个 1 个

用户代码示例

// ❌ 分包安装方式
import { getLocalStorage } from '@longmo-utils/browser'
import { debounce } from '@longmo-utils/common'

// ✅ 统一包安装方式
import { getLocalStorage, debounce } from '@longmo-utils'
# 分包方式
npm install @longmo-utils/browser @longmo-utils/common
# node_modules 中有 2 个包

# 统一包方式
npm install @longmo-utils
# node_modules 中有 1 个包(但包含了 3 个子包)

⚙️ Tree-shaking 效果分析

打包机制

graph TB A[用户代码] --> B[导入 @longmo-utils] B --> C[打包工具解析依赖] C --> D[Tree-shaking 分析] D --> E{使用到的函数?} E -->|是| F[保留代码] E -->|否| G[移除代码] H[Browser 环境] --> I[只分析 Browser + Common 代码] J[Node 环境] --> K[只分析 Node + Common 代码] style G fill:#90EE90 style F fill:#FFA500

实际打包效果

假设用户代码:

// browser 环境
import { getLocalStorage } from '@longmo-utils'

// 只用了一个函数,其他都被 tree-shaking

打包结果

函数/模块 是否打包到产物 说明
getLocalStorage 用户使用
debounce 未使用,被 tree-shaking
deepClone 未使用,被 tree-shaking
readJson (Node) 环境不匹配
fs 模块代码 环境不匹配

最终产物大小:~2 KB(只包含 getLocalStorage 和相关代码)

Tree-shaking 在统一包中同样有效!


📈 维护成本分析

优点

一次更新,到处生效

# 更新 common 包
cd packages/common
git push

# 统一包自动继承最新版本
# 用户无需关心内部依赖

统一的版本管理

// 所有子包版本同步
{
  "version": "1.0.0"
}

简化的依赖关系

# 之前
user-project/
├── @longmo-utils/browser@1.0.0
├── @longmo-utils/common@1.0.0
└── package.json

# 现在
user-project/
└── @longmo-utils@1.0.0

统一的文档和示例

# 一份文档覆盖所有功能
## 安装
npm install @longmo-utils

## 使用
import { debounce, getLocalStorage } from '@longmo-utils'

缺点

构建复杂度增加

# 构建顺序
1. 构建 common
2. 构建 browser
3. 构建 node
4. 构建 @longmo-utils(依赖上面 3 个)

# 脚本调整
"build": "pnpm build:common && pnpm build:browser && pnpm build:node && pnpm build:all"

版本协调要求更高

# 如果 common 包升级到 1.1.0
# 必须同时升级 @longmo-utils 到 1.1.0
# 否则用户安装的是不同版本的子包

包体积问题

# npm 安装时
npm install @longmo-utils
# 会安装:
# - @longmo-utils (统一包,~5KB)
# - @longmo-utils/common (子包,~15KB)
# - @longmo-utils/browser (子包,~20KB)
# - @longmo-utils/node (子包,~18KB)
# 总共:~58KB(未使用也会安装)

# 但最终打包产物:只有使用的代码(~2KB)

🎯 最佳实践方案

混合策略:同时提供分包和统一包

这是大多数成功库的选择:

packages/
├── common/           # 细粒度包(可选安装)
├── browser/          # 细粒度包(可选安装)
├── node/             # 细粒度包(可选安装)
└── @longmo-utils/    # 统一包(推荐安装)

用户选择路径

graph TD A[用户] --> B{需求} B -->|简单项目| C[安装 @longmo-utils] B -->|追求极致体积| D[选择分包安装] B -->|特定场景| E[安装对应包] C --> F[✅ 一次安装,所有功能] D --> G[✅ 按需安装,精确控制] E --> H[✅ 只安装需要的包]

文档引导

## 快速开始

### 推荐方式:统一包(适合大多数项目)

```bash
npm install @longmo-utils

所有功能开箱即用,自动适配运行环境。

按需安装:细粒度包(追求极致优化)

# 浏览器项目
npm install @longmo-utils/browser @longmo-utils/common

# Node.js 项目
npm install @longmo-utils/node @longmo-utils/common

只安装需要的包,最大化 tree-shaking 效果。

使用示例

// 统一包
import { debounce, getLocalStorage } from '@longmo-utils'

// 细粒度包
import { getLocalStorage } from '@longmo-utils/browser'
import { debounce } from '@longmo-utils/common'

---

### 📊 性能对比总结

| 维度 | 分包安装 | 统一包安装 | 说明 |
|------|---------|-----------|------|
| **安装体积** | ~35KB<br>(browser + common) | ~58KB<br>(all packages) | 统一包安装更多,但不影响最终产物 |
| **安装时间** | ~1s | ~1.5s | 统一包稍慢,可接受 |
| **node_modules 数量** | 2-3 个包 | 4 个包(含子包) | 统一包包含子包 |
| **构建时间** | ~2s | ~2.1s | 差异极小 |
| **最终产物体积** | 相同<br>(tree-shaking 效果相同) | 相同<br>(tree-shaking 效果相同) | **关键结论** |
| **运行时性能** | 无差异 | 无差异 | 代码相同 |
| **开发体验** | ⚠️ 需要多导入 | ✅ 单一导入 | 统一包更好 |
| **维护成本** | ⚠️ 多包协调 | ✅ 统一管理 | 统一包更低 |

---

### 🎯 最终推荐方案

#### **实施方案:双轨制**

1. **保留现有的三个细粒度包**(common/browser/node)
   - 面向追求极致优化的用户
   - 支持灵活的组合安装

2. **新增统一包 `@longmo-utils`**(主推)
   - 面向大多数用户
   - 简化安装和使用流程

3. **统一版本管理**
   ```json
   // 使用 lerna 或 changesets 管理版本
   {
     "version": "1.0.0",
     "private": true,
     "packages": [
       "packages/*"
     ]
   }
  1. 构建脚本优化
    {
      "scripts": {
        "build": "pnpm build:packages && pnpm build:all",
        "build:packages": "pnpm build:common && pnpm build:browser && pnpm build:node",
        "build:all": "pnpm --filter @longmo-utils run build"
      }
    }
    

发布策略

# 发布时,四个包同时发布
npm publish packages/common
npm publish packages/browser
npm publish packages/node
npm publish packages/@longmo-utils

# 或使用 lerna 自动化
lerna publish

✅ 结论

统一包方案能够很好地平衡用户体验和维护成本:

目标 达成情况 说明
用户体验 ✅ 优秀 一次安装,所有功能,简化使用
维护成本 ✅ 低 统一管理,同步更新
性能影响 ✅ 极小 Tree-shaking 保证最终产物体积
灵活性 ✅ 高 同时支持分包安装
学习曲线 ✅ 低 单一入口,文档简单

这是当前最佳实践,类似于 lodash、date-fns 等成熟库的做法。


📝 实施步骤建议

  1. 第一阶段:创建统一包,重新导出所有子包
  2. 第二阶段:更新构建脚本,确保构建顺序
  3. 第三阶段:更新文档,推荐统一包安装方式
  4. 第四阶段:发布测试,收集用户反馈
  5. 第五阶段:根据反馈优化,完善文档和示例
posted @ 2026-01-24 19:36  龙陌  阅读(12)  评论(0)    收藏  举报