VISALL改造记录

层级 当前方案 引入 Turborepo 影响兼容性
任务调度 npm scripts Turborepo ❌ 不影响
编译工具 Rollup + esbuild + Babel 保持不变 ✅ 完全兼容
兼容性处理 Babel + polyfills 保持不变 ✅ 完全兼容
输出格式 ESM/UMD 保持不变 ✅ 完全兼容

pnpm workspace 基础

当前状态分析

VISALL项目目前配置了npm workspace,但未真正使用pnpm workspace:

现状:

  • package.json中有"workspaces": ["packages/*"]配置
  • 使用package-lock.json,子包有独立node_modules
  • 实际是npm workspace,不是pnpm workspace

npm workspace vs pnpm workspace 核心区别

特性 npm workspace pnpm workspace
依赖管理机制 符号链接 + 实际文件 纯符号链接 + 内容寻址存储
磁盘空间使用 依赖可能重复,占用大 严格去重,节省50-70%空间
安装速度 中等(有重复下载) 快(去重+硬链接)
依赖解析 外部依赖可能重复安装 外部依赖严格去重
配置方式 package.json中配置 pnpm-workspace.yaml文件

实际结构对比

当前npm workspace结构:

1
2
3
4
5
6
7
VISALL/
├── node_modules/ (1.4GB)
├── packages/
│ ├── table/node_modules/ (重复依赖)
│ ├── text/node_modules/ (重复依赖)
│ └── timeline/node_modules/ (重复依赖)
└── package-lock.json

理想的pnpm workspace结构:

1
2
3
4
5
6
7
8
9
10
VISALL/
├── node_modules/ (~500MB,去重后)
│ ├── .pnpm/ (硬链接存储)
│ └── table -> symlink
├── packages/
│ ├── table/ (无node_modules)
│ ├── text/ (无node_modules)
│ └── timeline/ (无node_modules)
├── pnpm-lock.yaml
└── pnpm-workspace.yaml

命令对比

功能 npm workspace pnpm workspace
安装所有依赖 npm install pnpm install
安装到特定包 npm install lodash -w table pnpm add lodash --filter table
运行脚本 npm run build -w table pnpm run --filter table build
查看依赖树 npm ls pnpm why <package>

对VISALL的影响

当前问题:

  • 1.4GB磁盘空间占用过大
  • 子包依赖重复安装
  • 安装速度慢

pnpm workspace优势:

  • 节省50-70%磁盘空间(约700MB-1GB)
  • 更快的安装速度
  • 更好的依赖管理

迁移方案

1
2
3
4
5
6
7
8
# 1. 清理现有结构
rm -rf packages/*/node_modules packages/*/package-lock.json

# 2. 创建pnpm配置
echo "packages:\n - 'packages/*'" > pnpm-workspace.yaml

# 3. 安装依赖
pnpm install

流程

pnpm workspace

构建工具层面:

  • scripts/esbuild-watch.cjs 脚本完全可用

  • concurrently 并行执行功能正常

  • cross-env 环境变量设置正常

  • browsersync 热重载功能正常

Turborepo

当前构建流程

graph TD
    Start([开始构建]) --> PreCheck{预检查}
    PreCheck -->|✅ 通过| TestPhase["测试阶段<br/>~15秒"]
    PreCheck -->|❌ 失败| Error1[构建失败]
    
    TestPhase --> Jest["Jest 单元测试<br/>jest --passWithNoTests"]
    Jest -->|✅ 测试通过| TypePhase["类型生成阶段<br/>~25秒"]
    Jest -->|❌ 测试失败| Error2["构建失败"]
    
    TypePhase --> TSC["TypeScript 编译器<br/>tsc --emitDeclarationOnly"]
    TSC --> TypeFiles["生成类型定义<br/>types/*.d.ts"]
    TypeFiles --> MainBuild["主项目打包阶段<br/>~45秒"]
    
    MainBuild --> RollupConfig["加载 Rollup 配置<br/>rollup.config.js"]
    RollupConfig --> InputFiles["读取入口文件<br/>src/index.ts<br/>src/index.common.ts"]
    
    InputFiles --> Transform["代码转换管道"]
    Transform --> TSCompile["TypeScript 编译<br/>esbuild + babel"]
    TSCompile --> ModuleResolve["模块解析<br/>@rollup/plugin-node-resolve"]
    ModuleResolve --> CommonJSTransform["CommonJS 转换<br/>@rollup/plugin-commonjs"]
    CommonJSTransform --> AssetProcess["资源处理<br/>@rollup/plugin-url"]
    AssetProcess --> StyleProcess["样式处理<br/>rollup-plugin-styles"]
    StyleProcess --> Optimize["代码优化<br/>Tree Shaking + Terser"]
    
    Optimize --> MainOutput["主项目输出"]
    MainOutput --> ESMBundle["bundle.esm.js<br/>bundle.esm.min.js"]
    MainOutput --> UMDBundle["bundle.umd.js<br/>bundle.umd.min.js"]
    MainOutput --> CommonBundle["common.esm.js<br/>common.umd.js"]
    
    MainBuild --> ParallelPackages["子包并行构建<br/>~35秒"]
    
    ParallelPackages --> TablePkg["Table 包构建<br/>~12秒"]
    ParallelPackages --> DonutPkg["Operational-Donut 包<br/>~8秒"]
    ParallelPackages --> TimelinePkg["Timeline 包<br/>~10秒"]
    ParallelPackages --> TextPkg["Text 包<br/>~5秒"]
    
    TablePkg --> TableRollup["Rollup 构建<br/>rollup -c"]
    TableRollup --> TableOutput["dist/index.js<br/>dist/index.esm.js<br/>dist/index.d.ts"]
    
    DonutPkg --> ViteBuild["Vite 构建<br/>vite build"]
    ViteBuild --> DonutOutput["Vite 优化产物"]
    
    TimelinePkg --> TimelineRollup["Rollup 构建<br/>rollup -c"]
    TimelineRollup --> TimelineOutput["Timeline 产物"]
    
    TextPkg --> TextRollup["Rollup 构建<br/>rollup -c"]
    TextRollup --> TextOutput["Text 产物"]
    
    TableOutput --> Success["构建成功<br/>总耗时: ~120秒"]
    DonutOutput --> Success["构建成功<br/>总耗时: ~120秒"]
    TimelineOutput --> Success["构建成功<br/>总耗时: ~120秒"]
    TextOutput --> Success["构建成功<br/>总耗时: ~120秒"]
    ESMBundle --> Success["构建成功<br/>总耗时: ~120秒"]
    UMDBundle --> Success["构建成功<br/>总耗时: ~120秒"]
    CommonBundle --> Success["构建成功<br/>总耗时: ~120秒"]
    TypeFiles --> Success["构建成功<br/>总耗时: ~120秒"]
    
    Success --> Publish["是否发布?"]
    Publish -->|是| PublishFlow["发布流程<br/>pnpm publish --recursive"]
    Publish -->|否| End(["构建完成"])
    PublishFlow --> End
    
    %% 样式定义
    classDef phaseBox fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef toolBox fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    classDef outputBox fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
    classDef errorBox fill:#ffebee,stroke:#b71c1c,stroke-width:2px
    classDef timeBox fill:#fff3e0,stroke:#e65100,stroke-width:2px
    
    class TestPhase,TypePhase,MainBuild,ParallelPackages phaseBox
    class Jest,TSC,RollupConfig,TSCompile,ModuleResolve,CommonJSTransform,AssetProcess,StyleProcess,Optimize,TableRollup,ViteBuild,TimelineRollup,TextRollup toolBox
    class TypeFiles,ESMBundle,UMDBundle,CommonBundle,TableOutput,DonutOutput,TimelineOutput,TextOutput outputBox
    class Error1,Error2 errorBox
    class Success timeBox

Turborepo流程图

graph TD
    subgraph Current [当前构建方式 - 串行执行 120秒]
        S1[开始] --> T1["测试阶段<br/>15秒"]
        T1 --> TY1["类型生成<br/>25秒"]
        TY1 --> M1["主项目打包<br/>45秒"]
        M1 --> P1["Table包构建<br/>12秒"]
        P1 --> P2["Donut包构建<br/>8秒"]
        P2 --> P3["Timeline包构建<br/>10秒"]
        P3 --> P4["Text包构建<br/>5秒"]
        P4 --> E1["构建完成<br/>总计: 120秒"]
    end
    
    subgraph Optimized [Turborepo优化后 - 并行+缓存 40秒]
        S2[开始] --> Cache["检查缓存"]
        Cache -->|缓存命中| Skip["跳过构建<br/>~2秒"]
        Cache -->|缓存未命中| T2["测试阶段<br/>8秒"]
        
        T2 --> TY2["类型生成<br/>5秒"]
        TY2 --> M2["主项目打包<br/>15秒"]
        
        M2 --> PT1["Table包<br/>5秒"]
        M2 --> PT2["Donut包<br/>3秒"]
        M2 --> PT3["Timeline包<br/>4秒"]
        M2 --> PT4["Text包<br/>2秒"]
        
        PT1 --> E2["构建完成<br/>总计: 40秒"]
        PT2 --> E2["构建完成<br/>总计: 40秒"]
        PT3 --> E2["构建完成<br/>总计: 40秒"]
        PT4 --> E2["构建完成<br/>总计: 40秒"]
        Skip --> E2["构建完成<br/>总计: 40秒"]
    end
    
    subgraph Benefits [性能提升效果]
        B1["🚀 首次构建<br/>120s → 80s<br/>⬇️ 33%"]
        B2["⚡ 增量构建<br/>45s → 6s<br/>⬇️ 87%"]
        B3["💾 缓存命中<br/>120s → 2s<br/>⬇️ 98%"]
        B4["🔄 并行执行<br/>串行 → 4路并行<br/>⬆️ 400%"]
    end
    
    %% 连接线
    E1 -.->|优化对比| B1
    E2 -.->|性能提升| B2
    
    %% 样式定义
    classDef currentBox fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#000
    classDef optimizedBox fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#000
    classDef benefitBox fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000
    classDef parallelNode fill:#e1bee7,stroke:#8e24aa,stroke-width:2px,color:#000
    classDef cacheNode fill:#bbdefb,stroke:#1976d2,stroke-width:2px,color:#000
    
    class S1,T1,TY1,M1,P1,P2,P3,P4,E1 currentBox
    class S2,T2,TY2,M2,E2 optimizedBox
    class PT1,PT2,PT3,PT4 parallelNode
    class Cache,Skip cacheNode
    class B1,B2,B3,B4 benefitBox

“构建” = 完整的代码转换和打包流程:

  1. 代码验证:测试 + 类型检查

  2. 代码转换:TypeScript → JavaScript

  3. 模块打包:多文件 → 单一/多个输出文件

  4. 资源优化:压缩、Tree shaking、资源内联

  5. 格式输出:生成多种模块格式(ESM/UMD)

这就是为什么 120秒的构建时间成为瓶颈,也是 Turborepo 能够显著优化的原因 - 通过缓存和并行执行避免重复的转换和打包操作。

🚫 为什么不是可选的?

1. 构建性能已成为瓶颈

  • 120秒的构建时间严重影响开发效率

  • 没有缓存机制,重复工作严重

  • CI/CD 时间过长影响部署

2. 开发体验问题突出

  • 跨包调试困难

  • 热更新慢且不稳定

  • 新人上手成本高(1个月)

3. 项目正在快速发展

  • 50+图表组件持续增长

  • 团队规模扩大

  • 技术债务累积

🎯 总结

对于 VISALL 项目,Turborepo 不是可选的优化,而是必需的基础设施。原因如下:

  1. 项目规模:13个包的 monorepo 已达到必需使用的规模

  2. 性能瓶颈:120秒构建时间严重影响开发效率

  3. 发展趋势:项目持续增长,问题会进一步恶化

  4. 成本效益:实施成本2个月,收益立即且持续

建议:立即开始 Turborepo 改造,按照项目已有的10周计划执行。这不是一个技术选型问题,而是一个项目发展的必然需求。