Skip to content

Latest commit

 

History

History
348 lines (276 loc) · 12.5 KB

File metadata and controls

348 lines (276 loc) · 12.5 KB

English | 中文

Visual Edit (SWC)

什么是 Visual Edit?

一个基于 SWC (Rust/WebAssembly) 的 React 可视化编辑器,支持在浏览器中直接编辑组件样式和文本,修改会自动写回源代码。

为什么选择 SWC 而不是 Babel?

本项目是 Babel 版本Rust/WASM 移植版。Babel 版本使用 JavaScript 进行 AST 转换,而本版本利用:

  • 🦀 原生性能 - 用 Rust 编写的 SWC 插件,编译为 WebAssembly
  • ⚡️ Turbopack 兼容 - 与 Next.js Turbopack 配合获得更快的开发体验
  • 🔧 编译时集成 - 插件在 SWC 转换阶段运行

说明

本仓库是一个技术概念验证,展示如何使用 Rust/WASM 开发 SWC 插件。

功能特性

  • 可视化编辑 - 在浏览器中直接编辑组件样式和文本
  • 实时预览 - 修改立即生效
  • 内联样式 - 样式以 style={{ ... }} 形式写入源代码
  • 代码回写 - 保存时自动更新源文件
  • 热模块替换 - 无需刷新页面
  • 动态内容保护 - 包含状态的元素会被标记为不可编辑

技术方案

核心原理

  1. 编译时注入(Rust/WASM) - 使用 Rust 编写的自定义 SWC 插件,编译为 wasm32-wasip1 目标,在编译期间向每个 JSX 元素注入唯一的 data-vid 标识符

  2. 全局索引定位 - 使用全局元素计数器(如 page.tsx:h1:6)唯一标识元素,索引代表元素在深度优先遍历顺序中的位置

  3. 运行时编辑 - React 组件通过 data-vid 在浏览器中识别可编辑元素,样式修改直接操作 DOM 实现实时预览

  4. 后端 AST 回写 - 保存时,Next.js API 路由使用 @swc/core 重新解析源文件,通过全局索引定位目标节点并更新 style 属性

  5. 热更新同步 - 文件写入触发 Next.js HMR,实现无缝页面更新

整体流程图

┌─────────────────────────────────────────────────────────────────┐
│                编译时 (SWC Rust 插件 → WASM)                     │
├─────────────────────────────────────────────────────────────────┤
│  JSX 源码                                                        │
│  <h1 className="title">Hello</h1>                               │
│                           ↓                                      │
│  SWC 插件 (Rust) - VisitMut trait                               │
│                           ↓                                      │
│  visit_mut_jsx_element() → 注入 data-vid                        │
│                           ↓                                      │
│  <h1 className="title" data-vid="page.tsx:h1:6">Hello</h1>      │
│                                                                  │
│  VID = 文件名:元素名:全局索引                                     │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                       运行时 (浏览器)                            │
├─────────────────────────────────────────────────────────────────┤
│  用户点击元素                                                     │
│        ↓                                                         │
│  获取 data-vid → 显示编辑面板(右侧抽屉)                          │
│        ↓                                                         │
│  用户修改样式 → 实时预览(直接修改 el.style)                      │
│        ↓                                                         │
│  点击保存 → POST /api/visual-edit                                │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                后端处理 (Next.js API Route)                      │
├─────────────────────────────────────────────────────────────────┤
│  接收请求 { vid, styles, newText }                               │
│        ↓                                                         │
│  解析 VID → 获取文件名、元素名、全局索引                           │
│        ↓                                                         │
│  读取源文件 → 使用 @swc/core 解析                                 │
│        ↓                                                         │
│  使用全局索引遍历 AST → 找到目标元素                               │
│        ↓                                                         │
│  更新 style 属性(与现有样式合并)                                 │
│        ↓                                                         │
│  swc.print() → 写入文件 → 触发 HMR                               │
└─────────────────────────────────────────────────────────────────┘

快速开始

前置要求

  • Node.js 18+
  • Rust 工具链(用于插件开发)
  • wasm32-wasip1 目标:rustup target add wasm32-wasip1

安装运行

# 安装依赖
npm install

# 启动开发服务器(Turbopack)
npm run dev

访问 http://localhost:3000

构建 SWC 插件(如需修改 Rust 代码)

cd swc-plugin
cargo build --target wasm32-wasip1 --release
cp target/wasm32-wasip1/release/swc_plugin.wasm ../swc_plugin.wasm

项目结构

react-visual-edit-swc/
├── app/
│   ├── api/
│   │   └── visual-edit/
│   │       └── route.ts           # 保存编辑的 API 端点
│   ├── components/
│   │   └── VisualEditOverlay.tsx  # 前端编辑界面(React)
│   ├── layout.tsx                 # 根布局
│   ├── page.tsx                   # 演示页面
│   └── globals.css                # 全局样式
├── swc-plugin/                    # Rust SWC 插件
│   ├── src/
│   │   └── lib.rs                 # 插件实现
│   └── Cargo.toml                 # Rust 依赖
├── swc_plugin.wasm                # 编译后的 WASM 插件
├── next.config.mjs                # 包含 SWC 插件的 Next.js 配置
├── package.json
└── tsconfig.json

核心模块详解

1. SWC Rust 插件 (swc-plugin/src/lib.rs)

插件实现 VisitMut trait 来遍历和修改 JSX 元素:

impl VisitMut for VisualEditVisitor {
    fn visit_mut_jsx_element(&mut self, node: &mut JSXElement) {
        // 获取元素名称(如 "h1"、"div"、"Image")
        let element_name = self.get_element_name(&node.opening.name);

        // 生成 VID:文件名:元素名:全局索引
        let vid = self.make_vid(&element_name);

        // 注入 data-vid 属性
        node.opening.attrs.push(self.create_data_vid_attr(&vid));

        // 继续遍历子元素
        node.visit_mut_children_with(self);
    }
}

关键点:

  • 使用 swc_core v50.2.3(兼容 Next.js 16.1)
  • 编译目标为 wasm32-wasip1
  • 全局索引计数器为每个 JSX 元素递增(深度优先顺序)

VID 格式文件名:元素名:全局索引

  • 示例:page.tsx:h1:6 表示 page.tsx 中的第 7 个 JSX 元素,是一个 h1

2. Next.js 配置 (next.config.mjs)

const nextConfig = {
  experimental: {
    swcPlugins: [
      [
        resolvePluginPath('swc_plugin.wasm'),  // Turbopack 和 Webpack 路径不同
        {}
      ]
    ]
  }
}

Turbopack 兼容性:

  • Turbopack 需要相对路径(./swc_plugin.wasm
  • Webpack 需要绝对路径
  • 配置会自动检测并调整

3. API 路由 (app/api/visual-edit/route.ts)

使用 @swc/core 进行后端 AST 修改:

// 解析 VID
const vidInfo = parseVid(vid)  // { filename, elementName, index }

// 使用 SWC 解析源码
const ast = await swc.parse(sourceCode, { syntax: 'typescript', tsx: true })

// 通过全局索引查找元素(与插件遍历顺序一致)
let globalIndex = 0
function visitNode(node) {
  if (node.type === 'JSXElement') {
    if (globalIndex === targetIndex && elementName === targetElementName) {
      // 修改 style 属性
      modifyStyle(node.opening, newStyles)
    }
    globalIndex++  // 每个 JSX 元素都递增
    // 继续遍历子元素...
  }
}

// 生成并写入代码
const output = await swc.print(ast, { minify: false })
fs.writeFileSync(filePath, output.code)

4. 前端编辑界面 (app/components/VisualEditOverlay.tsx)

用于编辑 UI 的 React 组件:

// 状态管理
const [enabled, setEnabled] = useState(false)
const [currentElement, setCurrentElement] = useState<HTMLElement | null>(null)
const [styles, setStyles] = useState<StyleConfig>({...})

// 核心函数
highlight(el)           // 高亮悬停元素
checkHasState(el)       // 通过 React Fiber 检测动态内容
openPanel() / closePanel()  // 带 body margin 动画的右侧抽屉
handleSave()            // POST 到 /api/visual-edit

动态内容检测:

// 检查 React Fiber 中的动态子元素
const fiber = el[reactFiberKey]
if (Array.isArray(fiber.memoizedProps.children)) {
  // 包含动态内容如 {count}
  showToast('该元素包含动态内容,无法直接编辑')
  return
}

数据流

编辑保存流程

用户点击保存
      ↓
收集数据:{ vid, styles, newText }
      ↓
POST /api/visual-edit
      ↓
解析 VID → { filename: "page.tsx", elementName: "h1", index: 6 }
      ↓
读取源文件 → swc.parse() 转为 AST
      ↓
使用全局计数器遍历 AST → 找到索引为 6 的 JSX 元素
      ↓
修改/创建 style 属性:style={{ color: "...", ... }}
      ↓
swc.print() → 写入文件
      ↓
Next.js HMR 检测变化 → 页面更新

请求/响应格式

请求体:

interface EditRequest {
  vid: string           // "page.tsx:h1:6"
  newText?: string      // 新文本内容
  styles?: {
    color?: string
    backgroundColor?: string
    fontSize?: string   // 不带 "px"
    fontWeight?: string
  }
}

响应:

{
  "ok": true,
  "message": "源代码已更新",
  "data": { "vid": "...", "styles": {...}, "filePath": "..." }
}

技术栈

类别 技术
框架 Next.js 16 (App Router)
UI 框架 React 19
编译器 SWC (Turbopack)
插件语言 Rust → WebAssembly
插件目标 wasm32-wasip1
SWC Core 版本 swc_core v50.2.3
AST 操作 @swc/core (Node.js)
样式 Tailwind CSS 4

与 Babel 版本对比

特性 Babel 版本 SWC 版本
插件语言 JavaScript Rust → WASM
AST 库 @babel/parser swc_core
构建工具 Vite Next.js + Turbopack
编译性能 基准 ~20 倍提升
VID 策略 AST 路径 全局索引
运行时 Node.js WASM (Wasip1)

注意事项

  1. 仅开发模式 - 编辑功能仅在运行 npm run dev 时可用
  2. 版本控制 - 修改会直接写入源文件,建议使用 Git
  3. 动态内容 - 包含 {state} 表达式的元素无法编辑(会显示 Toast 提示)
  4. 文本元素 - 仅支持 p、span、button、h1-h6、a、label、div 等文本元素
  5. SWC 版本兼容 - 插件需要 swc_core v47-51 才能兼容 Next.js 16.1

许可证

MIT