Electron-Vue3项目数据的本地存储

Electron-Vue3 项目本地数据库与存储服务设计

#tech / dev / desktop #resource / electron #type / concept #status / growing

[!info] related notes

准备

确定需要实现的功能和代码大致结构层次

项目会产生目标相关(目标、文件夹等)、仓库相关、设置相关、任务相关等的数据
将这些数据存储到本地数据库中

  • 存储 基础服务,要实现 数据库 到 前端服务层
    实现前端能调用服务来实现对数据的增删读写
  • 初始化 基于存储服务
    在登录成功时调用,加载该用户的数据
  • 保存 composable,可复用函数
    基于存储服务的读取服务

表结构:

// 创建用户数据存储表
db.exec(`
  CREATE TABLE IF NOT EXISTS user_store_data (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL,
    store_name TEXT NOT NULL,
    data TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL,
    UNIQUE(username, store_name),
    FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE
  )
`);

通过 username,store_name 来获取整个 store 保存的数据

怎么实现用户登录时,初始化用户数据的功能(利用 storeService,从数据表中读取每个 store 的数据,加载到相应的 store 中)

增加一个 userDataInitService
用来给每个模块读取用户数据(登录时)、清空所有数据(登出时)、保存所有数据到数据库(内容修改时、退出时)

userDataInitService.ts 具体代码
import { UserStoreService } from "./userStoreService";
import { useGoalStore } from "@/modules/Goal/stores/goalStore";
import { useTaskStore } from "@/modules/Task/stores/taskStore";
import { useGoalDirStore } from "@/modules/Goal/stores/goalDirStore";
import { useGoalReviewStore } from "@/modules/Goal/stores/goalReviewStore";
import { useReminderStore } from "@/modules/Reminder/reminderStore";
import { useRepositoryStore } from "@/modules/Repository/stores/repositoryStore";
import { useSettingStore } from "@/modules/Setting/stores/settingStore";
import type { IGoal, IRecord, IGoalDir } from "@/modules/Goal/types/goal";
import type { ITaskInstance, ITaskTemplate } from "@/modules/Task/types/task";
import type { Review } from "@/modules/Goal/stores/goalReviewStore";
import type { Reminder } from "@/modules/Reminder/reminderStore";
import type { Repository } from "@/modules/Repository/stores/repositoryStore";
import type { AppSetting } from "@/modules/Setting/stores/settingStore";

/**
 * 用户数据初始化服务
 * 负责用户登录时从存储中加载各个模块的数据到对应的 store
 */
export class UserDataInitService {
  /**
   * 初始化用户所有数据
   * @param username 用户名(可选,如果不提供则使用当前登录用户)
   */
  static async initUserData(username?: string): Promise<void> {
    try {
      const targetUsername = username || UserStoreService.getCurrentUser();

      if (!targetUsername) {
        throw new Error("没有找到用户信息,无法初始化数据");
      }

      console.log(`开始初始化用户 ${targetUsername} 的数据...`);

      // 并行加载各个模块的数据
      await Promise.all([
        this.initGoalData(targetUsername),
        this.initTaskData(targetUsername),
        this.initGoalDirData(targetUsername),
        this.initGoalReviewData(targetUsername),
        this.initReminderData(targetUsername),
        this.initRepositoryData(targetUsername),
        this.initSettingData(targetUsername),
      ]);

      console.log(`用户 ${targetUsername} 数据初始化完成`);
    } catch (error) {
      console.error(`用户数据初始化失败:`, error);
      throw error;
    }
  }

  /**
   * 初始化目标模块数据
   */
  private static async initGoalData(username: string): Promise<void> {
    try {
      const goalStore = useGoalStore();

      const [goalsResponse, recordsResponse] = await Promise.all([
        UserStoreService.readWithUsername<IGoal[]>(username, "goals"),
        UserStoreService.readWithUsername<IRecord[]>(username, "records"),
      ]);

      goalStore.$patch({
        goals:
          goalsResponse.success && goalsResponse.data ? goalsResponse.data : [],
        records:
          recordsResponse.success && recordsResponse.data
            ? recordsResponse.data
            : [],
      });

      const goalsCount =
        goalsResponse.success && goalsResponse.data
          ? goalsResponse.data.length
          : 0;
      const recordsCount =
        recordsResponse.success && recordsResponse.data
          ? recordsResponse.data.length
          : 0;

      console.log(`加载了 ${goalsCount} 个目标和 ${recordsCount} 条记录`);
    } catch (error) {
      console.error("目标数据初始化失败:", error);
      const goalStore = useGoalStore();
      goalStore.$patch({
        goals: [],
        records: [],
      });
      throw error;
    }
  }

  /**
   * 清空所有 store 数据(用户退出登录时调用)
   */
  static clearAllStoreData(): void {
    try {
      const goalStore = useGoalStore();

      // 重置各个 store
      goalStore.goals = [];
      goalStore.records = [];
      goalStore.initTempGoal();
      goalStore.initTempKeyResult();

      console.log("所有 store 数据已清空");
    } catch (error) {
      console.error("清空 store 数据失败:", error);
    }
  }

  /**
   * 保存用户数据到存储
   */
  static async saveUserData(): Promise<void> {
    try {
      if (!UserStoreService.isUserLoggedIn()) {
        throw new Error("用户未登录,无法保存数据");
      }

      const goalStore = useGoalStore();

      // 并行保存所有数据
      const results = await Promise.all([
        UserStoreService.write("goals", goalStore.goals),
      ]);

      // 检查保存结果
      const failedSaves = results.filter((result) => !result.success);
      if (failedSaves.length > 0) {
        console.error("部分数据保存失败:", failedSaves);
        throw new Error("部分数据保存失败");
      }

      console.log(`用户数据保存成功`);
    } catch (error) {
      console.error(`用户数据保存失败:`, error);
      throw error;
    }
  }

  /**
   * 获取用户存储列表
   */
  static async getUserStoreList(): Promise<string[]> {
    try {
      const response = await UserStoreService.list();
      if (response.success && response.data) {
        return response.data;
      }
      return [];
    } catch (error) {
      console.error("获取用户存储列表失败:", error);
      return [];
    }
  }
}

实现数据更改时,将数据保存到数据库中

注意事项:
ipc 通信要将数据序列化,所以调用 IPC 接口时要调用 JSON.stringify() 将数据序列化;
如果后端还要处理数据,则在后端再转回 JS 对象;
数据库中一般存储 JSON 数据。

先提示 有对象不能被克隆(未序列化)
然后又在读取对象时报错(ipc 前序列化了,数据库里也序列化了,导致双重序列化,数据出问题)

对简单的 storeService 进行封装,实现防抖功能

useStoreSave.ts 具体代码
// useStoreSave.ts
import { UserStoreService } from "@/shared/services/userStoreService";
import { ref, type Ref } from "vue";

interface StoreSaveOptions {
  delay?: number; // 防抖延迟,默认 1000ms
  onSuccess?: (storeName: string) => void; // 保存成功回调
  onError?: (storeName: string, error: any) => void; // 保存失败回调
}

export function useStoreSave(options: StoreSaveOptions = {}) {
  const { delay = 1000, onSuccess, onError } = options;
  const timeouts = new Map<string, NodeJS.Timeout>();
  const saving = ref<Set<string>>(new Set());

  /**
   * 防抖保存数据
   */
  const debounceSave = async <T>(
    storeName: string,
    data: T
  ): Promise<boolean> => {
    // 清除之前的定时器
    if (timeouts.has(storeName)) {
      clearTimeout(timeouts.get(storeName)!);
    }

    return new Promise((resolve) => {
      const timeout = setTimeout(async () => {
        try {
          saving.value.add(storeName);
          let JSON_data = JSON.stringify(data);
          const response = await UserStoreService.write(storeName, JSON_data);

          if (response.success) {
            onSuccess?.(storeName);
            resolve(true);
          } else {
            onError?.(storeName, response.message);
            resolve(false);
          }
        } catch (error) {
          onError?.(storeName, error);
          resolve(false);
        } finally {
          saving.value.delete(storeName);
          timeouts.delete(storeName);
        }
      }, delay);

      timeouts.set(storeName, timeout);
    });
  };

  /**
   * 立即保存数据
   */
  const saveImmediately = async <T>(
    storeName: string,
    data: T
  ): Promise<boolean> => {
    try {
      saving.value.add(storeName);

      let JSON_data = JSON.stringify(data);
      const response = await UserStoreService.write(storeName, JSON_data);

      if (response.success) {
        onSuccess?.(storeName);
        return true;
      } else {
        onError?.(storeName, response.message);
        return false;
      }
    } catch (error) {
      onError?.(storeName, error);
      return false;
    } finally {
      saving.value.delete(storeName);
    }
  };

  /**
   * 检查是否正在保存
   */
  const isSaving = (storeName?: string): boolean => {
    if (storeName) {
      return saving.value.has(storeName);
    }
    return saving.value.size > 0;
  };

  /**
   * 清理定时器
   */
  const cleanup = () => {
    timeouts.forEach((timeout) => clearTimeout(timeout));
    timeouts.clear();
    saving.value.clear();
  };

  return {
    debounceSave,
    saveImmediately,
    isSaving,
    cleanup,
    saving: saving as Ref<ReadonlySet<string>>,
  };
}

在 store 中使用:

在 settingStore 中的使用案例
import { defineStore } from "pinia";
import { useThemeStore } from "@/modules/Theme/themeStroe";
import { useStoreSave } from "@/shared/composables/useStoreSave";
import { setLanguage } from "@/i18n";

// ...existing interfaces...

// 在模块级别创建 autoSave 实例
let autoSaveInstance: ReturnType<typeof useStoreSave> | null = null;

function getAutoSave() {
  if (!autoSaveInstance) {
    autoSaveInstance = useStoreSave({
      onSuccess: (storeName) => console.log(`✓ ${storeName} 数据保存成功`),
      onError: (storeName, error) =>
        console.error(`✗ ${storeName} 数据保存失败:`, error),
    });
  }
  return autoSaveInstance;
}

export const useSettingStore = defineStore("setting", {
  actions: {
    // 自动保存方法
    async saveSettings(): Promise<boolean> {
      const autoSave = getAutoSave();
      return autoSave.debounceSave("settings", this.$state);
    },

    async saveSettingsImmediately(): Promise<boolean> {
      const autoSave = getAutoSave();
      return autoSave.saveImmediately("settings", this.$state);
    },

    // 检查保存状态
    isSavingSettings(): boolean {
      const autoSave = getAutoSave();
      return autoSave.isSaving("settings");
    },

    //在对应的增删改 服务中调用保存方法
    // 修改现有方法,添加自动保存
    async setTheme(themeMode: string) {
      this.themeMode = themeMode;
      const themeStore = useThemeStore();
      themeStore.setCurrentTheme(themeMode);

      // 自动保存
      const saveSuccess = await this.saveSettings();
      if (!saveSuccess) {
        console.error("主题设置保存失败");
      }
    },
  },
});

另一种调用方法:

actions: {
    // 初始化自动保存(在需要时调用)
    _initAutoSave() {
        if (!this._autoSave) {
          this._autoSave = useStoreSave({
            onSuccess: (storeName) => console.log(`✓ ${storeName} 数据保存成功`),
            onError: (storeName, error) => console.error(`✗ ${storeName} 数据保存失败:`, error),
          });
        }
        return this._autoSave;
      },

      // 自动保存方法
      async saveGoals() {
        const autoSave = this._initAutoSave();
        return autoSave.debounceSave('goals', this.goals);
      },

      async saveRecords() {
        const autoSave = this._initAutoSave();
        return autoSave.debounceSave('records', this.records);
      },

      async saveGoalsImmediately() {
        const autoSave = this._initAutoSave();
        return autoSave.saveImmediately('goals', this.goals);
      },

      async saveRecordsImmediately() {
        const autoSave = this._initAutoSave();
        return autoSave.saveImmediately('records', this.records);
      },

      // 检查保存状态
      isSavingGoals() {
        const autoSave = this._initAutoSave();
        return autoSave.isSaving('goals');
      },

      isSavingRecords() {
        const autoSave = this._initAutoSave();
        return autoSave.isSaving('records');
      },

      isSavingAny() {
        const autoSave = this._initAutoSave();
        return autoSave.isSaving();
      },

}
创建于 2025/1/1 更新于 2026/5/27