现代前端部署实践指南

318 阅读6分钟

现代前端部署实践指南

2021_08_26_08_27_IMG_0019.JPG

"部署不是终点,而是产品生命周期的新起点。"

一、浏览器缓存:性能优化的第一道防线

前端工程化一直是前端开发中一个比较热门的话题,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 NodeUS NodeEU NodeOthers    │
└──────────────┴──────────────┴──────────────┴────────────┘
实现代码

网站中肯定会遇到一些静态资源、比如图片、一些公共的逻辑方法库,这些都可以放到 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 的价值

  1. 提升效率
    • 自动化替代手动操作
    • 标准化构建流程
    • 快速反馈
  2. 保证质量
    • 自动化测试
    • 代码规范检查
    • 构建质量把控
  3. 降低风险
    • 小步快跑
    • 问题早发现早解决
    • 自动化回滚机制

CI/CD 是现代前端工程化中不可或缺的环节,cicd 解决的发布过程中配置问题,可以是的发布过更加的的平滑.

五、灰度发布:平稳过渡的艺术

灰度发布流程

灰度发布策略:
┌────────────────┐     ┌────────────────┐     ┌────────────────┐
│    内部测试     │     │   白名单用户    │     │     5%用户     │
│  Internal Test │ --> │ Whitelist User │ --> │   5% Traffic   │
└────────────────┘     └────────────────┘     └────────────────┘
│                      │                      │
v                      v                      v
┌────────────────┐     ┌────────────────┐     ┌────────────────┐
│    20%用户      │     │    50%用户     │     │    全量发布     │
│  20% Traffic   │ --> │  50% Traffic   │ --> │  Full Release  │
└────────────────┘     └────────────────┘     └────────────────┘

[

六、监控告警:系统的眼睛和耳朵

监控体系设计

监控系统架构:
┌─────────────────────────────────────────────────────────┐
│                     监控数据采集层                        │
├────────────────┬────────────────┬────────────────┬─────┤
│    页面性能     │    JS错误      │    API监控     │ UV  │
│  PerformanceErrorAPIPV  │
└────────┬───────┴───────┬───────┴────────┬──────┴─────┘
│               │                │
v               v                v
┌─────────────────────────────────────────────────────────┐
│                     数据处理层                           │
├────────────────┬────────────────┬────────────────┬─────┤
│    实时计算     │    离线分析     │    告警触发     │存储 │
│   Real-time    │   AnalysisAlertStore│
└────────┬───────┴───────┬───────┴────────┬──────┴─────┘
│               │                │
v               v                v
┌─────────────────────────────────────────────────────────┐
│                     告警分发层                           │
├────────────────┬────────────────┬────────────────┬─────┤
│    钉钉通知     │    邮件通知     │    短信通知     │其他 │
│   DingTalkEmailSMSOther│
└────────────────┴────────────────┴────────────────┴─────┘

核心监控指标

关键指标看板:
┌────────────────────────────────────────────────────────┐
│                     核心业务指标                         │
├──────────────┬──────────────┬──────────────┬──────────┤
│   页面加载     │   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 [];
    }
  }
}

应急演练计划

演练计划表:
┌─────────────────────────────────────────────────────────┐
│                     季度演练计划                          │
├────────────────┬────────────────┬────────────────┬─────┤
│    场景演练     │    压力测试     │   故障恢复     │评估  │
│  ScenarioStress TestRecoveryCheck │
├────────────────┼────────────────┼────────────────┼─────┤
│  网络波动      │   超量请求      │   自动恢复     │完整性 │
│  服务器宕机    │   并发访问      │   人工介入     │及时性 │
│  数据异常      │   资源耗尽      │   降级恢复     │有效性 │
└────────────────┴────────────────┴────────────────┴─────┘

引用参考
juejin.cn/post/731620…
www.zhihu.com/question/20…