前端缓存实战指南:缓存相关的工作到底是些什么?
「观感:❤️❤️❤️❤️❤️」
「观看时长:15min」
如果觉得帮助到了你,请点赞和收藏,这样会激励到作者更勤快创作,内容不深但是好入口。
痛点:在前端面临缓存的学习、面试过程中,总会碰到过于概念化的问题,我在初次学习的时候单独去理解概念,其实是不太容易理解或者说没有很立体,回答相关面试题也总给人一种在背的感觉。现在整理曾经的笔记分享一下我是怎么学习前端缓存的。工作多年我发现我除了LocalStorage,Session以外,似乎没有写过关于任何缓存的上的代码。
后来我觉得有种解决办法那就是结合工作内容去理解记忆
比如你通过不断学习和总结之后,得到的笔记会是这样的:(❌这不推荐,建议快速浏览,写出仅仅供后面对比)
前端缓存概述
前端缓存主要分为以下几种:
- HTTP缓存:通过HTTP协议头控制的缓存,包括强缓存和协商缓存。
- 浏览器缓存:除了HTTP缓存,浏览器还有内存缓存(Memory Cache)和磁盘缓存(Disk Cache)等。
- Service Worker缓存:通过Service Worker拦截请求并缓存响应,可以实现离线应用。
- Web Storage:包括LocalStorage和SessionStorage,用于存储键值对数据。
- IndexedDB:用于存储大量结构化数据。
- 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。
浏览器缓存
浏览器在加载资源时,会按照一定的顺序查找缓存,通常为:
- 内存缓存(Memory Cache):快速读取,关闭标签页则释放。
- 磁盘缓存(Disk Cache):持久化存储,容量大。
Service Worker缓存
Service Worker是一个脚本,它在浏览器后台运行,可以拦截网络请求,并缓存资源。通过Service Worker,我们可以实现离线应用、消息推送等功能。
使用Service Worker缓存的基本步骤:
- 注册Service Worker。
- 安装阶段,预缓存资源。
- 拦截请求,返回缓存资源或网络请求。
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();
}
这种学习方式为什么科学?
- 目标导向:每个知识点都绑定一个具体的、可验证的工作任务(“配置Nginx解决缓存问题” vs “理解强缓存概念”)。
- 形成闭环:你不仅知道“是什么”,更知道“怎么用”,甚至能预测“用了会怎样”。
- 建立关联:将抽象概念(协商缓存)与具体工具(Nginx配置语法)和问题场景(用户看不到更新)强关联,记忆更牢固。
- 可迁移性:一旦你在一个项目中亲手配置过Nginx缓存,下次在任何技术栈的项目中遇到类似问题,你都知道解决方案的路径。
这种学习方式的优势:
- 真实可验证:每个方案都能立即看到效果
- 问题导向:针对具体业务问题提供解决方案
- 完整链路:从配置到代码到监控的完整实现
- 可迁移性:方案可以复用到任何技术栈的项目中
在面试中,你可以自信地讲述这些具体的实施经验和解决的实际问题,这比单纯背诵缓存概念要有说服力得多。