Files
wukuang/重构计划-25-12.md
T
2026-05-23 14:05:22 +08:00

5.3 KiB

Monorepo Refactoring Plan: Decoupling Designer, Engines, and Materials

This document outlines the step-by-step plan to refactor the current form-designer monolithic structure into a clean, decoupled Monorepo architecture.

Core Philosophy: Separation of Concerns. The Designer acts as a shell, Engines enforce logic, and Materials provide UI implementation.


🏗 Architecture Overview

Layer Package Responsibility
Foundation @lingshu/types Shared interfaces, DSL definitions, and constants.
@lingshu/core-utils Pure JS utilities (framework agnostic).
Logic Engines @lingshu/user-script Runtime for standard User Scripts (Hooks, Actions) and Rule Engine (Low-code logical linkage).
Materials @lingshu/widget-pc PC Vue components, widget configurations, and property panels.
@lingshu/widget-mobile Mobile Vue components (Future).
Host/Editor @lingshu/form-designer The visual editor shell. Implements adapters for engines and exposes extension points for materials.
Application apps/lcdp The assembler entry point that wires everything together.

📅 Phased Execution Plan

The refactoring is divided into 3 autonomous phases. At the end of each phase, the project MUST be buildable and runnable.

Phase 1: Foundation Construction (Infrastructure)

Goal: Establish a shared language (types) to break circular dependencies and prepare for code migration.

  • Boundary: No functional logic changes. Only moving definitions and constants.
  • Deliverable: Codebase uses @lingshu/types for shared entities instead of relative imports.
  • Verification:
    1. pnpm build passes for all packages.
    2. npm run dev in form-designer works exactly as before.

Tasks:

  1. Define Shared Types: Extract WidgetSchema, UserScriptContext, RuntimeEventApiBO to @lingshu/types.
  2. Define Injection Keys: Extract USER_SCRIPT_EVENT_BUS_KEY to @lingshu/types.
  3. Refactor Imports: Update form-designer and widget-pc to import from @lingshu/types.

Phase 2: Logic Engine Extraction (The Brain)

Goal: Isolate the "User Script" and "Rule Engine" logic into a standalone package that doesn't depend on UI stores directly.

  • Boundary: packages/user-script contains pure logic. Access to Store/API is done via Dependency Injection.
  • Deliverable: A new @lingshu/user-script package. form-designer initializes this engine by injecting its internal state adapters.
  • Verification:
    1. User Scripts (e.g., onClick logs) work in the Designer preview.
    2. Rule Linkages (e.g., input A changes -> input B hides) work in the Designer preview.

Tasks:

  1. Create Package: Set up packages/user-script workspace.
  2. Migrate Logic: Move src/config/user-script (Definitions) and src/utils/user-script (Runtime) to the new package.
  3. Refactor for DI: Transform functions that directly import PageStore to accept context or adapter arguments.
  4. Wire Up: In form-designer/main.ts (or boot sequence), call initScriptEngine(adapters) from the new package.

Phase 3: Material Decoupling (The Body)

Goal: Move all PC-specific widget configurations and implementations out of the Designer.

  • Boundary: form-designer becomes unaware of specific widgets. It loads whatever is passed to its .use() method.
  • Deliverable: packages/widget-pc contains all material definitions (config/*) and components components.
  • Verification:
    1. Designer starts empty initially (theoretically).
    2. After injecting WidgetPC, the left palette shows PC components.
    3. Dragging components to canvas works correctly.

Tasks:

  1. Move Configs: Relocate form-designer/src/config (Materials, Settings) to packages/widget-pc/src/config.
  2. Refactor Hooks: Move specific widget hooks (like components/anchor/useCompEvent) to widget-pc.
  3. Plugin Architecture: create a registerWidgets export in widget-pc.
  4. Injection: In the App Entry/Main, import registerWidgets and pass it to the Designer instance.

🔄 Dependency Injection Strategy (Crucial)

To achieve decoupling, we use standard Dependency Injection.

1. Service Injection (Logic Layer) Instead of importing axios or pinia in user-script:

// packages/user-script/src/index.ts
export function initEngine(context: IScriptContext) {
  // context.network.request(...)
  // context.store.getFieldValue(...)
}

2. Event Injection (Material Layer) Widgets use inject to communicate with the engine, without importing it.

// packages/widget-pc/src/hooks/useCompEvent.ts
const bus = inject(USER_SCRIPT_EVENT_BUS_KEY);
bus.emit("CLIENT_EVENT", payload);