Vue 3 项目中全局配置管理的最佳实践

发布于 2025-03-20  77 次阅读


在现代 Web 应用中,用户个性化设置是一项常见需求,例如主题偏好、语言选择或时间格式等。这些设置需要持久化存储,并在用户界面上提供直观的配置选项。本文将基于一个典型的 Vue 3 项目,详细讲解如何实现全局配置管理,包括使用 Pinia 进行状态管理、与后端 API 交互、页面表单反显、更新配置以及支持重置和还原操作的完整流程。这种方法模块化、清晰易懂,是管理全局配置的最佳实践之一,方便在类似场景中复用。


整体流程与思想

实现全局配置管理的核心思想是将配置数据集中管理,并通过清晰的模块分工实现状态存储、数据获取与更新、以及用户界面的展示与交互。以下是整体流程的四个关键部分:

  1. 状态管理:使用 Pinia 创建一个全局 Store,集中存储用户的配置数据。
  2. API 交互:定义服务模块,与后端 API 通信,负责获取和更新配置数据。
  3. 逻辑封装:通过自定义 Hook 封装获取和更新配置的逻辑,供组件调用。
  4. 用户界面:基于表单组件展示配置,提供保存、重置和还原功能。

这种分层设计不仅提高了代码的可维护性,还能让每个模块职责单一,便于扩展和调试。


详细实现步骤

以下是基于所给代码的具体实现步骤,涵盖从状态管理到用户界面的完整流程。

1. 状态管理:使用 Pinia 存储配置

我们首先使用 Pinia 创建一个全局 Store,命名为 userSettingStore,用于管理用户的配置数据。这里以时间相关的配置为例,包括 firstDayMode(每年第一周是否包含 1 月 1 日)和 startOfWeek(一周的起始天)。

// src/store/user-setting-store/index.ts
import { defineStore } from 'pinia';
import { FirstDayModeEnum, StartOfWeekEnum } from './typing';

export const useUserSettingStore = defineStore('userSettingStore', {
  state: () => ({
    userSetting: {
      firstDayMode: FirstDayModeEnum.A, // 默认包含 1 月 1 日
      startOfWeek: StartOfWeekEnum.MONDAY, // 默认周一为起始天
    },
  }),
  actions: {
    setUserSetting(userSetting: any) {
      this.userSetting = userSetting; // 更新配置数据
    },
  },
});

为了确保类型安全,我们在 typing.ts 中定义了枚举和接口:

// src/store/user-setting-store/typing.ts
export enum FirstDayModeEnum {
  A = 'A', // 包含 1 月 1 日
  B = 'B', // 不包含 1 月 1 日
}

export enum StartOfWeekEnum {
  MONDAY = '1', // 周一
  TUESDAY = '2', // 周二
  WEDNESDAY = '3', // 周三
  THURSDAY = '4', // 周四
  FRIDAY = '5', // 周五
  SATURDAY = '6', // 周六
  SUNDAY = '7', // 周日
}

export interface UserSettingForm {
  firstDayMode?: FirstDayModeEnum;
  startOfWeek?: StartOfWeekEnum;
}

思路:Pinia 提供了响应式的状态管理能力,通过 state 定义初始值,actions 提供更新方法。枚举的使用增强了代码的可读性和类型约束。


2. API 交互:与后端通信

接下来,我们定义服务模块 permission/index.ts,包含两个函数:getUserHabit 用于获取用户配置,setUserHabit 用于更新配置。这两个函数通过 HTTP 请求与后端交互。

// src/service/permission/index.ts
import http from '@/utils/http';

export async function getUserHabit(param: any) {
  return http.get(`/userHabit?code=${param.code}&module=${param.module}`, {
    baseURL: import.meta.env.VITE_APP_API_PREFIX_USER_CENTER,
  });
}

export async function setUserHabit(params: any) {
  return http.post('/userHabit', params, {
    baseURL: import.meta.env.VITE_APP_API_PREFIX_USER_CENTER,
  });
}

思路:服务模块封装了网络请求逻辑,使用环境变量配置 API 前缀,确保灵活性和可维护性。getUserHabit 通过查询参数获取数据,setUserHabit 通过 POST 请求提交更新。


3. 自定义 Hook:封装逻辑

为了让组件更专注于展示逻辑,我们创建了一个自定义 Hook useUserHabit.ts,封装了获取和更新配置的逻辑,并提供了加载状态和配置数据。

// src/hooks/useUserHabit.ts
import { ref, computed, onMounted } from 'vue';
import { getUserHabit, setUserHabit } from '@/service/permission';
import { useUserSettingStore } from '@/store/user-setting-store';

export const useUserHabit = () => {
  const userSettingStore = useUserSettingStore();
  const userSetting = computed(() => userSettingStore.userSetting); // 响应式配置数据
  const isLoading = ref(false); // 加载状态

  const fetchUserHabit = async () => {
    isLoading.value = true;
    const res = await getUserHabit({
      code: 'userSetting',
      module: 'yms',
    });
    const { data } = res;
    userSettingStore.setUserSetting(JSON.parse(data.data)); // 更新 Store
    isLoading.value = false;
  };

  const updateUserHabit = async (newUserSetting: any) => {
    isLoading.value = true;
    await setUserHabit({
      code: 'userSetting',
      module: 'yms',
      habit: JSON.stringify(newUserSetting),
    });
    isLoading.value = false;
    fetchUserHabit(); // 更新后重新获取数据
  };

  onMounted(() => {
    fetchUserHabit(); // 组件挂载时获取数据
  });

  return {
    isLoading,
    userSetting,
    fetchUserHabit,
    updateUserHabit,
  };
};

思路:Hook 使用 computed 提供响应式的配置数据,isLoading 管理加载状态。fetchUserHabitupdateUserHabit 分别处理数据获取和更新,onMounted 确保组件加载时自动获取最新配置。


4. 用户界面:表单展示与交互

最后,我们在 UserSetting.vue 中实现用户界面,使用 Element Plus 的表单组件展示配置,并提供保存、重置和还原按钮。逻辑通过 useUserSettingForm Hook 管理。

组件代码

// src/views/user-setting/UserSetting.vue
<template>
  <div class="user-setting">
    <div class="layout-container">
      <div class="right-panel">
        <div class="setting-content">
          <div class="setting-section">
            <div class="section-h1">时间</div>
            <div class="section-h2">自定义 Week</div>
            <div class="section-h2-content">
              <div class="line-item">
                <div class="sub-label">每年第一周是否包含1月1日</div>
                <el-radio-group v-model="form.firstDayMode">
                  <el-radio :label="FirstDayModeEnum.A">包含</el-radio>
                  <el-radio :label="FirstDayModeEnum.B">不包含</el-radio>
                </el-radio-group>
              </div>
              <div class="line-item">
                <div class="sub-label">第一周的起始天</div>
                <el-radio-group v-model="form.startOfWeek">
                  <el-radio :label="StartOfWeekEnum.MONDAY">周一</el-radio>
                  <el-radio :label="StartOfWeekEnum.SUNDAY">周日</el-radio>
                  <!-- 其他选项省略 -->
                </el-radio-group>
              </div>
            </div>
          </div>
          <div class="action-buttons">
            <el-button type="primary" :loading="isLoading" @click="handleSave">保存设置</el-button>
            <el-button :loading="isLoading" @click="initForm">重置表单</el-button>
            <el-button type="danger" :loading="isLoading" @click="handleReset">还原设置</el-button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { FirstDayModeEnum, StartOfWeekEnum } from '@/store/user-setting-store/typing';
import { useUserSettingForm } from './hooks/useUserSettingForm';

const activeTab = ref('data');
const { form, isLoading, initForm, handleSave, handleReset } = useUserSettingForm();
</script>

表单逻辑 Hook

// src/views/user-setting/hooks/useUserSettingForm.ts
import { onMounted, ref, watch } from 'vue';
import { useUserHabit } from '@/hooks/useUserHabit';
import { UserSettingForm } from '@/store/user-setting-store/typing';
import { DEFAULT_USER_SETTING } from '../constants';

export const useUserSettingForm = () => {
  const form = ref<UserSettingForm>(DEFAULT_USER_SETTING); // 表单数据
  const { isLoading, userSetting, fetchUserHabit, updateUserHabit } = useUserHabit();

  const initForm = () => {
    form.value = userSetting.value || DEFAULT_USER_SETTING; // 重置表单为当前配置或默认值
  };

  const resetUserSetting = async () => {
    await updateUserHabit(DEFAULT_USER_SETTING); // 还原为默认配置
  };

  const handleSave = async () => {
    try {
      await updateUserHabit(form.value); // 保存表单数据
      ElMessage.success('设置保存成功');
    } catch (error) {
      ElMessage.error('设置保存失败');
    }
  };

  const handleReset = async () => {
    try {
      await resetUserSetting();
      ElMessage.success('设置还原成功');
    } catch (error) {
      ElMessage.error('设置还原失败');
    }
  };

  watch(userSetting, () => {
    initForm(); // 配置变化时同步表单
  }, { deep: true });

  onMounted(() => {
    initForm(); // 初始化表单
  });

  return {
    form,
    isLoading,
    userSetting,
    initForm,
    fetchUserHabit,
    updateUserHabit,
    resetUserSetting,
    handleSave,
    handleReset,
  };
};

默认配置

// src/views/user-setting/constants.ts
import { FirstDayModeEnum, StartOfWeekEnum } from '@/store/user-setting-store/typing';

export const DEFAULT_USER_SETTING = {
  firstDayMode: FirstDayModeEnum.A,
  startOfWeek: StartOfWeekEnum.MONDAY,
};

思路

  • 界面:使用 el-radio-group 展示选项,双向绑定表单数据。
  • 逻辑useUserSettingForm 管理表单状态,watch 监听配置变化以同步表单,handleSavehandleReset 分别处理保存和还原操作。
  • 功能
    • 保存设置:将表单数据提交到后端并刷新。
    • 重置表单:将表单恢复为当前配置。
    • 还原设置:将配置恢复为默认值并更新后端。

总结

通过以上步骤,我们实现了一个完整的全局配置管理方案:

  • 状态管理:Pinia 集中存储配置数据,提供响应式支持。
  • API 交互:服务模块封装与后端通信,保证数据持久化。
  • 逻辑封装:自定义 Hook 解耦业务逻辑,提升复用性。
  • 用户界面:表单直观展示配置,支持修改、保存、重置和还原。

这种架构清晰、模块化,易于维护和扩展,非常适合需要在 Vue 3 项目中管理用户个性化设置的场景。开发者可以根据具体需求调整配置项和界面样式,快速复用这一最佳实践。


A Student on the way to full stack of Web3.