阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • CSS

    • ✨flex 布局 👌
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
      • ✨ 事件循环 👌
    • Promise

      • ✨Promise 面试题考点 👌
      • ✨Promise 基础 👌
      • ✨Promise 的链式调用 👌
      • ✨Promise 的静态方法 👌
      • ✨async 和 await👌
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • ✨SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新
    • Cypress

      • Cypress 测试框架面试题
    • 项目

      • FOFA 实习项目

        • /interview/project/fofa/FOFA%E5%AE%9E%E4%B9%A0%E9%A1%B9%E7%9B%AE%E9%9D%A2%E8%AF%95%E7%82%B9.html
      • 低代码问卷系统

        • 低代码问卷项目面试点
      • VR 全景看房

        • VR 全景看房项目面试点
  • TS

    • 快速入门

      • Playground 👌
      • 安装与运行 👌
      • 开发相关配置 👌
      • TS 常见类型 👌
      • 类型声明 👌
  • 工具库

    • 常用第三方工具库
    • JQuery
    • Lodash
    • Animate.css
    • Axios
    • MockJS
    • Moment
    • ECharts
  • 其他知识点

    • ✨ 前端项目打包流程与编译概念详解
    • ✨ 懒加载 👌
    • ✨ 前端路由的核心原理 👌

Q5: 在使用 IndexedDB 时遇到过什么问题?如何解决的?

口头回答

在这个低代码问卷平台项目中,我使用 dexie.js 作为 IndexedDB 的包装器来实现问卷数据的本地存储。在使用过程中主要遇到了以下几个问题:

1. 数据序列化和反序列化问题 最大的问题是组件实例的序列化。由于 IndexedDB 只能存储可序列化的数据,而我们的组件包含 Vue 组件实例,直接存储会丢失组件的方法和响应式特性。

2. 异步操作的错误处理 IndexedDB 的所有操作都是异步的,需要妥善处理各种异常情况,如数据库连接失败、存储空间不足、数据不存在等。

3. 数据库版本管理 在开发过程中需要考虑数据库结构的变更和版本升级问题。

4. 浏览器兼容性 不同浏览器对 IndexedDB 的支持程度不同,需要考虑兼容性问题。

具体实现细节

1. 数据库初始化和配置

项目中使用 dexie.js 简化了 IndexedDB 的操作:

// src/db/db.ts
import Dexie, { type Table } from 'dexie';
import type { Questionnaire } from '@/types';

class QuestionnaireDB extends Dexie {
  questionnaires!: Table<Questionnaire, number>;

  constructor() {
    super('questionnaireDB'); // 数据库名称
    this.version(1).stores({
      questionnaires: '++id, createTime, updateTime, title, questionNumber, questionComs',
    });
  }
}

export const db = new QuestionnaireDB();

解决方案:

  • 使用 dexie.js 简化 IndexedDB 操作
  • 明确定义数据库结构和索引
  • 使用自增主键 ++id 确保数据唯一性

2. 组件序列化问题的解决

问题: Vue 组件实例无法直接序列化存储到 IndexedDB

解决方案: 实现组件协议化存储和恢复机制

// 存储时的序列化处理
// src/components/Common/Header.vue
const saveQuestionnaire = async () => {
  try {
    // 输入标题提示框
    const { value } = await ElMessageBox.prompt('请输入问卷标题', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'info',
    });
    // 构建问卷数据
    const questionnaire: Questionnaire = {
      title: value,
      createTime: Date.now(),
      updateTime: Date.now(),
      questionNumber: editorStore.questionCount,
      // indexDB可以存无方法的对象数组
      questionComs: JSON.parse(JSON.stringify(editorStore.questionComs)), // TODO:这种方式恢复时存在问题
    };

    // 保存问卷
    const id = await editorStore.saveQuestionComs(questionnaire);
    ElMessage.success('保存问卷成功');
    router.push(`/editor/${id}/questionTypeGroup`);
  } catch (error) {
    if (error === 'cancel') {
      ElMessage.info('已取消保存');
    } else {
      ElMessage.info('问卷保存失败');
    }
  }
};
// 恢复时的反序列化处理
// src/utils/index.ts
// 还原组件状态
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];
    }
  });
}

点击编辑按钮时发生路由跳转,并携带问卷id,在编辑页根据id查询问卷数据,再根据数据恢复组件状态

3. 异步操作错误处理

问题: IndexedDB 操作可能失败,需要妥善处理各种异常

解决方案: 在所有数据库操作中添加 try-catch 错误处理

// src/views/HomeView.vue - 获取问卷列表
const getQuestionnaireList = async () => {
  try {
    const questionnaireList = await queryAllQuestionnaire();
    tableData.value = questionnaireList;
  } catch (error) {
    ElMessage.error('获取问卷列表失败');
  }
};

// 删除问卷
const deleteQuestionnaire = async (id: number) => {
  try {
    await ElMessageBox.confirm('确定删除该问卷吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
    });
    await editorStore.removeQuestionnaireById(id);
    ElMessage.success('删除问卷成功');
    getQuestionnaireList();
  } catch (error) {
    ElMessage.info('取消删除');
  }
};
// src/components/Common/Header.vue - 更新问卷
const updateQuestionnaire = async () => {
  try {
    const questionnaire: Partial<Questionnaire> = {
      updateTime: Date.now(),
      questionNumber: editorStore.questionCount,
      questionComs: JSON.parse(JSON.stringify(editorStore.questionComs)),
    };
    await editorStore.updateQuestionnaire(Number(props.questionnaireId), questionnaire);
    ElMessage.success('更新问卷成功');
  } catch (error) {
    ElMessage.error('更新问卷失败');
  }
};

4. 数据库操作封装

解决方案: 将所有数据库操作封装到独立的模块中

// src/db/operation.ts
import { db } from './db';
import type { Questionnaire } from '@/types';

// 添加问卷
export const addQuestionnaire = async (questionnaire: Questionnaire) => {
  return await db.questionnaires.add(questionnaire);
};

// 查询所有数据
export const queryAllQuestionnaire = async () => {
  return await db.questionnaires.toArray();
};

// 根据id查询某一条数据
export const queryQuestionnaireById = async (id: number) => {
  return await db.questionnaires.get(id);
};

// 根据id删除某一条数据
export const deleteQuestionnaireById = async (id: number) => {
  return await db.questionnaires.delete(id);
};

// 根据id更新某一条数据
export const updateQuestionnaireById = async (
  id: number,
  questionnaire: Partial<Questionnaire>,
) => {
  return await db.questionnaires.update(id, questionnaire);
};

5. 数据加载和恢复机制

问题: 从数据库加载数据后需要正确恢复组件状态

解决方案: 实现完整的数据恢复流程

// src/views/EditorView/index.vue - 编辑器页面数据恢复
if (questionnaireId) {
  editorStore.getQuestionnaireById(Number(questionnaireId.value)).then((res) => {
    if (res) {
      // 恢复组件实例
      restoreComponentsStatus(res.questionComs);
      // 恢复编辑器状态
      editorStore.restoreQuestionnaire(res);
    }
  });
}
// src/views/Preview.vue - 预览页面数据恢复
const getQuestionnaire = async () => {
  try {
    const res = await editorStore.getQuestionnaireById(id);
    if (res) {
      // 拿到数据后需要重新还原组件
      restoreComponentsStatus(res.questionComs);
      // 还原问卷仓库的状态
      editorStore.restoreQuestionnaire(res);
    }
  } catch (error) {
    ElMessage.error('加载问卷数据失败');
    router.push('/');
  }
};

6. 状态管理集成

解决方案: 将数据库操作集成到 Pinia 状态管理中

// src/stores/useEditor.ts
export const useEditorStore = defineStore('editor', {
  actions: {
    // 保存问卷
    saveQuestionComs(questionnaire: Questionnaire) {
      return addQuestionnaire(questionnaire);
    },

    // 更新问卷
    updateQuestionnaire(id: number, questionnaire: Partial<Questionnaire>) {
      return updateQuestionnaireById(id, questionnaire);
    },

    // 获取问卷
    getQuestionnaireById(id: number) {
      return queryQuestionnaireById(id);
    },

    // 删除问卷
    removeQuestionnaireById(id: number) {
      return deleteQuestionnaireById(id);
    },

    // 恢复问卷状态
    restoreQuestionnaire(questionnaire: Questionnaire) {
      this.questionComs = questionnaire.questionComs;
      this.questionCount = questionnaire.questionNumber;
      this.currentQuestionIndex = -1;
    },
  },
});

解决方案总结

1. 技术选型优势

  • 使用 dexie.js:简化了 IndexedDB 的复杂 API,提供了更友好的 Promise 接口
  • TypeScript 支持:提供了完整的类型定义,减少了运行时错误

2. 架构设计优势

  • 组件协议化:通过协议化设计解决了组件序列化问题
  • 分层架构:数据库操作、状态管理、UI 组件分离,便于维护
  • 错误处理机制:在每个关键操作点都添加了错误处理

3. 用户体验优化

  • 友好的错误提示:使用 Element Plus 的消息组件提供用户友好的错误信息
  • 数据恢复机制:确保用户数据的完整性和一致性
  • 离线可用性:本地存储保证了核心功能的离线可用性

4. 性能优化

  • 按需加载:只在需要时加载和恢复数据
  • 深拷贝优化:使用 JSON.parse(JSON.stringify()) 进行数据清理
  • 索引优化:合理设计数据库索引提高查询性能

这种混合存储架构既保证了核心功能的离线可用性,又为后续的服务器端扩展预留了空间,是一个实用且可扩展的解决方案。

最近更新:: 2025/7/18 12:04
Contributors: AK