阅读视图

发现新文章,点击刷新页面。

像使用 Redis 一样操作 LocalStorage

参考 Redis 缓存接口封装 LocalStorage,在全栈项目中保持一致的缓存操作体验,降低前端同学转向全栈开发的心智负担

1. 为什么需要 Redis 风格的 LocalStorage 封装

在全栈开发中,后端通常使用 Redis 进行缓存管理,而前端则使用 LocalStorage 存储本地数据。两者的 API 接口差异较大,这给从前端转向全栈的开发者带来了额外的学习成本。

通过封装一个 Redis 风格的 LocalStorage 工具类,我们可以:

  • 保持前后端缓存操作接口一致
  • 提供更丰富的缓存管理功能(如过期时间、键匹配等)
  • 增强代码的可维护性和可读性
  • 为未来可能的后端迁移做好准备

2. 核心功能实现与代码解析

完整工具类实现

export abstract class CacheUtil {
  /**
   * 设置缓存
   * @param key 缓存键
   * @param value 缓存值
   * @param ttl 过期时间(单位:秒),-1 表示永不过期
   */
  static set(key: string, value: any, ttl: number = -1) {
    const data = { value, ttl: ttl === -1 ? ttl : Date.now() + ttl * 1000 }
    localStorage.setItem(key, JSON.stringify(data))
  }

  /**
   * 获取缓存
   * @param key 缓存键
   * @param defaultValue 缓存不存在或过期时的默认值
   * @returns 缓存值或默认值
   */
  static get<T = any>(key: string, defaultValue: T | null = null): T | null {
    try {
      const jsonStr = localStorage.getItem(key)
      if (!jsonStr) return defaultValue
      const data = JSON.parse(jsonStr)
      if (data.ttl === -1 || Date.now() <= data.ttl) return data.value
      localStorage.removeItem(key)
      return defaultValue
    } catch (error: unknown) {
      localStorage.removeItem(key)
      return defaultValue
    }
  }

  /**
   * 获取缓存剩余过期时间(秒)
   * -1 = 永久有效
   * -2 = 已过期/不存在
   */
  static ttl(key: string): number {
    try {
      const item = localStorage.getItem(key)
      if (!item) return -2
      const data = JSON.parse(item)
      if (data.ttl === -1) return -1
      const remaining = data.ttl - Date.now()
      return remaining > 0 ? Math.floor(remaining / 1000) : -2
    } catch {
      return -2 // 解析失败,视为无效缓存
    }
  }

  /**
   * 动态设置缓存过期时间
   * @param key 缓存键
   * @param ttl 过期时间(秒)
   * @returns 是否设置成功
   */
  static expire(key: string, ttl: number): boolean {
    const value = this.get(key)
    if (value === null) return false
    this.set(key, value, ttl)
    return true
  }

  /**
   * 删除缓存
   * @param key 缓存键
   */
  static del(key: string) {
    localStorage.removeItem(key)
  }

  /**
   * 清空所有缓存
   */
  static flushall() {
    localStorage.clear()
  }

  /**
   * 查找缓存键(支持通配符 *,和 Redis 用法一致)
   * @param pattern 匹配规则,例如 user*、*info、*token*,默认 *
   * @returns 匹配的键数组
   */
  static keys(pattern: string = '*'): string[] {
    const allKeys = Object.keys(localStorage)
    const regex = new RegExp(pattern.replace(/\*/g, '.*'))
    return allKeys.filter((key) => regex.test(key))
  }

  /**
   * 检查缓存是否存在且未过期
   * @param key 缓存键
   * @returns 是否存在有效缓存
   */
  static exists(key: string): boolean {
    return this.get(key) !== null
  }
}

核心设计要点

  1. 数据结构设计:使用 { value, ttl } 结构存储缓存数据,其中 ttl 为过期时间戳或 -1(永不过期)

  2. 过期时间处理

    • 设置时计算绝对过期时间戳
    • 获取时检查是否过期,过期则自动清理
    • 提供 ttl 方法查看剩余过期时间
  3. 错误处理:通过 try-catch 捕获 JSON 解析异常,确保缓存操作的稳定性

  4. Redis 风格 API:实现了与 Redis 相似的 setgetdelexpirekeysexists 等方法

  5. 通配符支持keys 方法支持 * 通配符匹配,与 Redis 用法一致

3. 完整 API 接口说明

方法 功能描述 参数说明 返回值
set(key, value, ttl) 设置缓存 key: 缓存键
value: 缓存值
ttl: 过期时间(秒),默认 -1
get(key, defaultValue) 获取缓存 key: 缓存键
defaultValue: 默认值,默认 null
缓存值或默认值
ttl(key) 获取剩余过期时间 key: 缓存键 -1: 永久有效
-2: 已过期/不存在
正数: 剩余秒数
expire(key, ttl) 设置过期时间 key: 缓存键
ttl: 过期时间(秒)
是否设置成功
del(key) 删除缓存 key: 缓存键
flushall() 清空所有缓存
keys(pattern) 查找匹配的键 pattern: 匹配规则,默认 * 匹配的键数组
exists(key) 检查缓存是否存在 key: 缓存键 是否存在有效缓存

4. 实战使用示例

基础操作

// 设置缓存,1小时过期
CacheUtil.set('USER', { id: 1, name: 'John' }, 3600)

// 获取缓存
const user = CacheUtil.get('USER')
console.log(user) // { id: 1, name: 'John' }

过期时间管理

// 续期缓存,设置为2小时过期
CacheUtil.expire('USER', 7200)

// 查看剩余过期时间
const remainingTime = CacheUtil.ttl('USER')
console.log(`剩余过期时间:${remainingTime}秒`)

键管理

// 通配符查找键
const userKeys = CacheUtil.keys('USER*')
const infoKeys = CacheUtil.keys('*INFO')
console.log('用户相关键:', userKeys)
console.log('信息相关键:', infoKeys)

// 检查缓存是否存在
const exists = CacheUtil.exists('USER')
console.log('USER 缓存存在:', exists)

删除操作

// 删除指定缓存
CacheUtil.del('USER')

// 清空所有缓存
CacheUtil.flushall()

5. 性能考量与最佳实践

性能考量

  1. 存储限制:LocalStorage 通常有 5MB 左右的存储限制,避免存储过大的数据

  2. 读取性能:频繁读取大对象会影响性能,建议将数据合理拆分

  3. 过期检查:每次 get 操作都会检查过期时间,对性能影响较小但需注意

  4. JSON 序列化:复杂对象的序列化/反序列化会有性能开销,建议存储结构尽量简单

最佳实践

  1. 命名规范:使用统一的命名前缀(如 APP_)避免键名冲突

  2. 数据类型:只存储必要的数据,避免存储整个应用状态

  3. 过期策略:为临时数据设置合理的过期时间,避免占用存储空间

  4. 错误处理:虽然工具类已做了错误处理,但调用时仍需考虑异常情况

  5. 安全注意:不要存储敏感信息(如密码、Token)到 LocalStorage


你在项目中是如何管理本地缓存的?有哪些好用的缓存策略或工具推荐?欢迎在评论区分享你的经验和想法!

Vue3 集成 NProgress 进度条:从入门到精通

在前端应用开发中,用户体验至关重要。当页面加载或进行数据请求时,一个优雅的进度条不仅能告知用户系统正在工作,还能有效缓解用户的等待焦虑。NProgress 作为一款轻量级的进度条库,凭借其简洁的设计和良好的兼容性,被广泛应用于各类 Web 项目中。本文将详细介绍如何在 Vue3 项目中优雅地集成和使用 NProgress

一、环境准备

1.1 安装依赖

# 安装 NProgress 核心库和 lodash-es 工具库
pnpm i nprogress lodash-es

# 安装 TypeScript 类型定义(开发依赖)
pnpm i @types/nprogress @types/lodash-es -D

1.2 依赖说明

  • nprogress:进度条核心库,提供简单的进度控制 API
  • lodash-es:高效的 JavaScript 工具库,用于对象合并等操作
  • @types/nprogress:NProgress 的 TypeScript 类型定义文件
  • @types/lodash-es:lodash-es 的 TypeScript 类型定义文件

1.3 环境变量配置

.env 文件中配置进度条的开关:

# 路由进度条,默认开启(设置为 'false' 可关闭)
VITE_ROUTER_NPROGRESS = true

# 请求进度条,默认开启(设置为 'false' 可关闭)
VITE_REQUEST_NPROGRESS = true

二、核心实现

2.1 基础封装

// src/hooks/useProgress.ts

import { merge } from 'lodash-es'
import NProgress from 'nprogress'
import type { NProgressOptions } from 'nprogress'

interface ProgressConfig extends NProgressOptions {
  /** 是否显示进度条 */
  show: boolean
}

const DEFAULT_CONFIG: Partial<ProgressConfig> = {
  /** CSS3 缓冲动画字符串,支持 ease、linear、ease-in、ease-out、ease-in-out 以及自定义 cubic-bezier 等 */
  easing: 'ease',
  /** 指定进度条的父容器,默认为 body */
  parent: 'body',
  /** 是否显示进度条,可通过环境变量控制 */
  show: true,
  /** 是否显示右侧的环形进度动画 */
  showSpinner: false,
  /** 是否开启自动递增模式 */
  trickle: true,
  /** 设置开始时最低百分比,范围 0-1 */
  minimum: 0.08,
  /** 动画速度,单位毫秒 */
  speed: 200,
}

/**
 * 进度条控制工具 Hook
 * @param config 自定义配置,会与默认配置深度合并
 * @returns { start, done } 启动/结束进度条方法
 */
export function useProgress(config: Partial<ProgressConfig> = {}) {
  const mergeConfig = merge({}, DEFAULT_CONFIG, config)
  NProgress.configure(mergeConfig)

  /**
   * 启动进度条
   */
  function start() {
    if (!mergeConfig.show) return
    NProgress.start()
  }

  /**
   * 结束进度条
   */
  function done() {
    if (!mergeConfig.show || !NProgress.isStarted()) return
    NProgress.done()
  }

  return { start, done }
}

三、实际应用场景

3.1 Axios 请求拦截器集成

在实际项目中,我们通常需要为 API 请求自动添加进度条。以下是配合 Axios 使用的完整示例,通过环境变量控制是否显示:

// src/utils/request.ts

import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'

const NProgress = useProgress({ show: import.meta.env.VITE_REQUEST_NPROGRESS !== 'false' })

const instance: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 15000,
})

// 请求拦截器
instance.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    NProgress.start()
    return config
  },
  (error) => {
    NProgress.done()
    return Promise.reject(error)
  },
)

// 响应拦截器
instance.interceptors.response.use(
  (response: AxiosResponse) => {
    NProgress.done()
    return response
  },
  (error) => {
    NProgress.done()
    return Promise.reject(error)
  },
)

export const request = instance

3.2 Vue Router 路由守卫集成

结合 Vue Router,可以在页面切换时显示进度条,通过环境变量控制是否显示:

// src/router/index.ts

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [],
})

const NProgress = useProgress({ show: import.meta.env.VITE_ROUTER_NPROGRESS !== 'false' })

router.beforeEach((to, from, next) => {
  NProgress.start()
  next()
})

router.afterEach(() => {
  NProgress.done()
})

export default router

3.3 组合式使用示例

<template>
  <div class="app">
    <button @click="loadData">加载数据</button>
  </div>
</template>

<script setup lang="ts">
import { useProgress } from '@/hooks/useProgress'

const NProgress = useProgress({ show: import.meta.env.VITE_REQUEST_NPROGRESS !== 'false' })

async function loadData() {
  NProgress.start()
  try {
    await fetch('/api/data')
  } finally {
    NProgress.done()
  }
}
</script>

四、全局样式配置

4.1 全局样式入口文件

创建全局样式入口文件,统一管理项目样式:

// src/styles/index.scss

@use './variables.scss';
@use './transition.scss';
@use 'nprogress/nprogress.css';
@use './element-plus/el-table.scss';
@use './element-plus/el-dialog.scss';
@use './element-plus/el-dropdown.scss';

body {
  font-family: var(--el-font-family);
  background-color: var(--el-bg-color-page);
}

#nprogress .bar {
  background-color: var(--el-color-primary);
}

4.2 样式文件说明

  • variables.scss:Element Plus 主题变量定义
  • transition.scss:全局过渡动画样式
  • nprogress.css:NProgress 进度条基础样式
  • element-plus/*.scss:Element Plus 组件样式覆盖
  • 全局样式:包含进度条颜色等自定义样式

4.3 NProgress 主题样式覆盖

如果需要更详细的自定义 NProgress 样式,可以创建专门的样式文件:

// src/styles/nprogress.scss

#nprogress .bar {
  background-color: var(--el-color-primary);
  height: 3px;

  // 添加渐变效果
  background: linear-gradient(90deg, var(--el-color-primary-light-3) 0%, var(--el-color-primary) 100%);
}

#nprogress .peg {
  box-shadow: 0 0 10px var(--el-color-primary);
}

#nprogress .spinner-icon {
  border-top-color: var(--el-color-primary);
  border-left-color: var(--el-color-primary);
}

4.4 在入口文件中引入

// src/styles/index.scss

@use './variables.scss';
@use './transition.scss';
@use './nprogress.scss'; // 替换为自定义样式文件
@use './element-plus/el-table.scss';
@use './element-plus/el-dialog.scss';
@use './element-plus/el-dropdown.scss';

body {
  font-family: var(--el-font-family);
  background-color: var(--el-bg-color-page);
}

#nprogress .bar {
  background-color: var(--el-color-primary);
}

4.5 main.ts 中引入全局样式

// src/main.ts

import { createApp } from 'vue'
import App from './App.vue'
import './styles/index.scss' // 引入全局样式

const app = createApp(App)
app.mount('#app')

五、NProgress 配置详解

5.1 核心配置项

配置项 类型 默认值 说明
easing string 'ease' CSS3 缓动函数
speed number 200 动画速度(毫秒)
trickle boolean true 是否自动递增
trickleSpeed number 200 自动递增速度
minimum number 0.08 起始百分比
showSpinner boolean false 是否显示环形动画
showUI boolean false 是否显示进度条
parent string 'body' 父容器选择器
positionUsing string '' 定位方式

5.2 缓动函数推荐

const EASING_FUNCTIONS = {
  // 匀速运动
  linear: 'linear',

  // 标准缓动
  ease: 'ease',
  easeIn: 'ease-in',
  easeOut: 'ease-out',
  easeInOut: 'ease-in-out',

  // 自定义贝塞尔曲线
  smooth: 'cubic-bezier(0.4, 0, 0.2, 1)',
  gentle: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
  swift: 'cubic-bezier(0.4, 0, 0.6, 1)',
}

五、总结

通过本文的学习,你应该已经掌握了:

  1. 基础集成:如何在 Vue3 项目中安装和配置 NProgress
  2. 封装技巧:如何封装通用的进度条 Hook,提高代码复用性
  3. 环境变量控制:如何通过环境变量灵活控制进度条的开关
  4. 实际应用:如何与 Axios、Vue Router 等常见库配合使用
  5. 全局样式配置:如何通过全局样式统一管理进度条外观
❌