手把手打造高可用表单设计器的核心技术实践

设计师景荣 交互 阅读 808
赞 17 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

上周接到一个需求:让用户自己拖拽组件生成表单,支持保存、预览、导出 JSON。听起来很常见,但真做起来才发现坑不少。我折腾了三天,最后用原生 JS + Vue3 搞定了一套轻量方案,核心代码不到 200 行,亲测有效。

手把手打造高可用表单设计器的核心技术实践

先说效果:左侧是组件库(输入框、下拉框、开关等),中间是画布,拖过去就能放,点一下还能编辑属性。整个交互流畅,连产品经理都点头说“这次做得快”。

核心思路就两点:1. 用拖拽 API 实现组件拖入;2. 用响应式数组管理表单结构。下面直接上关键代码。

// 组件库项(简化版)
const components = [
  { type: 'input', label: '单行文本', placeholder: '请输入' },
  { type: 'select', label: '下拉选择', options: ['选项1', '选项2'] },
  { type: 'switch', label: '开关', default: false }
];

// 表单数据结构
const formSchema = ref([]);
<!-- 左侧组件库 -->
<div class="component-list">
  <div
    v-for="comp in components"
    :key="comp.type"
    draggable="true"
    @dragstart="handleDragStart(comp)"
    class="component-item"
  >
    {{ comp.label }}
  </div>
</div>

<!-- 中间画布 -->
<div class="canvas" @drop="handleDrop" @dragover.prevent></div>
// 拖拽开始
function handleDragStart(comp) {
  // 把组件数据存到 dataTransfer
  event.dataTransfer.setData('application/json', JSON.stringify(comp));
}

// 放下组件
function handleDrop() {
  const data = event.dataTransfer.getData('application/json');
  const comp = JSON.parse(data);
  // 注意:这里要深拷贝!否则多个实例会共享同一个 options 引用
  formSchema.value.push({ ...comp, id: Date.now() });
}

这段代码跑起来基本就能拖了。但注意那个 id: Date.now() —— 我一开始没加唯一 ID,结果编辑的时候改一个,所有同类型组件全变了,折腾半天才定位到是引用问题。

踩坑提醒:这三点一定注意

表单设计器看着简单,实际开发中这几个坑我全踩过:

  • 引用问题:从组件库拖出来的对象如果直接 push 到 schema,修改时会污染原始模板。一定要深拷贝,或者像上面那样用展开运算符+新 ID。
  • 拖拽兼容性@dragover.prevent 必须加,否则 drop 事件不会触发。这个在 Chrome 没问题,但在 Safari 上不加 prevent 就直接失效,血泪教训。
  • 动态组件渲染:画布里怎么根据 type 渲染不同组件?别用 v-if 堆一堆,用动态组件 <component :is="item.type" /> 更干净。但要注意,组件名必须注册,比如:
// 在父组件中注册
import InputField from './fields/InputField.vue';
import SelectField from './fields/SelectField.vue';

export default {
  components: {
    input: InputField,
    select: SelectField,
    // 注意:这里 key 是小写,和 schema.type 保持一致
  }
}

另外,不要用大驼峰命名 type,比如 InputField,因为 Vue 动态组件对大小写敏感,容易出错。统一用小写最省事。

这个场景最好用:实时预览 + 属性面板

用户拖完组件,肯定要改 label、placeholder 这些。我搞了个右侧属性面板,点击画布上的组件就高亮并显示可编辑字段。

<!-- 画布中的每个组件 -->
<div
  v-for="item in formSchema"
  :key="item.id"
  @click="selectItem(item)"
  :class="{ selected: selectedItem?.id === item.id }"
>
  <component :is="item.type" :config="item" />
</div>

<!-- 属性面板 -->
<div v-if="selectedItem" class="props-panel">
  <input v-model="selectedItem.label" placeholder="标签" />
  <input v-model="selectedItem.placeholder" placeholder="占位符" v-if="selectedItem.type === 'input'" />
</div>

关键点:selectedItem 直接指向 formSchema 中的某个对象,所以 v-model 修改会自动同步。但这里有个隐患 —— 如果用户清空了 label,表单可能校验失败。所以我在保存前加了个校验:

function validateSchema() {
  for (const item of formSchema.value) {
    if (!item.label.trim()) {
      alert('请填写字段标签');
      return false;
    }
  }
  return true;
}

简单粗暴,但有效。毕竟这是给内部运营用的工具,不是给终端用户,没必要搞复杂 UI 提示。

高级技巧:支持嵌套布局(比如两栏)

客户后来提了个需求:能不能放个“两栏布局”容器,里面再拖字段?这就要把 schema 改成树形结构了。

我原来的 flat 数组不行了,得改成:

// 新结构
const formSchema = ref([
  {
    type: 'container',
    layout: 'two-column',
    children: [
      { type: 'input', label: '左边字段' },
      { type: 'select', label: '右边字段' }
    ]
  }
]);

渲染时递归处理:

<template>
  <div v-for="item in items" :key="item.id">
    <component
      v-if="!item.children"
      :is="item.type"
      :config="item"
    />
    <div v-else class="container-wrapper">
      <div v-for="child in item.children" :key="child.id">
        <FormField :item="child" />
      </div>
    </div>
  </div>
</template>

拖拽逻辑也得改:drop 的时候要判断当前是否在容器内。我加了个 currentContainer 变量,通过冒泡事件记录路径。不过这样代码复杂度飙升,如果你项目不需要复杂布局,建议直接禁用嵌套,保持 flat 结构最稳

导出与加载:JSON 就够了

最后一步,保存和加载。直接把 formSchema 转 JSON 存数据库就行:

// 保存
const saveData = JSON.stringify(formSchema.value);
// 发给后端
fetch('https://jztheme.com/api/save-form', {
  method: 'POST',
  body: JSON.stringify({ schema: saveData })
});

// 加载
const res = await fetch('https://jztheme.com/api/get-form?id=123');
formSchema.value = await res.json();

注意:JSON 里不能有函数或循环引用,但我们 schema 都是纯数据,没问题。不过 Date 对象会变成字符串,所以 ID 我改用 crypto.randomUUID() 了,更稳妥。

另外,别在 JSON 里存 DOM 信息或临时状态,比如 isFocused、isValid 这些。只存业务数据,避免后续解析出错。

结尾:这东西水很深,但 MVP 其实很简单

表单设计器可以做得很重(比如阿里云的 LowCode),但大多数内部需求,一个周末就能搞出能用的版本。我这套方案虽然简陋,但支撑了我们三个项目的运营配置,稳定跑了半年多。

当然,它还有不少问题:比如撤销/重做没做、移动端适配差、性能在 50+ 字段时有点卡。但这些都不影响核心流程,优先级不高就没动。

以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如结合 JSON Schema 自动生成表单、支持自定义组件注册等),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流 —— 毕竟谁还没被表单需求折磨过呢?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论