web-storage-cache 是对浏览器 localStorage/sessionStorage 的增强封装库,解决了原生存储的核心痛点 ——过期时间数据类型自动转换命名空间隔离批量操作,在 CRM 系统中主要用于「用户偏好设置」「临时筛选条件」「token 缓存」「本地字典数据」等场景,是本地存储的 “增强版方案”。

一、核心定位(为什么选 web-storage-cache?)

原生 localStorage 痛点 & web-storage-cache 解决方案:

表格

原生 localStorage 问题

web-storage-cache 优势

CRM 适配场景

仅支持字符串存储(需手动 JSON 转换)

自动支持 Object/Array/Number/Boolean 类型

缓存客户筛选条件(对象)、订单列表(数组)

无过期时间(数据永久存储)

支持设置过期时间(秒 / 毫秒 / 日期)

缓存临时 token(2 小时过期)、验证码(5 分钟过期)

无命名空间(易键名冲突)

支持命名空间隔离,不同模块数据分开存储

客户模块 / 订单模块 / 系统模块数据隔离

无批量操作(需循环读写)

支持批量设置 / 获取 / 删除数据

批量缓存字典数据(行业类型、订单状态)

无加密(敏感数据明文存储)

可结合加密库实现加密存储(扩展)

缓存用户手机号、临时凭证等敏感数据

二、快速集成(Vue3 + Vite 项目)

1. 安装依赖

bash

运行

# 核心库
npm install web-storage-cache -S
# TS 类型(可选,社区维护)
npm install @types/web-storage-cache -D

2. 封装全局存储工具(CRM 首选)

封装统一的存储工具类,统一配置命名空间、过期时间,结合加密实现敏感数据保护:

typescript

运行

// src/utils/storage.ts
import WebStorageCache from 'web-storage-cache';
import { aesEncrypt, aesDecrypt } from '@/utils/crypto'; // 之前封装的加密工具

// 初始化 WebStorageCache 实例
// 配置1:命名空间(避免不同项目/模块键名冲突)
// 配置2:存储类型(localStorage/sessionStorage)
const cache = new WebStorageCache({
  namespace: 'crm_2026_', // CRM 专属命名空间
  storage: 'localStorage', // 默认用 localStorage,临时数据用 sessionStorage
  exp: 0 // 默认永不过期(按需覆盖)
});

// 临时存储实例(sessionStorage,页面关闭即失效)
const sessionCache = new WebStorageCache({
  namespace: 'crm_temp_',
  storage: 'sessionStorage'
});

/**
 * 通用存储方法(支持加密)
 * @param key 存储键名
 * @param value 存储值(任意类型)
 * @param expire 过期时间(秒,0=永不过期)
 * @param isEncrypt 是否加密(默认 false)
 * @param isSession 是否用 sessionStorage(默认 false)
 */
export const setCache = (
  key: string,
  value: any,
  expire = 0,
  isEncrypt = false,
  isSession = false
) => {
  const targetCache = isSession ? sessionCache : cache;
  // 敏感数据加密
  const finalValue = isEncrypt ? aesEncrypt(value) : value;
  // 设置存储(支持过期时间)
  targetCache.set(key, finalValue, { exp: expire });
};

/**
 * 通用获取方法(支持解密)
 * @param key 存储键名
 * @param isEncrypt 是否加密存储(需和 set 一致)
 * @param isSession 是否用 sessionStorage
 * @returns 存储值(自动转换类型)
 */
export const getCache = (
  key: string,
  isEncrypt = false,
  isSession = false
) => {
  const targetCache = isSession ? sessionCache : cache;
  const value = targetCache.get(key);
  // 过期/不存在返回 null
  if (value === null || value === undefined) return null;
  // 解密敏感数据
  return isEncrypt ? aesDecrypt(value) : value;
};

/**
 * 删除指定缓存
 * @param key 存储键名
 * @param isSession 是否用 sessionStorage
 */
export const removeCache = (key: string, isSession = false) => {
  const targetCache = isSession ? sessionCache : cache;
  targetCache.remove(key);
};

/**
 * 批量设置缓存
 * @param data 键值对对象 { key1: value1, key2: value2 }
 * @param expire 统一过期时间(秒)
 * @param isSession 是否用 sessionStorage
 */
export const setCacheBatch = (
  data: Record<string, any>,
  expire = 0,
  isSession = false
) => {
  const targetCache = isSession ? sessionCache : cache;
  Object.keys(data).forEach(key => {
    targetCache.set(key, data[key], { exp: expire });
  });
};

/**
 * 批量获取缓存
 * @param keys 键名数组
 * @param isSession 是否用 sessionStorage
 * @returns 键值对对象
 */
export const getCacheBatch = (keys: string[], isSession = false) => {
  const targetCache = isSession ? sessionCache : cache;
  const result: Record<string, any> = {};
  keys.forEach(key => {
    result[key] = targetCache.get(key);
  });
  return result;
};

/**
 * 清空指定命名空间下的所有缓存
 * @param isSession 是否清空 sessionStorage
 */
export const clearCache = (isSession = false) => {
  const targetCache = isSession ? sessionCache : cache;
  targetCache.clear();
};

/**
 * 获取缓存剩余过期时间(秒)
 * @param key 存储键名
 * @param isSession 是否用 sessionStorage
 * @returns 剩余秒数(-1=永不过期,0=已过期)
 */
export const getCacheExpire = (key: string, isSession = false) => {
  const targetCache = isSession ? sessionCache : cache;
  return targetCache.getExpire(key);
};

// 导出原始实例(自定义扩展用)
export { cache, sessionCache };

三、CRM 实战场景(核心用法)

场景 1:缓存用户 Token(带过期时间 + 加密)

CRM 登录后缓存 Token,设置 2 小时过期,且加密存储避免明文泄露:

vue

<script setup lang="ts">
import { setCache, getCache, removeCache } from '@/utils/storage';
import { useUserStore } from '@/stores/user';

const userStore = useUserStore();

// 登录成功后缓存 Token
const handleLoginSuccess = (tokenInfo) => {
  // Token 加密存储,2小时(7200秒)过期
  setCache('access_token', tokenInfo.accessToken, 7200, true);
  setCache('refresh_token', tokenInfo.refreshToken, 86400, true); // 刷新令牌 24 小时过期
  // 缓存用户基本信息(不加密,永不过期)
  setCache('user_info', {
    id: tokenInfo.userId,
    name: tokenInfo.userName,
    role: tokenInfo.role
  });
};

// 初始化:从缓存获取 Token
const initToken = () => {
  const accessToken = getCache('access_token', true);
  const userInfo = getCache('user_info');
  if (accessToken && userInfo) {
    userStore.setToken(accessToken);
    userStore.setUserInfo(userInfo);
  }
};

// 退出登录:清除 Token 缓存
const handleLogout = () => {
  removeCache('access_token');
  removeCache('refresh_token');
  removeCache('user_info');
  userStore.clearToken();
};

// 页面初始化时调用
initToken();
</script>

场景 2:缓存客户筛选条件(临时存储)

CRM 客户列表的筛选条件(如行业、地区、状态)缓存到 sessionStorage,页面刷新不丢失,关闭页面清空:

vue

<template>
  <div class="customer-filter">
    <el-select v-model="filterForm.industry" placeholder="选择行业" @change="saveFilter">
      <el-option label="电商" value="ecommerce" />
      <el-option label="金融" value="finance" />
    </el-select>
    <el-select v-model="filterForm.region" placeholder="选择地区" @change="saveFilter">
      <el-option label="华东" value="east" />
      <el-option label="华北" value="north" />
    </el-select>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { setCache, getCache } from '@/utils/storage';

// 筛选表单
const filterForm = ref({
  industry: '',
  region: '',
  status: 'active'
});

// 保存筛选条件到 sessionStorage(临时存储,页面关闭失效)
const saveFilter = () => {
  setCache('customer_filter', filterForm.value, 0, false, true);
};

// 初始化:从缓存恢复筛选条件
onMounted(() => {
  const cachedFilter = getCache('customer_filter', false, true);
  if (cachedFilter) {
    filterForm.value = { ...filterForm.value, ...cachedFilter };
    // 恢复筛选后立即查询数据
    // queryCustomerList(filterForm.value);
  }
});
</script>

场景 3:缓存字典数据(批量 + 长期)

CRM 中的通用字典数据(如订单状态、客户类型)批量缓存,设置 1 天过期,减少接口请求:

vue

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { setCacheBatch, getCacheBatch, getCacheExpire } from '@/utils/storage';
import { getDictData } from '@/api/common';

// 字典数据
const dictData = ref({
  orderStatus: [],
  customerType: [],
  industryType: []
});

// 加载字典数据
const loadDictData = async () => {
  // 1. 先从缓存获取
  const cachedDict = getCacheBatch(['orderStatus', 'customerType', 'industryType']);
  const expire = getCacheExpire('orderStatus');
  
  // 2. 缓存未过期且有数据,直接使用
  if (expire > 0 && cachedDict.orderStatus?.length) {
    dictData.value = cachedDict;
    return;
  }

  // 3. 缓存过期/无数据,从接口获取
  const res = await getDictData(['orderStatus', 'customerType', 'industryType']);
  dictData.value = res.data;
  
  // 4. 批量缓存字典数据,1天(86400秒)过期
  setCacheBatch(res.data, 86400);
};

onMounted(() => {
  loadDictData();
});
</script>

场景 4:缓存临时表单数据(防丢失)

CRM 新建订单 / 客户时,缓存临时表单数据,防止页面刷新 / 意外关闭导致数据丢失:

vue

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { setCache, getCache, removeCache } from '@/utils/storage';

// 新建客户表单
const customerForm = ref({
  name: '',
  phone: '',
  industry: '',
  address: '',
  remark: ''
});

// 表单是否提交中
const submitting = ref(false);

// 监听表单变化,实时缓存(5分钟过期)
watch(
  customerForm,
  (newVal) => {
    if (!submitting.value) {
      setCache('temp_customer_form', newVal, 300, false, true); // 300秒=5分钟
    }
  },
  { deep: true, immediate: true }
);

// 初始化:恢复临时表单数据
onMounted(() => {
  const cachedForm = getCache('temp_customer_form', false, true);
  if (cachedForm) {
    customerForm.value = cachedForm;
  }
});

// 提交表单:成功后清除临时缓存
const submitForm = async () => {
  submitting.value = true;
  try {
    // await createCustomer(customerForm.value);
    removeCache('temp_customer_form', true); // 清除临时缓存
    // ElMessage.success('客户创建成功');
  } catch (err) {
    // ElMessage.error('创建失败');
  } finally {
    submitting.value = false;
  }
};
</script>

四、核心避坑点

1. 过期时间单位错误

  • 问题:设置过期时间时把 “毫秒” 当成 “秒”,导致数据立即过期;
  • 解决方案:
    • web-storage-cache 的 exp 参数单位是(如 2 小时 = 7200 秒);
    • 如需用毫秒,可转换:exp: ms / 1000

2. 命名空间冲突

  • 问题:多个项目 / 环境共用同一域名,缓存键名冲突;
  • 解决方案:
    • 初始化时设置唯一 namespace(如 crm_prod_/crm_test_);
    • 不同模块用不同子前缀(如 customer_/order_)。

3. 加密 / 解密不匹配

  • 问题:存储时加密,获取时未解密,导致数据是加密字符串;
  • 解决方案:
    • setCachegetCacheisEncrypt 参数必须一致;
    • 敏感数据(Token / 手机号)强制加密存储。

4. 大数据存储导致性能问题

  • 问题:缓存大量数组 / 对象(如上万条订单数据),导致页面卡顿;
  • 解决方案:
    • 只缓存核心数据,而非全量数据;
    • 定期清理过期缓存(如登录时清理无用数据);
    • 大数据优先用后端分页,而非本地缓存。

5. 浏览器禁用 localStorage

  • 问题:部分浏览器 / 隐私模式禁用本地存储,调用 setCache 报错;
  • 解决方案:
    • 封装容错逻辑,捕获存储异常:typescript运行
export const setCache = (key, value, expire = 0, isEncrypt = false, isSession = false) => {
  try {
    const targetCache = isSession ? sessionCache : cache;
    const finalValue = isEncrypt ? aesEncrypt(value) : value;
    targetCache.set(key, finalValue, { exp: expire });
  } catch (err) {
    console.warn('本地存储失败:', err);
    // 降级方案:用内存临时存储
    window.__tempCache = window.__tempCache || {};
    window.__tempCache[key] = value;
  }
};

五、扩展功能(CRM 进阶)

1. 缓存自动刷新(如 Token 刷新)

监听 Token 过期前 5 分钟自动刷新:

typescript

运行

// 检查 Token 剩余时间,提前刷新
const checkTokenExpire = () => {
  const expire = getCacheExpire('access_token');
  // 剩余时间小于 5 分钟(300秒),触发刷新
  if (expire > 0 && expire < 300) {
    // refreshTokenApi().then(res => {
    //   setCache('access_token', res.accessToken, 7200, true);
    // });
  }
};

// 定时检查(每分钟一次)
setInterval(checkTokenExpire, 60 * 1000);

2. 多环境缓存隔离

根据环境变量设置不同命名空间:

typescript

运行

const namespace = import.meta.env.VITE_ENV === 'production' 
  ? 'crm_prod_' 
  : import.meta.env.VITE_ENV === 'test' 
    ? 'crm_test_' 
    : 'crm_dev_';

const cache = new WebStorageCache({
  namespace,
  storage: 'localStorage'
});

六、总结(CRM 开发最佳实践)

  1. 封装统一工具:所有本地存储通过封装的 setCache/getCache 操作,统一处理加密、过期、命名空间;
  2. 场景化存储策略
    • 永久数据(用户偏好):localStorage + 无过期;
    • 临时数据(筛选条件 / 表单):sessionStorage + 短期过期;
    • 敏感数据(Token / 手机号):加密存储 + 合理过期;
    • 字典数据:批量缓存 + 1 天过期(减少接口请求);
  1. 容错与降级:处理浏览器禁用存储、大数据存储异常等场景,避免页面崩溃;
  2. 性能优化
    • 避免缓存超大数据,优先后端分页;
    • 定期清理过期缓存,减少存储占用;
    • 表单数据实时缓存,防止意外丢失;
  1. 安全注意
    • 敏感数据必须加密存储,避免明文泄露;
    • Token 等核心数据设置合理过期时间,降低泄露风险。

web-storage-cache 是 Vue3 + CRM 本地存储的 “增强工具”,解决了原生存储的所有痛点;封装后的工具类可直接复用,兼顾安全性、易用性和性能,能完美支撑 CRM 系统的所有本地存储需求。

Vue3 + Pinia 开发者会纠结的点 ——并非完全不需要 web-storage-cache,而是两者适用场景不同,可互补而非互替。下面我会从「核心差异」「场景选择」「最佳实践」三个维度讲清楚,帮你判断何时该用 Pinia 持久化插件,何时该用 web-storage-cache。

一、核心差异:Pinia 持久化插件 vs web-storage-cache

先看两者的核心定位和能力对比,就能明白为什么不能完全替代:

表格

特性

Pinia-plugin-persistedstate(Pinia 持久化插件)

web-storage-cache

核心定位

专为 Pinia Store 设计,自动同步 Store 数据到本地存储

通用本地存储增强工具,独立于框架,操作原生 localStorage/sessionStorage

数据粒度

以「Store 模块」为单位持久化(如 userStore

整个模块)

以「单个键值对」为单位,支持精细化控制(如只存一个 token

、一个筛选条件)

过期时间

无原生支持(需手动扩展)

核心能力,支持秒 / 毫秒 / 日期级过期,精准控制单条数据有效期

命名空间隔离

需手动给不同 Store 加前缀,易冲突

初始化时支持全局命名空间,天然隔离不同模块 / 项目数据

加密能力

无原生支持(需手动结合加密库)

可无缝结合加密工具,单条数据可按需加密(如 Token 加密,普通数据不加密)

批量操作

无原生支持(需循环操作 Store)

支持批量设置 / 获取 / 删除,适合字典数据等批量缓存场景

容错处理

无原生容错,存储失败会导致 Store 同步异常

可封装容错逻辑,适配浏览器禁用本地存储的场景

适用场景

全局状态持久化(如用户信息、全局主题、侧边栏状态)

精细化本地存储(如临时筛选条件、单条 Token、表单草稿、字典缓存)

二、什么时候可以只用 Pinia 持久化插件?

如果你的 CRM 项目中,本地存储需求仅满足以下场景,确实可以不用 web-storage-cache:

  1. 只需要持久化 Pinia Store 中的全局状态:比如 userStore(用户信息)、appStore(主题 / 布局)、cartStore(购物车,若有),且这些状态不需要设置过期时间
  2. 无需精细化控制单条数据:比如整个 userStore 要么全存,要么全清,不需要只存其中的 token 而不存 userName
  3. 无加密 / 过期 / 批量操作需求:比如存储的都是非敏感数据(如主题色、侧边栏折叠状态),不需要加密,也不需要设置过期。

示例:用 Pinia 持久化插件实现用户信息持久化(无过期)

typescript

运行

// stores/user.ts
import { defineStore } from 'pinia';
import { createPersistedState } from 'pinia-plugin-persistedstate';

export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userName: '',
    role: ''
  }),
  actions: {
    setToken(token) {
      this.token = token;
    },
    clearUser() {
      this.$reset();
    }
  },
  // 持久化配置:默认存 localStorage,以 store 名为键
  persist: true
});

// main.ts 注册插件
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

三、什么时候必须用 web-storage-cache?

只要你的 CRM 项目有以下需求,Pinia 持久化插件就满足不了,必须用 web-storage-cache:

场景 1:需要给单条数据设置过期时间

比如:

  • Token 需 2 小时过期,刷新 Token 需 24 小时过期;
  • 表单草稿需 5 分钟过期(避免长期缓存无效数据);
  • 字典数据需 1 天过期(定期刷新最新字典)。

Pinia 持久化插件只能同步 Store 数据,但无法给 userStore.token 单独设置 2 小时过期 —— 要么整个 userStore 永不过期,要么手动写大量代码扩展过期逻辑,远不如 web-storage-cache 简洁:

typescript

运行

// web-storage-cache 一行设置 Token 过期,简单高效
setCache('access_token', token, 7200, true); // 7200秒=2小时,加密存储

场景 2:需要精细化控制单条数据,而非整个 Store

比如:

  • 客户列表的筛选条件(仅缓存 industry/region/status 三个字段,而非整个 customerStore);
  • 临时表单草稿(仅缓存当前编辑的客户表单,不需要关联 Store);
  • 单独缓存一个验证码(60 秒过期),无需放入 Store。

这些场景下,用 Pinia 持久化插件会 “大材小用”—— 为了存一个筛选条件,要新建一个 Store,还要配置持久化,反而增加代码冗余;而 web-storage-cache 可直接操作单条数据,更轻量。

场景 3:需要加密 / 批量操作 / 命名空间隔离

比如:

  • Token 需加密存储,而用户昵称无需加密(web-storage-cache 可单条数据按需加密,Pinia 插件需整个 Store 加密 / 解密);
  • 批量缓存 10 个字典数据(web-storage-cache 批量设置,Pinia 需循环给 Store 赋值);
  • 多环境 / 多项目共用域名(web-storage-cache 命名空间隔离,避免缓存冲突,Pinia 插件需手动加前缀)。

场景 4:非 Store 相关的临时存储

比如:

  • 页面级临时数据(如当前页面的分页页码、排序方式),不需要全局共享,仅页面刷新保留;
  • 下载 / 上传的临时进度,不需要放入 Store,仅本地临时存储。

四、CRM 项目最佳实践:两者结合使用

在实际的 CRM 开发中,最优方案是两者结合,各司其职:

1. Pinia 持久化插件:负责「全局状态」的持久化

  • 存储范围:需要全局共享、无过期需求、非敏感的状态(如用户基本信息、全局主题、侧边栏折叠状态、系统配置);
  • 优势:自动同步 Store 和本地存储,无需手动调用 set/get,符合 Vue 响应式思维。

2. web-storage-cache:负责「精细化本地存储」

  • 存储范围:
    • 敏感数据(Token、刷新令牌):加密 + 精准过期;
    • 临时数据(筛选条件、表单草稿、验证码):sessionStorage + 短期过期;
    • 批量数据(字典缓存):批量操作 + 1 天过期;
    • 非全局数据(页面级临时状态):独立于 Store,轻量存储。

示例:CRM 项目中的典型结合方式

typescript

运行

// 1. Pinia 持久化:存储全局用户基本信息(无过期,非敏感)
export const useUserStore = defineStore('user', {
  state: () => ({
    userName: '',
    role: '',
    avatar: ''
  }),
  persist: true // 自动存 localStorage
});

// 2. web-storage-cache:存储 Token(加密 + 2小时过期)
const handleLogin = async (loginForm) => {
  const res = await loginApi(loginForm);
  // Pinia 存非敏感信息
  const userStore = useUserStore();
  userStore.userName = res.userName;
  userStore.role = res.role;
  // web-storage-cache 存敏感 Token(加密 + 过期)
  setCache('access_token', res.accessToken, 7200, true);
  setCache('refresh_token', res.refreshToken, 86400, true);
};

// 3. web-storage-cache:存储客户筛选条件(sessionStorage + 无过期)
const saveFilter = () => {
  setCache('customer_filter', filterForm.value, 0, false, true);
};

总结

关键点回顾

  1. Pinia 持久化插件≠web-storage-cache:前者是「全局状态同步工具」,后者是「精细化本地存储工具」,核心定位不同;
  2. 无需互相替代,而是互补:Pinia 插件管 “全局状态持久化”,web-storage-cache 管 “单条数据精细化存储”;
  3. 取舍原则
    • 只存 Pinia Store 全局状态、无过期 / 加密需求 → 只用 Pinia 插件;
    • 需过期、加密、批量操作、精细化单条存储 → 必须用 web-storage-cache;
    • 实际 CRM 项目 → 两者结合,各司其职,效率最高。

简单来说:Pinia 持久化插件解决 “Store 数据不丢” 的问题,web-storage-cache 解决 “本地存储用得爽” 的问题 —— 前者是框架层面的状态同步,后者是通用层面的存储增强,缺一不可。

你的这个想法很直接 ——“既然都要持久化,那把所有数据都塞到缓存里(不管是 Pinia 持久化还是 web-storage-cache)不就行了?” 理论上可以这么做,但在实际的 CRM 项目中,这么做会带来性能、维护、安全三大核心问题,反而得不偿失。下面我会从「为什么不能全存」「哪些能存 / 哪些不能存」「正确的存储策略」三个维度讲清楚,帮你避开这个坑。

一、为什么 “全存储到缓存里” 不可行?

1. 性能问题:缓存膨胀导致页面卡顿

浏览器的 localStorage/sessionStorage 有存储上限(一般是 5MB),CRM 系统的核心数据量远不止这个量级:

  • 如果你把客户列表(几百 / 几千条)、订单明细、字典数据、表单草稿、Token、用户信息全塞进去,很快会触发存储上限,导致:
    • 新数据存不进去,页面报 QuotaExceededError 错误;
    • 每次读取 / 写入缓存时,浏览器需要遍历大量数据,页面加载速度变慢、操作卡顿;
    • 甚至会影响其他依赖本地存储的功能(如第三方插件、埋点工具)。

举个例子:一个客户信息对象约 1KB,存 5000 条就占满 5MB 上限,而 CRM 中客户数远不止这个数 —— 全存缓存等于 “把数据库搬到浏览器里”,完全违背前端缓存的设计初衷。

2. 维护问题:数据冗余 + 同步混乱

“全存缓存” 会让缓存里的数据和后端数据、Pinia 状态形成 “三方同步”,极易出现不一致:

  • 数据冗余:比如客户信息在 Pinia 里存一份、缓存里存一份、后端数据库里存一份,修改时要同时改三处,漏改一处就会导致数据错乱(比如改了 Pinia 里的客户名称,没更缓存,刷新页面又变回旧名称);
  • 过期失控:所有数据都存缓存,你无法精准控制 “哪些数据该过期、哪些该永久存”—— 比如 Token 需 2 小时过期,客户筛选条件只需页面级临时存储,全存后要么全设过期(丢了永久数据),要么全不设过期(Token 长期有效,有安全风险);
  • 调试困难:缓存里塞满了各种数据,排查问题时要翻几十上百个键值对,分不清哪些是有用的、哪些是冗余的,维护成本翻倍。

3. 安全问题:敏感数据泄露风险

CRM 里有大量敏感数据(客户手机号、身份证、订单金额、企业机密),如果 “全存缓存”:

  • 缓存是明文存储在浏览器里的(即使加密,全存也会增加解密风险),一旦浏览器被劫持 / XSS 攻击,所有敏感数据都会泄露;
  • 缓存数据没有 “权限隔离”:比如普通员工的账号能缓存到管理员的客户数据,导致权限越界;
  • 过期数据未清理:比如离职员工的 Token 还存在缓存里,若未及时清理,可能被恶意利用。

二、缓存的核心原则:“按需存储” 而非 “全量存储”

前端缓存的本质是“临时缓存高频访问的少量核心数据”,而非 “存储全量数据”。针对 CRM 系统,我帮你梳理了「能存」和「不能存」的边界:

✅ 可以存到缓存里的(核心是 “少量、高频、非全量”)

表格

数据类型

存储方式

原因

全局状态(用户信息 / 主题)

Pinia 持久化插件

少量、全局共享、无过期需求

敏感凭证(Token / 刷新令牌)

web-storage-cache(加密 + 过期)

少量、高频使用、需精准过期

临时数据(筛选条件 / 表单草稿)

web-storage-cache(sessionStorage)

页面级临时存储,关闭页面可清空

高频字典数据(订单状态 / 行业类型)

web-storage-cache(批量 + 1 天过期)

减少接口请求,1 天过期保证数据新鲜

用户偏好(分页大小 / 排序方式)

Pinia 持久化插件

少量、个性化设置,无需频繁请求后端

❌ 绝对不能存到缓存里的

表格

数据类型

原因

全量列表数据(客户 / 订单列表)

数据量大、易过期、占满存储上限

大文件 / 二进制数据(合同图片 / 附件)

缓存不支持二进制存储,且体积远超上限

实时性要求高的数据(订单实时状态)

缓存数据易滞后,需实时从后端获取

超敏感数据(客户身份证 / 银行卡)

即使加密,缓存也有泄露风险,仅后端存储

临时会话数据(验证码 / 临时链接)

应存在后端 Redis,前端仅存临时标识

三、CRM 项目的正确存储策略:“分层存储”

与其 “全存缓存”,不如按数据的 “使用场景 + 重要性” 做分层存储,既保证性能,又兼顾安全和维护性:

第一层:内存层(Pinia 非持久化)

  • 存储内容:临时计算数据(如页面分页结果、筛选后的客户列表、表单临时校验状态);
  • 特点:页面刷新 / 关闭即丢失,无需持久化,用完即弃;
  • 优势:响应式、操作快,不占用缓存空间。

第二层:缓存层(Pinia 持久化 + web-storage-cache)

  • 存储内容:少量核心持久化数据(Token、用户信息、字典数据、筛选条件);
  • 规则
    • 全局状态用 Pinia 持久化(无过期、非敏感);
    • 精细化数据用 web-storage-cache(加密、过期、批量操作);
    • 缓存数据量控制在 1MB 以内,定期清理过期数据。

第三层:后端层(数据库 / Redis)

  • 存储内容:全量业务数据(客户、订单、合同、附件)、超敏感数据、实时数据;
  • 特点:前端仅在需要时通过接口获取,不缓存全量数据;
  • 优势:数据安全、可扩展、支持权限控制,是 CRM 数据的 “唯一可信源”。

四、实操示例:CRM 正确的存储方式(对比 “全存” 和 “分层存”)

反面示例:全存缓存(错误做法)

typescript

运行

// ❶ 把全量客户列表存缓存(5000条,占满5MB)
setCache('all_customers', allCustomerList); 
// ❷ 把订单明细存缓存(冗余)
setCache('order_detail_123', orderDetail);
// ❸ 把Token和客户身份证全存缓存(无加密、无过期)
setCache('token', token);
setCache('customer_idcard_456', idCard);

→ 结果:缓存爆满、页面卡顿、身份证泄露、Token 长期有效。

正面示例:分层存储(正确做法)

typescript

运行

// 第一层:内存层(Pinia 非持久化)—— 临时客户列表
export const useCustomerStore = defineStore('customer', {
  state: () => ({
    list: [], // 仅存当前页的客户列表(内存中,刷新丢失)
    page: 1,
    size: 10
  })
});

// 第二层:缓存层
// ❶ Pinia 持久化:用户基本信息(非敏感、全局共享)
export const useUserStore = defineStore('user', {
  state: () => ({ userName: '', role: '' }),
  persist: true
});
// ❷ web-storage-cache:Token(加密+2小时过期)
setCache('access_token', token, 7200, true);
// ❸ web-storage-cache:客户筛选条件(sessionStorage+临时存储)
setCache('customer_filter', filterForm, 0, false, true);
// ❹ web-storage-cache:字典数据(批量+1天过期)
setCacheBatch(dictData, 86400);

// 第三层:后端层 —— 全量客户/订单数据,用时接口获取
const getCustomerList = async (params) => {
  const res = await api.getCustomerList(params); // 后端分页返回10条
  customerStore.list = res.data; // 仅存当前页到内存
};

→ 结果:缓存占用<1MB、页面流畅、敏感数据加密过期、数据与后端一致。

总结

关键点回顾

  1. 不是 “不能全存”,是 “全存必出问题”:浏览器缓存有上限、无权限控制、同步成本高,全存会导致性能卡顿、数据错乱、安全泄露;
  2. 缓存的核心是 “按需存储”:只存 “少量、高频、非全量” 的核心数据,全量数据必须从后端获取;
  3. 正确策略是 “分层存储”
    • 内存层(Pinia 非持久化)存临时计算数据;
    • 缓存层(Pinia 持久化 + web-storage-cache)存少量核心持久化数据;
    • 后端层存全量 / 敏感 / 实时数据(唯一可信源)。

简单来说:前端缓存是 “便利店”,只放常用的少量商品;后端数据库是 “仓库”,放所有商品 —— 你不可能把仓库里的东西全搬到便利店里,否则便利店会挤爆,反而没法正常营业。CRM 开发中,“合理缓存少量核心数据 + 按需请求后端全量数据”,才是兼顾性能和稳定性的最优解。

更多推荐