前端缓存怎么学?

63 阅读7分钟

前端缓存实战指南:缓存相关的工作到底是些什么?

「观感:❤️❤️❤️❤️❤️」

「观看时长:15min」

如果觉得帮助到了你,请点赞和收藏,这样会激励到作者更勤快创作,内容不深但是好入口。

痛点:在前端面临缓存的学习、面试过程中,总会碰到过于概念化的问题,我在初次学习的时候单独去理解概念,其实是不太容易理解或者说没有很立体,回答相关面试题也总给人一种在背的感觉。现在整理曾经的笔记分享一下我是怎么学习前端缓存的。工作多年我发现我除了LocalStorage,Session以外,似乎没有写过关于任何缓存的上的代码。

后来我觉得有种解决办法那就是结合工作内容去理解记忆

比如你通过不断学习和总结之后,得到的笔记会是这样的:(❌这不推荐,建议快速浏览,写出仅仅供后面对比)

前端缓存概述

前端缓存主要分为以下几种:

  1. HTTP缓存:通过HTTP协议头控制的缓存,包括强缓存和协商缓存。
  2. 浏览器缓存:除了HTTP缓存,浏览器还有内存缓存(Memory Cache)和磁盘缓存(Disk Cache)等。
  3. Service Worker缓存:通过Service Worker拦截请求并缓存响应,可以实现离线应用。
  4. Web Storage:包括LocalStorage和SessionStorage,用于存储键值对数据。
  5. IndexedDB:用于存储大量结构化数据。
  6. Cookie:通常用于存储少量信息,如会话管理。

HTTP缓存

HTTP缓存是通过服务器设置的HTTP头来控制的,分为强缓存和协商缓存。

强缓存

强缓存是指浏览器在请求资源时,先检查该资源的强缓存字段,如果命中且未过期,则直接使用缓存资源,不再向服务器发送请求。

强缓存的字段有两个:

  • Expires:HTTP/1.0的字段,指定资源的过期时间(绝对时间)。

  • Cache-Control:HTTP/1.1的字段,常用值有:

    • max-age=:设置缓存存储的最大周期,单位为秒。
    • public:表示响应可以被任何对象(包括客户端和代理服务器)缓存。
    • private:表示响应只能被单个用户缓存,不能作为共享缓存。
    • no-cache:强制使用协商缓存,即每次使用缓存前都必须向服务器验证。
    • no-store:不缓存任何内容。

协商缓存

当强缓存未命中时,浏览器会向服务器发送请求,进行协商缓存。如果资源未改变,服务器返回304状态码,告诉浏览器使用缓存。

协商缓存的字段有两组:

  • Last-Modified 和 If-Modified-Since

    • 服务器在响应头中设置Last-Modified,表示资源最后修改时间。
    • 浏览器下次请求时带上If-Modified-Since,服务器比较时间,如果未改变则返回304。
  • ETag 和 If-None-Match

    • 服务器生成资源的唯一标识(ETag),在响应头中返回。
    • 浏览器下次请求时带上If-None-Match,服务器比较ETag,如果相同则返回304。

浏览器缓存

浏览器在加载资源时,会按照一定的顺序查找缓存,通常为:

  1. 内存缓存(Memory Cache):快速读取,关闭标签页则释放。
  2. 磁盘缓存(Disk Cache):持久化存储,容量大。

Service Worker缓存

Service Worker是一个脚本,它在浏览器后台运行,可以拦截网络请求,并缓存资源。通过Service Worker,我们可以实现离线应用、消息推送等功能。

使用Service Worker缓存的基本步骤:

  1. 注册Service Worker。
  2. 安装阶段,预缓存资源。
  3. 拦截请求,返回缓存资源或网络请求。

Web Storage

  • LocalStorage:持久化存储,除非手动删除,否则一直存在。
  • SessionStorage:会话级存储,关闭标签页则清除。

IndexedDB

IndexedDB是一个事务型数据库,用于在客户端存储大量结构化数据。它使用索引高效地查询数据。

Cookie

Cookie通常用于存储少量数据,每次请求都会自动携带在请求头中。可以设置过期时间。

😅看完之后什么感觉?感觉懂了又似乎没懂❌❌❌❌❌❌❌❌❌

😍那么这样来试试?(推荐,代码部分快速浏览)✔️✔️✔️✔️✔️✔️✔️✔️✔️

比如说大家经常提到的:

1.HTTP缓存:通过HTTP协议头控制的缓存,包括强缓存和协商缓存。从真实工作内容视角的话:通过Nignx配置修改协议头内容:xxxx来实现强缓存和协商缓存的xxx?

2.又比如浏览器缓存,从真正工作内容视角的话:不需要任何主动的编码或者配置,只需要理解其的存在和特点,合理利用这个特点?

3.又比如Service worker,从真正工作内容视角的话:需要创建xxx文件,写什么代码,创建一个什么类,写下哪些属性和方法,从而实现怎样的一个缓存机制?

你需要想象实际工作场景是怎么用到它们的、带着问题去学习(该概念实际工作中是什么样子?需要些什么样的代码还是配置?),再利用AI给我们生成多个案例,这样就会感觉自己学会了。🔥🔥🔥🔥🔥

1. HTTP缓存实战:Nginx配置与打包策略

真实工作场景

问题:用户反馈网站更新后还是显示旧版本,必须强制刷新才能看到最新内容。

具体实施步骤

1.1 Webpack打包配置(生成带哈希的文件名)
// webpack.config.js
module.exports = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
};
1.2 Nginx服务器配置
# /etc/nginx/conf.d/your-site.conf
server {
    listen 80;
    server_name your-domain.com;
    root /var/www/your-project/dist;
    
    # HTML文件 - 不缓存或短时间缓存
    location ~* \.html$ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        expires 0;
        try_files $uri $uri/ /index.html;
    }
    
    # 带哈希的静态资源 - 长期强缓存
    location ~* \.[a-f0-9]{8}\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        add_header Cache-Control "public, immutable, max-age=31536000"; # 1年
        expires 1y;
        access_log off; # 不记录访问日志,减少IO
    }
    
    # 普通静态资源 - 协商缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        add_header Cache-Control "public, max-age=86400"; # 1天
        expires 1d;
    }
    
    # API接口 - 不缓存
    location /api/ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        proxy_pass http://backend-api;
    }
}
1.3 验证配置效果
# 测试HTTP缓存头
curl -I https://your-domain.com/static/js/main.abc123def.js
# 应该返回:Cache-Control: public, immutable, max-age=31536000

curl -I https://your-domain.com/index.html
# 应该返回:Cache-Control: no-cache, no-store, must-revalidate

工作成果:部署后静态资源加载速度提升60%,用户不再抱怨更新问题。

2. Service Worker缓存实战:离线可用应用

真实工作场景

需求:电商活动页需要在弱网环境下保证核心功能可用,提升用户体验。

具体实施步骤

2.1 创建Service Worker文件
// public/sw.js
const CACHE_NAME = 'ecommerce-v1.2.0';
const OFFLINE_URL = '/offline.html';

// 需要预缓存的关键资源
const PRECACHE_URLS = [
  '/',
  '/static/css/main.css',
  '/static/js/main.js',
  '/static/images/logo.svg',
  '/static/images/placeholder-product.jpg',
  OFFLINE_URL
];

// 安装阶段 - 预缓存核心资源
self.addEventListener('install', (event) => {
  console.log('Service Worker 安装中...');
  
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => {
        console.log('开始缓存关键资源');
        return cache.addAll(PRECACHE_URLS);
      })
      .then(() => {
        console.log('跳过等待,立即激活');
        return self.skipWaiting();
      })
  );
});

// 激活阶段 - 清理旧缓存
self.addEventListener('activate', (event) => {
  console.log('Service Worker 激活中...');
  
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME) {
            console.log('删除旧缓存:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => {
      console.log('Claiming clients');
      return self.clients.claim();
    })
  );
});

// 请求拦截 - 实现缓存策略
self.addEventListener('fetch', (event) => {
  // 只处理GET请求
  if (event.request.method !== 'GET') return;
  
  // 第三方资源直接通过网络获取
  if (event.request.url.indexOf('analytics') > -1) {
    return;
  }
  
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // 缓存优先,回退到网络请求
        if (response) {
          return response;
        }
        
        // 克隆请求,因为请求是流只能使用一次
        const fetchRequest = event.request.clone();
        
        return fetch(fetchRequest)
          .then((response) => {
            // 检查响应是否有效
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
            
            // 克隆响应,因为响应是流只能使用一次
            const responseToCache = response.clone();
            
            // 缓存新请求的资源
            caches.open(CACHE_NAME)
              .then((cache) => {
                // 只缓存同源资源
                if (event.request.url.startsWith(self.location.origin)) {
                  cache.put(event.request, responseToCache);
                }
              });
            
            return response;
          })
          .catch(() => {
            // 网络请求失败,尝试返回离线页面
            if (event.request.destination === 'document') {
              return caches.match(OFFLINE_URL);
            }
            
            // 对于其他资源,可以返回占位图等
            if (event.request.destination === 'image') {
              return caches.match('/static/images/placeholder-product.jpg');
            }
          });
      })
  );
});
2.2 在主应用中注册Service Worker
// src/registerServiceWorker.js
export const registerServiceWorker = () => {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker
        .register('/sw.js')
        .then((registration) => {
          console.log('SW registered: ', registration);
          
          // 检查更新
          registration.addEventListener('updatefound', () => {
            const newWorker = registration.installing;
            console.log('SW update found!');
            
            newWorker.addEventListener('statechange', () => {
              if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
                // 显示更新提示
                showUpdateNotification();
              }
            });
          });
        })
        .catch((registrationError) => {
          console.log('SW registration failed: ', registrationError);
        });
    });
    
    // 监听控制权变化
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      window.location.reload();
    });
  }
};

// 显示更新提示
const showUpdateNotification = () => {
  const notification = document.createElement('div');
  notification.innerHTML = `
    <div style="position: fixed; top: 20px; right: 20px; background: #4CAF50; color: white; padding: 16px; border-radius: 4px; z-index: 10000;">
      有新版本可用,<a href="#" onclick="window.location.reload()" style="color: white; text-decoration: underline;">点击刷新</a>
    </div>
  `;
  document.body.appendChild(notification);
};
2.3 在React应用中集成
// src/App.js
import { useEffect } from 'react';
import { registerServiceWorker } from './registerServiceWorker';

function App() {
  useEffect(() => {
    registerServiceWorker();
  }, []);
  
  return (
    <div className="App">
      {/* 应用内容 */}
    </div>
  );
}

工作成果:活动页在弱网环境下可用性从45%提升到92%,用户跳出率降低35%。

3. 应用层缓存实战:API请求优化

真实工作场景

问题:后台管理系统频繁请求相同数据,造成服务器压力大,用户体验差。

具体实施步骤

3.1 使用React Query实现API缓存
// src/utils/queryClient.js
import { QueryClient } from 'react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5分钟
      cacheTime: 10 * 60 * 1000, // 10分钟
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});
3.2 在组件中使用缓存
// src/components/UserList.jsx
import { useQuery, useMutation, useQueryClient } from 'react-query';

const fetchUsers = async () => {
  const response = await fetch('/api/users');
  if (!response.ok) throw new Error('获取用户列表失败');
  return response.json();
};

const createUser = async (userData) => {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData),
  });
  return response.json();
};

function UserList() {
  const queryClient = useQueryClient();
  
  // 使用缓存获取用户列表
  const { data: users, isLoading, error } = useQuery(
    'users', // 缓存key
    fetchUsers,
    {
      staleTime: 2 * 60 * 1000, // 2分钟内不重新请求
    }
  );
  
  // 创建用户的mutation
  const createUserMutation = useMutation(createUser, {
    onSuccess: () => {
      // 用户创建成功后,使users缓存失效,触发重新获取
      queryClient.invalidateQueries('users');
      
      // 显示成功提示
      alert('用户创建成功!');
    },
    onError: (error) => {
      alert(`创建用户失败: ${error.message}`);
    },
  });
  
  const handleCreateUser = () => {
    createUserMutation.mutate({
      name: '新用户',
      email: 'new@example.com',
    });
  };
  
  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>错误: {error.message}</div>;
  
  return (
    <div>
      <button onClick={handleCreateUser} disabled={createUserMutation.isLoading}>
        创建用户
      </button>
      <ul>
        {users?.map(user => (
          <li key={user.id}>{user.name} - {user.email}</li>
        ))}
      </ul>
    </div>
  );
}
3.3 实现乐观更新
// src/components/TodoList.jsx
const updateTodo = async ({ id, completed }) => {
  const response = await fetch(`/api/todos/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ completed }),
  });
  return response.json();
};

function TodoList() {
  const queryClient = useQueryClient();
  
  const updateTodoMutation = useMutation(updateTodo, {
    // 乐观更新:立即更新UI,如果失败则回滚
    onMutate: async (updatedTodo) => {
      // 取消正在进行的refetch,避免冲突
      await queryClient.cancelQueries('todos');
      
      // 保存前一个状态,用于回滚
      const previousTodos = queryClient.getQueryData('todos');
      
      // 乐观更新缓存
      queryClient.setQueryData('todos', (old) => 
        old.map(todo => 
          todo.id === updatedTodo.id 
            ? { ...todo, completed: updatedTodo.completed }
            : todo
        )
      );
      
      return { previousTodos };
    },
    
    // 出错时回滚到前一个状态
    onError: (err, updatedTodo, context) => {
      queryClient.setQueryData('todos', context.previousTodos);
      alert('更新失败,已恢复原状态');
    },
    
    // 成功时使缓存失效,重新获取最新数据
    onSettled: () => {
      queryClient.invalidateQueries('todos');
    },
  });
  
  const handleToggleTodo = (todo) => {
    updateTodoMutation.mutate({
      id: todo.id,
      completed: !todo.completed,
    });
  };
  
  // ... 组件渲染逻辑
}

4. 本地存储缓存实战:用户偏好设置

真实工作场景

需求:记住用户的主题偏好、语言设置等,提升用户体验。

具体实施步骤

4.1 创建本地存储工具类
// src/utils/storage.js
class StorageManager {
  constructor() {
    this.prefix = 'myapp_';
  }
  
  // 设置缓存,带过期时间
  set(key, value, expiresIn = null) {
    const item = {
      value,
      timestamp: Date.now(),
      expires: expiresIn ? Date.now() + expiresIn : null,
    };
    
    try {
      localStorage.setItem(`${this.prefix}${key}`, JSON.stringify(item));
      return true;
    } catch (e) {
      console.warn('localStorage写入失败:', e);
      return false;
    }
  }
  
  // 获取缓存,检查是否过期
  get(key) {
    try {
      const itemStr = localStorage.getItem(`${this.prefix}${key}`);
      if (!itemStr) return null;
      
      const item = JSON.parse(itemStr);
      
      // 检查是否过期
      if (item.expires && Date.now() > item.expires) {
        this.remove(key);
        return null;
      }
      
      return item.value;
    } catch (e) {
      console.warn('localStorage读取失败:', e);
      this.remove(key);
      return null;
    }
  }
  
  // 删除缓存
  remove(key) {
    localStorage.removeItem(`${this.prefix}${key}`);
  }
  
  // 清空所有本应用的缓存
  clear() {
    Object.keys(localStorage)
      .filter(key => key.startsWith(this.prefix))
      .forEach(key => localStorage.removeItem(key));
  }
  
  // 获取所有缓存键
  getAllKeys() {
    return Object.keys(localStorage)
      .filter(key => key.startsWith(this.prefix))
      .map(key => key.replace(this.prefix, ''));
  }
}

export const storage = new StorageManager();
4.2 在React中使用本地存储
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
import { storage } from '../utils/storage';

export const useLocalStorage = (key, initialValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = storage.get(key);
      return item !== null ? item : initialValue;
    } catch (error) {
      console.warn(`读取localStorage键"${key}"失败:`, error);
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      storage.set(key, valueToStore);
    } catch (error) {
      console.warn(`设置localStorage键"${key}"失败:`, error);
    }
  };
  
  return [storedValue, setValue];
};

// 主题设置示例
export const useTheme = () => {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme);
  }, [theme]);
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return [theme, toggleTheme];
};
4.3 在组件中使用
// src/components/ThemeToggle.jsx
import { useTheme } from '../hooks/useLocalStorage';

function ThemeToggle() {
  const [theme, toggleTheme] = useTheme();
  
  return (
    <button onClick={toggleTheme} className="theme-toggle">
      切换到{theme === 'light' ? '深色' : '浅色'}模式
    </button>
  );
}

// src/components/UserPreferences.jsx
import { useLocalStorage } from '../hooks/useLocalStorage';

function UserPreferences() {
  const [language, setLanguage] = useLocalStorage('language', 'zh-CN');
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 'medium');
  const [notifications, setNotifications] = useLocalStorage('notifications', true);
  
  return (
    <div className="preferences">
      <h2>偏好设置</h2>
      
      <div>
        <label>语言:</label>
        <select value={language} onChange={(e) => setLanguage(e.target.value)}>
          <option value="zh-CN">中文</option>
          <option value="en-US">English</option>
        </select>
      </div>
      
      <div>
        <label>字体大小:</label>
        <select value={fontSize} onChange={(e) => setFontSize(e.target.value)}>
          <option value="small"></option>
          <option value="medium"></option>
          <option value="large"></option>
        </select>
      </div>
      
      <div>
        <label>
          <input
            type="checkbox"
            checked={notifications}
            onChange={(e) => setNotifications(e.target.checked)}
          />
          启用通知
        </label>
      </div>
    </div>
  );
}

5. 缓存监控与调试实战

真实工作场景

需求:监控缓存命中率,调试缓存问题。

具体实施步骤

5.1 缓存监控工具
// src/utils/cacheMonitor.js
class CacheMonitor {
  constructor() {
    this.stats = {
      hits: 0,
      misses: 0,
      errors: 0,
    };
  }
  
  recordHit() {
    this.stats.hits++;
    this.logStats();
  }
  
  recordMiss() {
    this.stats.misses++;
    this.logStats();
  }
  
  recordError() {
    this.stats.errors++;
  }
  
  getHitRate() {
    const total = this.stats.hits + this.stats.misses;
    return total > 0 ? (this.stats.hits / total) * 100 : 0;
  }
  
  logStats() {
    if ((this.stats.hits + this.stats.misses) % 10 === 0) {
      console.log(`缓存命中率: ${this.getHitRate().toFixed(1)}%`, this.stats);
    }
  }
  
  // 在开发环境显示缓存状态
  renderDebugPanel() {
    if (process.env.NODE_ENV !== 'development') return;
    
    const panel = document.createElement('div');
    panel.style.cssText = `
      position: fixed;
      bottom: 10px;
      right: 10px;
      background: rgba(0,0,0,0.8);
      color: white;
      padding: 10px;
      border-radius: 5px;
      font-size: 12px;
      z-index: 9999;
    `;
    
    const updatePanel = () => {
      panel.innerHTML = `
        <div>缓存监控</div>
        <div>命中: ${this.stats.hits}</div>
        <div>未命中: ${this.stats.misses}</div>
        <div>命中率: ${this.getHitRate().toFixed(1)}%</div>
      `;
    };
    
    updatePanel();
    document.body.appendChild(panel);
    
    // 每秒更新
    setInterval(updatePanel, 1000);
  }
}

export const cacheMonitor = new CacheMonitor();
5.2 集成监控到缓存逻辑
// 在React Query中集成监控
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onSuccess: () => cacheMonitor.recordHit(),
      onError: () => cacheMonitor.recordError(),
      onSettled: (data, error, variables, context) => {
        if (!context || !context.isCached) {
          cacheMonitor.recordMiss();
        }
      },
    },
  },
});

// 在应用启动时启动监控
if (process.env.NODE_ENV === 'development') {
  cacheMonitor.renderDebugPanel();
}

这种学习方式为什么科学?

  1. 目标导向:每个知识点都绑定一个具体的、可验证的工作任务(“配置Nginx解决缓存问题” vs “理解强缓存概念”)。
  2. 形成闭环:你不仅知道“是什么”,更知道“怎么用”,甚至能预测“用了会怎样”。
  3. 建立关联:将抽象概念(协商缓存)与具体工具(Nginx配置语法)和问题场景(用户看不到更新)强关联,记忆更牢固。
  4. 可迁移性:一旦你在一个项目中亲手配置过Nginx缓存,下次在任何技术栈的项目中遇到类似问题,你都知道解决方案的路径。

这种学习方式的优势:

  • 真实可验证:每个方案都能立即看到效果
  • 问题导向:针对具体业务问题提供解决方案
  • 完整链路:从配置到代码到监控的完整实现
  • 可迁移性:方案可以复用到任何技术栈的项目中

在面试中,你可以自信地讲述这些具体的实施经验和解决的实际问题,这比单纯背诵缓存概念要有说服力得多。