English | 中文
一个基于 SWC (Rust/WebAssembly) 的 React 可视化编辑器,支持在浏览器中直接编辑组件样式和文本,修改会自动写回源代码。
本项目是 Babel 版本 的 Rust/WASM 移植版。Babel 版本使用 JavaScript 进行 AST 转换,而本版本利用:
- 🦀 原生性能 - 用 Rust 编写的 SWC 插件,编译为 WebAssembly
- ⚡️ Turbopack 兼容 - 与 Next.js Turbopack 配合获得更快的开发体验
- 🔧 编译时集成 - 插件在 SWC 转换阶段运行
本仓库是一个技术概念验证,展示如何使用 Rust/WASM 开发 SWC 插件。
- 可视化编辑 - 在浏览器中直接编辑组件样式和文本
- 实时预览 - 修改立即生效
- 内联样式 - 样式以
style={{ ... }}形式写入源代码 - 代码回写 - 保存时自动更新源文件
- 热模块替换 - 无需刷新页面
- 动态内容保护 - 包含状态的元素会被标记为不可编辑
-
编译时注入(Rust/WASM) - 使用 Rust 编写的自定义 SWC 插件,编译为
wasm32-wasip1目标,在编译期间向每个 JSX 元素注入唯一的data-vid标识符 -
全局索引定位 - 使用全局元素计数器(如
page.tsx:h1:6)唯一标识元素,索引代表元素在深度优先遍历顺序中的位置 -
运行时编辑 - React 组件通过
data-vid在浏览器中识别可编辑元素,样式修改直接操作 DOM 实现实时预览 -
后端 AST 回写 - 保存时,Next.js API 路由使用
@swc/core重新解析源文件,通过全局索引定位目标节点并更新style属性 -
热更新同步 - 文件写入触发 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 devcd swc-plugin
cargo build --target wasm32-wasip1 --release
cp target/wasm32-wasip1/release/swc_plugin.wasm ../swc_plugin.wasmreact-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
插件实现 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_corev50.2.3(兼容 Next.js 16.1) - 编译目标为
wasm32-wasip1 - 全局索引计数器为每个 JSX 元素递增(深度优先顺序)
VID 格式:文件名:元素名:全局索引
- 示例:
page.tsx:h1:6表示 page.tsx 中的第 7 个 JSX 元素,是一个h1
const nextConfig = {
experimental: {
swcPlugins: [
[
resolvePluginPath('swc_plugin.wasm'), // Turbopack 和 Webpack 路径不同
{}
]
]
}
}Turbopack 兼容性:
- Turbopack 需要相对路径(
./swc_plugin.wasm) - Webpack 需要绝对路径
- 配置会自动检测并调整
使用 @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)用于编辑 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 版本 | SWC 版本 |
|---|---|---|
| 插件语言 | JavaScript | Rust → WASM |
| AST 库 | @babel/parser | swc_core |
| 构建工具 | Vite | Next.js + Turbopack |
| 编译性能 | 基准 | ~20 倍提升 |
| VID 策略 | AST 路径 | 全局索引 |
| 运行时 | Node.js | WASM (Wasip1) |
- 仅开发模式 - 编辑功能仅在运行
npm run dev时可用 - 版本控制 - 修改会直接写入源文件,建议使用 Git
- 动态内容 - 包含
{state}表达式的元素无法编辑(会显示 Toast 提示) - 文本元素 - 仅支持 p、span、button、h1-h6、a、label、div 等文本元素
- SWC 版本兼容 - 插件需要 swc_core v47-51 才能兼容 Next.js 16.1
MIT