低代码问卷项目面试点
项目介绍
项目描述
该项目构建了一个现代化的低代码问卷平台。该平台允许用户通过拖拽式快速创建和编辑问卷。平台提供的协议化组件市场支持多种题型,可以轻松对题目的内容和样式进行动态调整,同时提供了问卷的在线预览和 PDF 生成功能。系统采用前端本地存储方案,通过 IndexedDB 实现数据持久化。
技术栈
vue3、ts、vite、element-plus、pinia、vue-router、dexie.js 、vuedraggable
项目亮难点
- 实现了基于组件市场和拖拽式调整的问卷设计器,构建“组件拖拽 + 动态渲染 + 属性热更新”全链路能力,大幅提升了问卷创建效率,降低了使用门槛。
- 采用组件协议化的设计思路,封装了多种问卷题型组件(单选、多选等),实现了组件市场功能。通过组件协议化注册使系统具有良好的扩展性和可维护性。
- 采用 IndexedDB 实现问卷模板的本地存储与管理,同时结合服务器处理图片上传和问卷答案提交,实现了一种灵活的混合存储架构,既保证了核心功能的离线可用性,又满足了图片处理和数据提交的服务器需求,提高了系统的可用性和用户体验。
面试题
Q1: 你提到采用了组件协议化的设计思路,能详细说明一下什么是组件协议化?它是如何实现的?
参考答案:
组件协议化是这个低代码问卷平台中采用的核心设计思路,它的本质是通过统一的数据结构和接口规范,让不同的组件能够按照相同的协议进行注册、配置和管理。
简单来说,组件协议化就是为每个题型组件定义了一套标准的"身份证",这个身份证包含了组件的类型、名称、唯一 ID,以及最重要的状态配置。通过这种方式,系统可以动态地识别、加载和管理各种不同类型的问卷组件,而不需要硬编码每个组件的具体实现。
这种设计的核心优势在于:
- 可扩展性:新增组件只需要按照协议定义配置文件,无需修改核心代码
- 可维护性:组件的业务逻辑和配置分离,便于管理和维护
- 动态性:支持运行时动态加载和渲染组件
- 一致性:所有组件都遵循相同的接口规范,降低了系统复杂度
Q2: 你是如何设计"组件拖拽 + 动态渲染 + 属性热更新"这个全链路能力的?
参考答案:
在这个项目中通过这个全链路能力将复杂的问卷创建过程简化为三个无缝衔接的步骤:拖拽添加组件、动态渲染展示、实时属性编辑。
- 组件拖拽:使用 H5 的拖曳 API 实现组件库到画布的拖拽,利用
vuedraggable
库实现题目组件顺序的拖曳,维护组件列表的响应式数据 - 动态渲染:通过 Vue 的动态组件
<component :is="componentType">
根据配置数据渲染对应组件 - 属性热更新:利用 Vue 的响应式系统,当题目组件的属性编辑面板发生修改时时,实时更新仓库中该组件对应的属性 props 触发重新渲染
整体的一个流程就是,当用户点击或者拖曳左侧组件市场对应的组件到画布的时候,通过该组件对应的题目类型(如 singleSelect),在我们的默认状态映射表(defaultStatusMap.ts)中查找对应组件的默认状态,如果是一些预设的信息组件(如性别、年龄组件),需要利用一个工具函数对(单选组件的)默认状态进行修改,然后将默认状态添加到状态仓库中,状态仓库维护了一个列表,其中存放了所有需要在画布中展示的组件状态。画布区域就是通过拿到状态仓库中的组件状态列表,利用动态组件 component 组件和 is 属性进行动态渲染。 默认状态其实就是我们定义的组件协议,这个协议其实就是一个对象,为了防止多个组件公用一个状态,他是通过一个工厂函数返回的,然后这个对象规定了需要渲染的题目组件以及题目组件的初始状态(也就是需要传递给这个组件的 props)。点击画布中的对应题目时会修改状态仓库中表示当前选中题目组件的索引,根据这个索引会动态的给右侧的编辑面板组件传递当前正在编辑的组件状态,组件状态中包含了每一个可编辑属性对应的编辑组件,同样通过动态 component 组件和 is 属性进行动态渲染。所有的编辑组件都是通过修改状态仓库中对应的属性值来实现动态编辑。
Q3: 问卷中的题目组件协议是如何设计的?
参考答案:
首先协议是服务于所有的题目组件的,因此需要考虑要实现哪些题目组件比如单选题、日期选择题、有图片的单选题等等,那么协议里面肯定要包含代表该题目组件的一些基本信息,比如组件名、id、对应的渲染组件等。
其次由于不同的题目组件实现的功能之间有差异,比如单选题(el-radio)、日期选择题(el-date-picker)、由于这些组件都是第三饭库封装好,其实我们只需要关注传递的数据就好了,也就是考虑题目组件的哪些属性是可以修改的(单选题的选项是可以修改的、日期选择题的日期类型是可以修改的),因此在协议中还需要体现每一个题目组件对应能够修改的整体的属性状态,并且是以一个对象的形式,因为能够修改的属性可能有多个,并且每一个能够修改的属性也对应于一个对象,这个对象中包括了这个属性的默认值,以及对应的编辑组件(一个属性对应一个编辑组件)等等。由于不同属性的表现形式不同,描述这个属性默认值的方式也不同,比如对于单选题中的标题属性,直接用一个字符串来表示就行,对于选项属性就需要用一个数组来表示了。
所以从整体上看,协议针对具体某一个题目组件的配置是比较灵活的,只要满足基本的配置,对于特定与这个组件的功能其实是可以非常灵活的在相应的状态属性配置中增加额外的配置就可以了,只要和对应的属性编辑器和渲染组件逻辑上互恰当就行,保证它的一个弹性扩展能力。
协议它只规定了最小的需要满足的配置条件,只有满足了这些条件组件才能注册成功,也就是说某个具体组件的配置其实是这个协议的超集,协议是子集。
很多时候是想到了实现方法之后去倒推这个组件是具体怎么配置比较合适的。
Q4: 为什么选择混合存储架构而不是纯前端或纯后端存储?
参考答案:
- 离线可用性:问卷模板存储在 IndexedDB 中,用户可以离线创建和编辑问卷
- 功能需求:图片上传需要服务器处理,问卷答案需要持久化到服务器
- 性能考虑:本地存储减少网络请求,提升用户体验
- 数据安全:重要数据通过服务器备份,避免本地数据丢失
Q5: 在使用 IndexedDB 时遇到过什么问题?如何解决的?
参考答案:
在这个低代码问卷平台项目中,我使用 dexie.js 库简化 IndexedDB 操作来实现问卷数据的本地存储。在使用过程中遇到的最大问题组件的数据序列化和反序列化问题。因为保存的问卷恢复的时候需要还原画布中的题目组件所有保存的时候需要保存组件的实例(有实例方法),但是 IndexedDB 只能存储可序列化的数据,直接存储报错,因此只能先通过JSON.parse(JSON.stringify())
过滤对象的方法在存储,但是直接恢复时会丢失组件的方法和响应式特性。
// 构建问卷数据
const questionnaire: Questionnaire = {
title: value,
createTime: Date.now(),
updateTime: Date.now(),
questionNumber: editorStore.questionCount,
// indexDB可以存无方法的对象数组
questionComs: JSON.parse(JSON.stringify(editorStore.questionComs)), // TODO:这种方式恢复时存在问题
};
因此需要通过一个工具函数,去遍历读取出来的数据中组件的各个属性,重新进行组件的映射。
// 还原组件状态
export function restoreComponentsStatus(questionComs: SchemaType[]) {
questionComs.forEach((item) => {
// 业务组件还原
item.type = componentMap[item.name];
// 编辑组件还原
for (let key in item.status) {
const name = item.status[key].name as EditorComType;
item.status[key].editCom = componentMap[name];
}
});
}
Q6: vuedraggable 在实现拖拽功能时有什么注意事项?
参考答案:
- 数据绑定:确保拖拽操作正确更新 Vue 的响应式数据
- 拖拽约束:设置合理的拖拽区域和规则,防止误操作
- 性能优化:大量组件时使用虚拟滚动或分页加载
Q7:为什么添加组件到画布使用了 H5 原始拖曳 API,调整画布题目和大纲顺序使用了 vuedraggable?有什么特别的考量吗?
参考答案:
1. 添加组件使用 H5 原始拖曳 API:主要考虑是跨容器拖拽的需求。从左侧组件库拖拽到中间画布,这是两个完全不同的 DOM 容器,需要传递组件类型等数据信息,H5 原生 API 的 dataTransfer
机制非常适合这种场景。
2. 调整顺序使用 vuedraggable:主要考虑是同容器内排序的需求。画布内的组件排序和大纲面板的排序都是在同一个数组内调整元素位置,vuedraggable 提供了更好的 Vue 响应式集成和用户体验,同时列表的数据格式也更加适用于 vuedraggable,原生的拖曳 API 也可以实现但是需要计算拖曳的位置等操作、代码量比较多。
Q8: 如何实现问卷的 PDF 生成功能?
参考答案:
该项目主要基于浏览器原生的打印功能window.print()
来实现 PDF 导出。这种方案既保证了生成质量,又避免了引入复杂的第三方 PDF 库,降低了项目复杂度和包体积。
生成 PDF 时主要进行了以下的操作:
检查问卷中是否包含不适合 PDF 格式的交互组件(日期选择、下拉选择等)
通过媒体查询来隐藏一些元素和边框来保证 PDF 的美观。
// 媒体查询打印时隐藏边框和打印按钮(打印时会引用相应的样式) @media print { .no-border { border: none; box-shadow: none; } .no-print { display: none; } }
Q9: 在这个项目中,Pinia 是如何组织状态管理的?
参考答案:
在这个项目中 Pinia 的状态管理采用了模块化和职责分离的设计理念。将整个应用的状态按照业务领域划分为两个主要的 Store:编辑器状态管理(useEditorStore)和组件市场状态管理(useMaterialStore),同时通过公共 Actions 和统一的状态更新机制来保证代码的复用性和一致性。
Q10: 在大量组件渲染时,如何保证性能(比如一个问卷有上百个题目组件)?
参考答案:
- 虚拟滚动:对于长列表使用虚拟滚动技术
- 组件懒加载:按需加载组件,减少初始包大小
- 防抖节流:对频繁操作如拖拽、输入进行防抖处理
- 缓存策略:合理使用 computed 和 watch,避免不必要的计算
Q11: 在实现动态表单渲染时遇到的最大挑战是什么?
参考答案:
- 组件通信复杂性:大量动态组件间的数据传递和状态同步
- 性能问题:大量组件同时渲染的性能优化
- 类型安全:TypeScript 下动态组件的类型定义
- 调试困难:动态生成的组件调试和错误定位