Complex Interaction

v1.0.0

Implements complex frontend interactions including drag-and-drop, virtual scrolling, rich text editing, real-time collaboration, Canvas/WebGL rendering, and...

0· 22·0 current·0 all-time
bywangzhiming@wangzhiming1999
Security Scan
VirusTotalVirusTotal
Benign
View report →
OpenClawOpenClaw
Benign
high confidence
Purpose & Capability
Name/description match the SKILL.md content: the document provides implementation guidance and code snippets for drag-and-drop (@dnd-kit), virtual scrolling (@tanstack/react-virtual), rich text (Tiptap/Slate), real-time collaboration (Yjs/y-websocket), Canvas/WebGL. It does not request unrelated credentials, binaries, or system access.
Instruction Scope
SKILL.md stays on-topic and provides concrete code snippets and architectural guidance. It references connecting to collaboration transport (example: WebsocketProvider('wss://your-server.com', ...)) — expected for collaboration but users should be aware that runtime collaboration requires hosting/trusting a realtime server; the file does not instruct reading local files or environment variables.
Install Mechanism
No install spec and no code files—this is instruction-only so nothing will be downloaded or written by the skill itself. The guidance recommends standard npm/yarn packages; installing those in a project is normal and outside the skill's runtime footprint.
Credentials
The skill declares no required environment variables, credentials, or config paths. The examples show connecting to a realtime endpoint but use a placeholder URL; no secrets are requested by the skill.
Persistence & Privilege
Skill is not always:true and does not request persistent system-level privileges or modify other skills' configs. As an instruction-only skill it cannot persist code on the host by itself.
Assessment
This appears to be a straight technical guide rather than executable code — that's why it needs no env vars or installs. Before using: (1) verify the referenced libraries and versions (npm packages) and audit them for licensing/security when adding to your project; (2) if you implement real-time collaboration, choose or host a trusted WebSocket/RTC provider (the SKILL.md uses a placeholder wss:// URL); (3) prefer skills with a public source repo or homepage — clawhub.json points to a GitHub repo, but the registry header showed no homepage and the owner ID differs, so confirm the source and author if provenance matters; (4) because this is instruction-only, the skill itself won’t run code on your system, but following its guidance will cause you to add third-party packages and network endpoints to your app — review those independently.

Like a lobster shell, security has layers — review code before you run it.

latestvk977wwfcpx841d406jawq98kw18563a9
22downloads
0stars
1versions
Updated 9h ago
v1.0.0
MIT-0

复杂交互实现(Complex Interaction)

实现拖拽排序、虚拟滚动、富文本编辑器、实时协作、Canvas/WebGL 等高难度交互,给出可落地的方案和关键代码。

触发场景

  • 「拖拽排序怎么做」「拖拽上传」「看板拖拽」
  • 「列表有几万条,怎么不卡」「虚拟滚动」
  • 「富文本编辑器」「表格编辑器」「低代码画布」
  • 「多人实时协作」「冲突解决」
  • 「Canvas 性能优化」「WebGL 入门」「手势识别」

执行流程

1. 先判断复杂度,选对方案

不要上来就自己实现,先评估:

场景推荐方案自己实现的条件
简单拖拽排序(同列表)@dnd-kit/sortable几乎不需要
复杂拖拽(跨容器、看板、树形)@dnd-kit/core 自定义有特殊约束时
长列表虚拟滚动@tanstack/react-virtual几乎不需要
富文本编辑器Tiptap / Slate.js极度定制化需求
实时协作Yjs + 对应绑定几乎不需要
Canvas 2D 交互Konva.js / Fabric.js性能极限场景
3D / WebGLThree.js / React Three Fiber几乎不需要

自己实现的成本远超预期——拖拽涉及触摸事件、键盘无障碍、滚动容器、嵌套列表,富文本涉及光标、选区、撤销栈,这些库已经踩过所有坑。

2. 拖拽排序

标准实现(@dnd-kit):

import { DndContext, closestCenter } from '@dnd-kit/core'
import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'

function SortableItem({ id, children }) {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })
  return (
    <div
      ref={setNodeRef}
      style={{
        transform: CSS.Transform.toString(transform),
        transition,
        opacity: isDragging ? 0.5 : 1,
      }}
      {...attributes}
      {...listeners}
    >
      {children}
    </div>
  )
}

function SortableList({ items, onReorder }) {
  return (
    <DndContext
      collisionDetection={closestCenter}
      onDragEnd={({ active, over }) => {
        if (over && active.id !== over.id) {
          const oldIndex = items.findIndex(i => i.id === active.id)
          const newIndex = items.findIndex(i => i.id === over.id)
          onReorder(arrayMove(items, oldIndex, newIndex))
        }
      }}
    >
      <SortableContext items={items.map(i => i.id)} strategy={verticalListSortingStrategy}>
        {items.map(item => (
          <SortableItem key={item.id} id={item.id}>{item.name}</SortableItem>
        ))}
      </SortableContext>
    </DndContext>
  )
}

跨容器拖拽(看板)的关键点:

  • DragOverlay 渲染拖拽中的预览,避免原位置闪烁
  • over.data.current 判断拖到了哪个容器
  • 状态更新要在 onDragEnd 里做,onDragOver 只做视觉反馈

拖拽上传的关键点:

  • 用原生 dragover + drop 事件,不需要 dnd-kit
  • dragover 必须 preventDefault() 才能触发 drop
  • DataTransfer.items 处理文件夹递归上传

3. 虚拟滚动

何时需要虚拟滚动: 列表超过 500 条且有明显卡顿,或超过 2000 条无论是否卡顿。

标准实现(@tanstack/react-virtual):

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualList({ items }) {
  const parentRef = useRef(null)

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 56,          // 预估行高,影响滚动条比例
    overscan: 5,                      // 视口外额外渲染的行数
  })

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>  {/* 撑开滚动高度 */}
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${virtualRow.start}px)`,
              height: `${virtualRow.size}px`,
            }}
          >
            {items[virtualRow.index].name}
          </div>
        ))}
      </div>
    </div>
  )
}

动态行高的处理:

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 56,
  // 测量实际渲染高度,自动修正
  measureElement: (el) => el.getBoundingClientRect().height,
})

// 在每个 item 上加 ref
<div ref={virtualizer.measureElement} data-index={virtualRow.index}>

虚拟滚动 + 无限加载:

// 监听最后一个 item 进入视口
useEffect(() => {
  const lastItem = virtualItems[virtualItems.length - 1]
  if (!lastItem) return
  if (lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) {
    fetchNextPage()
  }
}, [virtualItems, items.length, hasNextPage, isFetchingNextPage])

4. 富文本编辑器

选型决策:

  • 需要协作编辑 → Tiptap(基于 ProseMirror,有 Yjs 扩展)
  • 需要高度定制数据结构 → Slate.js(数据模型完全可控)
  • 简单格式化(加粗/斜体/链接)→ Tiptap 基础版,5 分钟搭起来
  • 类 Notion 块编辑器 → Tiptap + 自定义 Node

Tiptap 快速上手:

import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Placeholder from '@tiptap/extension-placeholder'

function RichTextEditor({ value, onChange }) {
  const editor = useEditor({
    extensions: [
      StarterKit,
      Placeholder.configure({ placeholder: '开始输入...' }),
    ],
    content: value,
    onUpdate: ({ editor }) => onChange(editor.getHTML()),
  })

  return (
    <div className="editor-wrapper">
      <Toolbar editor={editor} />
      <EditorContent editor={editor} />
    </div>
  )
}

function Toolbar({ editor }) {
  if (!editor) return null
  return (
    <div>
      <button onClick={() => editor.chain().focus().toggleBold().run()}
        className={editor.isActive('bold') ? 'active' : ''}>B</button>
      <button onClick={() => editor.chain().focus().toggleItalic().run()}>I</button>
    </div>
  )
}

5. 实时协作(Yjs)

Yjs 的核心概念:

  • Y.Doc:共享文档,所有协作数据的容器
  • CRDT:冲突自动合并,不需要手写冲突解决逻辑
  • Provider:传输层(WebSocket、WebRTC、IndexedDB 本地持久化)

与 Tiptap 集成:

import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { Collaboration } from '@tiptap/extension-collaboration'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'

const ydoc = new Y.Doc()
const provider = new WebsocketProvider('wss://your-server.com', 'room-name', ydoc)

const editor = useEditor({
  extensions: [
    StarterKit.configure({ history: false }),  // 禁用内置 history,用 Yjs 的
    Collaboration.configure({ document: ydoc }),
    CollaborationCursor.configure({
      provider,
      user: { name: '张三', color: '#f783ac' },
    }),
  ],
})

后端 WebSocket 服务器(y-websocket):

# 最简单的方式:直接用官方服务器
HOST=localhost PORT=1234 npx y-websocket

6. Canvas 性能优化

Canvas 卡顿的常见原因:

原因解法
每帧重绘整个画布分层 Canvas(静态背景一层,动态元素一层)
在主线程做大量计算OffscreenCanvas + Web Worker
频繁创建/销毁对象对象池(Object Pool)
没有用 requestAnimationFrame用 rAF 驱动动画循环
高 DPI 屏幕模糊canvas.width = width * devicePixelRatio

高 DPI 修复:

function setupCanvas(canvas: HTMLCanvasElement) {
  const dpr = window.devicePixelRatio || 1
  const rect = canvas.getBoundingClientRect()
  canvas.width = rect.width * dpr
  canvas.height = rect.height * dpr
  const ctx = canvas.getContext('2d')!
  ctx.scale(dpr, dpr)
  return ctx
}

分层 Canvas(低代码画布场景):

// 背景网格层(静态,只在 resize 时重绘)
<canvas ref={bgRef} style={{ position: 'absolute' }} />
// 元素层(拖拽时重绘)
<canvas ref={elementsRef} style={{ position: 'absolute' }} />
// 交互层(hover/选中高亮,频繁重绘)
<canvas ref={interactionRef} style={{ position: 'absolute' }} />

常见坑

场景解法
拖拽触摸设备不触发 mouse 事件dnd-kit 已处理,自己实现要加 touch 事件
虚拟滚动滚动到底部时跳动measureElement 测量实际高度
富文本粘贴时带入外部样式配置 transformPastedHTML 清理
实时协作断线重连后数据不一致y-websocket 自动处理,确保 provider 有重连逻辑
Canvas事件坐标偏移canvas.getBoundingClientRect() 转换坐标

Comments

Loading comments...