现代前端部署实践指南
"部署不是终点,而是产品生命周期的新起点。"
一、浏览器缓存:性能优化的第一道防线
前端工程化一直是前端开发中一个比较热门的话题,CI/CD、DDD 这些概念的文章一直比较火热,在现代前端开发中,将代码从开发环境顺利部署到生产环境并非简单的文件上传过程(可能一些小公司,或者一些纯静态网页就是打包之后 dist 目录往磁盘一扔就跑了,但是稍微大一些的项目或多或少都会有一些工程化的影子)。一个完善的前端部署方案需要考虑性能优化、稳定性保障和发布策略等多个维度。基本都包括缓存策略、CDN分发、自动化部署流程(CI/CD)以及先进的发布策略(非覆盖式发布、灰度发布)等核心主题,构建一个可靠且高效的前端部署体系。这些技术手段不仅能够提升应用的访问速度,确保用户体验,还能够降低发布风险,提高系统可用性。通过合理运用这些部署策略,我们可以更好地应对大规模用户访问、频繁版本迭代等现代web应用面临的挑战。
1. HTTP 缓存
HTTP 缓存是最基础也是最重要的缓存机制,主要分为强缓存和协商缓存。
1.1 强缓存
强缓存通过设置 HTTP 头部字段来实现,主要涉及以下字段:
Cache-Control: max-age=31536000
Expires: Wed, 21 Oct 2025 07:28:00 GMT
- Cache-Control 是 HTTP/1.1 的产物,优先级高于 Expires
- 常用的 Cache-Control 指令:
- max-age:资源最大有效期
- no-cache:强制验证缓存
- no-store:禁止缓存
- private:仅浏览器可缓存
- public:所有中间节点都可缓存
1.2 协商缓存
当强缓存失效时,浏览器会启用协商缓存机制:
// 请求头
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
// 响应头
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
ETag/If-None-Match 的优先级高于 Last-Modified/If-Modified-Since,因为时间戳精度只能到秒级,而 ETag 可以更精确地标识资源版本。
但是协商缓存会存在一个请求(和 post 请求的预检一样,但是目的不一样),顾名思义就是存在一个 HTTP Get 请求,如果资源已改变,返回 200 OK 和新的资源内容,如果资源未改变,返回 304 Not Modified。
2. 浏览器存储机制
LocalStorage 和 SessionStorage 概念都比较熟悉了,区别SessionStorage仅在当前会话期间有效
3、前端缓存策略实践
1. 静态资源缓存策略
1.1 构建工具配置
使用 webpack 等构建工具时,通过文件名 hash 实现缓存更新:
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
}
};
1.2 Service Worker 缓存
利用 Service Worker 实现离线缓存:
// service-worker.js
const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
2. API 数据缓存
2.1 内存缓存
使用 Map 或自定义缓存类实现:
class MemoryCache {
constructor() {
this.cache = new Map();
}
set(key, value, ttl) {
const expires = ttl ? Date.now() + ttl : null;
this.cache.set(key, { value, expires });
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (item.expires && item.expires < Date.now()) {
this.cache.delete(key);
return null;
}
return item.value;
}
}
2.2 请求缓存
使用 axios 拦截器实现请求缓存:
const cache = new Map();
axios.interceptors.request.use(config => {
const key = `${config.url}${JSON.stringify(config.params)}`;
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) {
return Promise.reject({ cached: cached.data });
}
return config;
});
axios.interceptors.response.use(
response => {
const config = response.config;
const key = `${config.url}${JSON.stringify(config.params)}`;
cache.set(key, {
data: response.data,
expires: Date.now() + 5 * 60 * 1000 // 5分钟缓存
});
return response;
},
error => {
if (error.cached) {
return Promise.resolve({ data: error.cached });
}
return Promise.reject(error);
}
);
4、性能优化与最佳实践
一些是个人直接的一些关于缓存使用的在特定场景下的最佳实践,其实大家都是这么干的
1. 缓存策略选择
- 频繁变动的资源:使用协商缓存
- 基本不变的资源:使用强缓存,设置较长的 max-age
- API 数据:结合业务场景使用内存缓存或 localStorage
- 大文件资源:考虑使用 Service Worker 或 IndexedDB
实战案例:预加载优化
在最近的开发工作中,遇到了一个恰好比较适合这个场景的问题:用户点击"产品介绍"按钮后,弹出的详情模态框需要加载多张高清产品图片,导致明显的加载延迟,会让用户先看见一段时间空白,在后才会慢慢陆续加载出来几个图片,体验不是很好。
优化方案
最近开发中,遇到这样一个场景,在一个 Modal 弹窗中,存在一个相关的介绍按钮,点击按钮弹出一个介绍页的 Modal,这个介绍页存在很多大图,打开是会存在一个明显的图片有无到有的过程,用户体验不是很好。
我的解决方案是利用图片缓存,在第一个 Modal 出现的时候,向 document 上附加一个 link <link rel="preload" as="image" href="/XXX/OSS/img" /> 标签,url 是图片的 CDN 地址, 加载图片,当回点击这个介绍页是,这个时候就会走缓存加载图片了,逻辑大致就是这样的(后来需求改来改去就没上🤦♂️,但是思路可以参考一下)
import { useEffect, useState } from 'react';
interface PreloadStatus {
loaded: boolean;
error: Error | null;
progress: number; // 0-100
}
export const useImagesPreload = (
images: string[],
options: PreloadOptions = {}
) => {
const [status, setStatus] = useState<PreloadStatus>({
loaded: false,
error: null,
progress: 0
});
useEffect(() => {
if (!images.length) {
setStatus({ loaded: true, error: null, progress: 100 });
return;
}
const links: HTMLLinkElement[] = [];
let loadedCount = 0;
const updateProgress = () => {
const progress = Math.round((loadedCount / images.length) * 100);
setStatus(prev => ({ ...prev, progress }));
};
// 组件卸载,清理 link 标签
const cleanup = () => {
links.forEach(link => {
if (document.head.contains(link)) {
document.head.removeChild(link);
}
});
};
try {
images.forEach((imagePath, index) => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = imagePath;
if (options.priority) {
link.setAttribute('fetchpriority', 'high');
}
link.onload = () => {
loadedCount++;
updateProgress();
if (loadedCount === images.length) {
setStatus({ loaded: true, error: null, progress: 100 });
options.onComplete?.();
}
};
link.onerror = (error) => {
const errorMessage = `Failed to preload image: ${imagePath}`;
setStatus(prev => ({
...prev,
error: new Error(errorMessage)
}));
options.onError?.(new Error(errorMessage));
};
links.push(link);
document.head.appendChild(link);
});
} catch (error) {
setStatus(prev => ({
...prev,
error: error as Error
}));
options.onError?.(error as Error);
}
return cleanup;
}, [images]);
return status;
};
优化效果
优化效果对比:
┌────────────────────────────┐ ┌────────────────────────────┐
│ 优化前 │ │ 优化后 │
├────────────────────────────┤ ├────────────────────────────┤
│ 加载时间: 2.3s │ │ 加载时间: 300ms │
│ 首屏渲染: 1.8s │ │ 首屏渲染: 500ms │
│ 用户等待: 明显 │ │ 用户等待: 几乎无感 │
└────────────────────────────┘ └────────────────────────────┘
部署为什么会讲到缓存呢? 因为缓存是 CDN 的一个主要概念。
二、CDN部署:全球加速的利器
1、CDN 基础概念
CDN(Content Delivery Network)内容分发网络是一种通过互联网互相连接的计算机网络系统。它利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户。
2、CDN架构设计
CDN部署架构:
┌─────────────────────────────────────────────────────────┐
│ 用户请求 │
└───────────────────────┬─────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ DNS解析 │
└───────────────────────┬─────────────────────────────────┘
↓
┌──────────────┬──────────────┬──────────────┬────────────┐
│ 中国节点 │ 美国节点 │ 欧洲节点 │ 其他节点 │
│ CN Node │ US Node │ EU Node │ Others │
└──────────────┴──────────────┴──────────────┴────────────┘
实现代码
网站中肯定会遇到一些静态资源、比如图片、一些公共的逻辑方法库,这些都可以放到 CDN,以获得更快的访问速度,但是这样会存在一个问题,就是这些静态资源本身更新之后,如何在主体包发版的时候,静态资源请求路径同步改变。
现在通用的做法就是根据文件内容计算哈希值,如果文件变化了哈希值同样会变化,这样就不会像使用 V1.1 这样后缀导致每次发布都要手动替换,比如这样
2.1 文件名 Hash 策略
从图片中可以看到,文件名包含了 hash 值(如 react.50f50de6.async.js),这是现代前端工程化的常见做法。
// webpack 配置示例
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
assetModuleFilename: 'assets/[name].[hash][ext]'
}
};
不同 hash 策略的选择:
- hash:整个项目内容一旦有变化,hash 就会变化
- chunkhash:根据不同的入口文件进行依赖文件解析,构建对应的 chunk,生成对应的 hash
- contenthash:根据文件内容生成 hash,内容不变,hash 就不变
2.2 资源 URL 处理
2.2.1 构建时处理
// webpack 配置
module.exports = {
output: {
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/assets/'
: '/',
}
};
2.2.2 运行时处理
// 动态设置 CDN 基础路径
window.__CDN_BASE_URL__ = 'https://cdn.example.com/assets/';
// 资源加载函数
function loadResource(path) {
return window.__CDN_BASE_URL__ + path;
}
3、CDN 部署流程
3.1 构建与上传
其实就是文件上传到 oss,这里举个例子🌰
// 部署脚本示例
const OSS = require('ali-oss');
const fs = require('fs');
const path = require('path');
async function deployToCDN() {
const client = new OSS({
region: 'oss-region',
accessKeyId: 'your-key-id',
accessKeySecret: 'your-key-secret',
bucket: 'your-bucket'
});
const distDir = path.join(__dirname, 'dist');
const files = fs.readdirSync(distDir);
for (const file of files) {
await client.put(
`assets/${file}`,
path.join(distDir, file)
);
}
}
3.1 缓存策略配置
# Nginx CDN 节点配置示例
location /assets/ {
expires 1y;
add_header Cache-Control "public, no-transform";
# 针对不同类型文件设置不同缓存策略
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 1M;
access_log off;
add_header Cache-Control "public";
}
location ~* \.(?:css|js)$ {
expires 1y;
access_log off;
add_header Cache-Control "public";
}
}
三、非覆盖式发布:化解更新风险
发布流程设计
非覆盖式发布流程:
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 构建新版本 │ │ 上传新版本 │ │ 切换软链接 │
│ Build New Ver │ --> │ Upload Files │ --> │ Switch Link │
└────────────────┘ └────────────────┘ └────────────────┘
│ │ │
v v v
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 版本备份 │ │ 健康检查 │ │ 清理旧版 │
│ Backup Ver │ <-- │ Health Check │ <-- │ Clean Old │
└────────────────┘ └────────────────┘ └────────────────┘
实现代码
class Deployer {
async deploy() {
const version = `${Date.now()}_${git.getCommitHash()}`;
const newVersionPath = `/app/versions/${version}`;
try {
// 1. 创建新版本目录
await fs.mkdir(newVersionPath);
// 2. 上传新版本文件
await this.uploadFiles(newVersionPath);
// 3. 原子性切换软链接
await this.atomicSymlink(newVersionPath, '/app/current');
// 4. 验证新版本
if (!await this.healthCheck()) {
await this.rollback();
throw new Error('Health check failed');
}
} catch (error) {
await this.cleanup(newVersionPath);
throw error;
}
}
}
四、CI/CD 基础概念
什么是 CI/CD?
CI/CD 流程图:
┌─────────────────────────────────────────────────────────┐
│ 持续集成 (CI) │
├────────────────┬────────────────┬────────────────┬─────┤
│ 代码提交 │ 自动构建 │ 自动测试 │代码检查│
│ Commit │ Build │ Test │ Lint │
└────────────────┴────────────────┴────────────────┴─────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 持续交付 (CD) │
├────────────────┬────────────────┬────────────────┬─────┤
│ 自动部署 │ 环境配置 │ 版本管理 │监控 │
│ Deploy │ Config │ Version │Monitor│
└────────────────┴────────────────┴────────────────┴─────┘
CI/CD 的价值
- 提升效率
- 自动化替代手动操作
- 标准化构建流程
- 快速反馈
- 保证质量
- 自动化测试
- 代码规范检查
- 构建质量把控
- 降低风险
- 小步快跑
- 问题早发现早解决
- 自动化回滚机制
CI/CD 是现代前端工程化中不可或缺的环节,cicd 解决的发布过程中配置问题,可以是的发布过更加的的平滑.
五、灰度发布:平稳过渡的艺术
灰度发布流程
灰度发布策略:
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 内部测试 │ │ 白名单用户 │ │ 5%用户 │
│ Internal Test │ --> │ Whitelist User │ --> │ 5% Traffic │
└────────────────┘ └────────────────┘ └────────────────┘
│ │ │
v v v
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 20%用户 │ │ 50%用户 │ │ 全量发布 │
│ 20% Traffic │ --> │ 50% Traffic │ --> │ Full Release │
└────────────────┘ └────────────────┘ └────────────────┘
[
六、监控告警:系统的眼睛和耳朵
监控体系设计
监控系统架构:
┌─────────────────────────────────────────────────────────┐
│ 监控数据采集层 │
├────────────────┬────────────────┬────────────────┬─────┤
│ 页面性能 │ JS错误 │ API监控 │ UV │
│ Performance │ Error │ API │ PV │
└────────┬───────┴───────┬───────┴────────┬──────┴─────┘
│ │ │
v v v
┌─────────────────────────────────────────────────────────┐
│ 数据处理层 │
├────────────────┬────────────────┬────────────────┬─────┤
│ 实时计算 │ 离线分析 │ 告警触发 │存储 │
│ Real-time │ Analysis │ Alert │Store│
└────────┬───────┴───────┬───────┴────────┬──────┴─────┘
│ │ │
v v v
┌─────────────────────────────────────────────────────────┐
│ 告警分发层 │
├────────────────┬────────────────┬────────────────┬─────┤
│ 钉钉通知 │ 邮件通知 │ 短信通知 │其他 │
│ DingTalk │ Email │ SMS │Other│
└────────────────┴────────────────┴────────────────┴─────┘
核心监控指标
关键指标看板:
┌────────────────────────────────────────────────────────┐
│ 核心业务指标 │
├──────────────┬──────────────┬──────────────┬──────────┤
│ 页面加载 │ API成功率 │ JS错误率 │ UV/PV │
│ ≤ 2s │ ≥ 99.9% │ ≤ 0.1% │ 监控趋势 │
└──────────────┴──────────────┴──────────────┴──────────┘
实现代码
// monitor.ts
class MonitorSDK {
private static instance: MonitorSDK;
private config: MonitorConfig;
private constructor(config: MonitorConfig) {
this.config = config;
this.initPerformanceMonitor();
this.initErrorMonitor();
this.initApiMonitor();
}
static getInstance(config: MonitorConfig): MonitorSDK {
if (!MonitorSDK.instance) {
MonitorSDK.instance = new MonitorSDK(config);
}
return MonitorSDK.instance;
}
private initPerformanceMonitor() {
// 性能监控
if (window.performance) {
const timing = performance.timing;
const loadTime = timing.loadEventEnd - timing.navigationStart;
this.report('performance', { loadTime });
}
}
private initErrorMonitor() {
// 错误监控
window.addEventListener('error', (event) => {
this.report('error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
});
});
}
private initApiMonitor() {
// API监控
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const startTime = Date.now();
try {
const response = await originalFetch(...args);
this.report('api', {
url: args[0],
duration: Date.now() - startTime,
status: response.status
});
return response;
} catch (error) {
this.report('api', {
url: args[0],
duration: Date.now() - startTime,
error: error.message
});
throw error;
}
};
}
private report(type: string, data: any) {
// 上报逻辑
const reportData = {
type,
data,
timestamp: Date.now(),
pageUrl: location.href,
userAgent: navigator.userAgent
};
// 采用 beacon 上报,防止页面卸载时丢失数据
navigator.sendBeacon(
this.config.reportUrl,
JSON.stringify(reportData)
);
}
}
七、应急预案:未雨绸缪
应急预案体系
应急处理流程:
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 告警触发 │ │ 问题定位 │ │ 应急处理 │
│ Alert Trigger │ --> │ Diagnosis │ --> │ Emergency │
└────────┬───────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
v v v
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 自动降级 │ │ 人工介入 │ │ 后续复盘 │
│ Auto-down │ <-- │Manual Control │ <-- │ Review │
└────────────────┘ └────────────────┘ └────────────────┘
降级策略实现
// degradation.ts
class DegradationManager {
private readonly strategies = new Map<string, () => Promise<void>>();
private readonly healthChecks = new Map<string, () => Promise<boolean>>();
constructor() {
this.initStrategies();
}
private initStrategies() {
// 策略1: 关闭非核心功能
this.strategies.set('disableNonCore', async () => {
window.__APP_CONFIG__.enableAnimation = false;
window.__APP_CONFIG__.enableRealTimeUpdate = false;
});
// 策略2: 使用本地缓存
this.strategies.set('useLocalCache', async () => {
const cache = await caches.open('emergency-cache');
// 实现缓存逻辑
});
// 策略3: 降低请求频率
this.strategies.set('reduceRequests', async () => {
window.__APP_CONFIG__.pollingInterval = 30000; // 降低轮询频率
window.__APP_CONFIG__.batchSize = 100; // 增加批处理大小
});
}
async degrade(level: 'low' | 'medium' | 'high'): Promise<void> {
const strategies = this.getStrategiesByLevel(level);
for (const strategy of strategies) {
try {
await this.strategies.get(strategy)?.();
console.log(`Strategy ${strategy} applied successfully`);
} catch (error) {
console.error(`Strategy ${strategy} failed:`, error);
}
}
}
private getStrategiesByLevel(level: string): string[] {
switch(level) {
case 'low':
return ['disableNonCore'];
case 'medium':
return ['disableNonCore', 'reduceRequests'];
case 'high':
return ['disableNonCore', 'reduceRequests', 'useLocalCache'];
default:
return [];
}
}
}
应急演练计划
演练计划表:
┌─────────────────────────────────────────────────────────┐
│ 季度演练计划 │
├────────────────┬────────────────┬────────────────┬─────┤
│ 场景演练 │ 压力测试 │ 故障恢复 │评估 │
│ Scenario │ Stress Test │ Recovery │Check │
├────────────────┼────────────────┼────────────────┼─────┤
│ 网络波动 │ 超量请求 │ 自动恢复 │完整性 │
│ 服务器宕机 │ 并发访问 │ 人工介入 │及时性 │
│ 数据异常 │ 资源耗尽 │ 降级恢复 │有效性 │
└────────────────┴────────────────┴────────────────┴─────┘