Skip to content

CDN 实战总结

引言

在之前的文章中我们也讲述了什么是 CDN 以及 CDN 的选择方式,现在我们可以开始一番实践了,只有在实践中才成长的更快嘛。我将带大家一起来实现多CDN动态择优加载策略,集成超时检测、指数退避重试和本地缓存优化,通过Performance API构建可视化性能监控体系,确保第三方资源的高可靠加载

一、核心代码全解析

1.1 预连接优化

首先,我们找到项目的index.html文件,在文件中做好预连接,以及相应的资源初始化配置,为后续我们编写CDN代码打下基础。 1.1.1预连接的代码

html
  <link rel="preconnect" href="https://unpkg.com" crossorigin="anonymous"> 
  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>

可能有些小伙伴没有遇见过crossorigin,其实crossorigin就是一个用于指定跨域请求方式的属性,他的默认值是'anonymous',也就是默认不发送用户凭据(eg:cookies),而相对应的'use-credentials'表示发送用户凭据。 做好预连接之后就要开始相应的CDN配置啦,这里的配置其实也不难,主要就是定义Window.CDN_CONFIG,在里面编写上CDN提供商配置以及资源配置、当前活动资源提供者......

1.2资源配置初始化

1.2.1资源配置初始化

js
// CDN 配置
window.CDN_CONFIG = {
// CDN 提供商配置
providers: [
  { name: 'unpkg', status: 'pending', loadTime: null, priority: 1 },
  { name: 'jsdelivr', status: 'pending', loadTime: null, priority: 2 },
  { name: '本地', status: 'pending', loadTime: null, priority: 3 }
],
// 资源配置
resources: {
  'vue': {
    paths: {
      'unpkg': 'https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js',
      'jsdelivr': 'https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js',
      '本地': './node_modules/vue/dist/vue.global.prod.min.js'
    },
    integrity: {
      'unpkg': '',
      'jsdelivr': '',
      '本地': ''
      },
    loaded: false, // 资源是否已加载的状态
    loadingStartTime: null, // 资源加载开始的时间
    timeoutId: null // 加载超时的标识符
  },
  'vue-router': {
    paths: {
      'unpkg': 'https://unpkg.com/vue-router@4.2.4/dist/vue-router.global.prod.js',
      'jsdelivr': 'https://cdn.jsdelivr.net/npm/vue-router@4.2.4/dist/vue-router.global.prod.js',
      '本地': './node_modules/vue-router/dist/vue-router.global.prod.min.js'
    },
    integrity: {
      'unpkg': '',
      'jsdelivr': '',
      '本地': ''
    },
    loaded: false,
    loadingStartTime: null,
    timeoutId: null
  },
  'pinia': {
    paths: {
      'unpkg': 'https://unpkg.com/pinia@2.1.6/dist/pinia.iife.prod.js',
      'jsdelivr': 'https://cdn.jsdelivr.net/npm/pinia@2.1.6/dist/pinia.iife.prod.js',
      '本地': './node_modules/pinia/dist/pinia.iife.prod.min.js'
    },
    integrity: {
      'unpkg': '',
      'jsdelivr': '',
      '本地': ''
    },
    loaded: false,
    loadingStartTime: null,
    timeoutId: null
  }
},
// 当前活动的资源提供者
activeProvider: 'unpkg',
// 失败的资源提供者列表
failedProviders: [],
// 重试延迟时间(毫秒)
retryDelay: 100,
// 最大重试次数
maxRetries: 2,
// 当前重试计数
retryCount: 0,
// 资源加载超时时间(毫秒)
loadTimeout: 5000, // 5秒超时
// 性能度量指标
metrics: { 
  // 加载开始时间
  loadStart: Date.now(),
  // 资源加载时间记录
  resourceTiming: {},
  // 错误记录
  errors: []
}
};

以上就是第一部分的具体代码啦,注释写的也算相对完善了,那我这里就不再赘述啦,让我们进入下一部分。

二、资源加载与故障转移

s当我们做好了预加载以及定义了相应的CDN资源,那么现在就该思考了,如何让我们定义好的资源发挥它的最大作用呢?这里我首先考虑到了资源的动态加载。资源的加载自然有成功和失败,那么为了整个程序的稳健,就会让人去思考,如果加载失败了怎么办,是放弃还是进行故障的转移呢?这里我选择了故障转移。

2.1 动态加载资源

在加载资源之前,我们要先拿到相应的配置以及检查配置是否有效,即供应商、资源是否存在且有效?这里可以通过判断和return进行... 既然是动态加载资源,就要对资源的加载时间来进行记录,并判断资源的加载时间是否超时,如果超时了应该如何故障转移。包括在加载资源时,我们也应该要对资源的完整性进行验证,如果哈希值不匹配的话,那就要将资源先移除。资源加载成功后我们要将资源进行标记,失败进行转移。

2.1.1 动态加载资源代码
js
// 1. 动态加载资源
function loadResource(resource, provider) {
  const config = window.CDN_CONFIG;
  console.log('window.CDN_CONFIG',config);
  
  const resourceConfig = config.resources[resource];
  console.log('resourceConfig',resourceConfig);
  
  if (!resourceConfig || !resourceConfig.paths[provider]) {
    console.error(`[CDN错误] 无效的资源或提供商: ${resource}, ${provider}`);
    return;
  }
  
  // 通过提供商名称从资源配置中获取对应的资源路径和完整性哈希值
  // 这样做是为了确保加载的资源是正确的,并且可以通过完整性验证来防止篡改
  const url = resourceConfig.paths[provider];
  const integrity = resourceConfig.integrity[provider];
  // 记录加载开始时间
  resourceConfig.loadingStartTime = Date.now();
  
  // 设置加载超时
  resourceConfig.timeoutId = setTimeout(() => {
    // 资源加载超时,进行故障转移
    loadBackupCDN(resource, 'timeout');
  }, config.loadTimeout);

  const script = document.createElement('script');
  script.src = url;
  // 添加完整性验证 - 暂时移除,因为哈希值不匹配
  if (integrity && provider !== '本地' && false) {
    script.integrity = integrity;
    script.crossOrigin = 'anonymous';
  } else {
    // 添加 crossOrigin 属性可以确保浏览器在加载资源时允许跨域请求,
    // 这样在将来添加 integrity 属性时,浏览器能够正确验证资源的完整性。
    // 没有 crossOrigin,某些跨域资源可能会因为安全策略而无法进行完整性验证。
    script.crossOrigin = 'anonymous'; 
  }
  
  // 为 script 添加事件处理程序
  script.onload = () => trackResourceTiming(resource, provider, url); // 资源加载成功时记录时间
  script.onerror = () => loadBackupCDN(resource, 'error'); // 资源加载失败时进行故障转移
  
  // 添加到文档
  document.head.appendChild(script);
  
  console.info(`[CDN加载] 正在从 ${provider} 加载 ${resource}`);
}

2.2 智能CDN故障转移

前面我们提到了如果CDN加载失败了怎么办,我们也想到了对他进行故障转移,具体是怎么实现的呢,让我们一起看看吧!

2.2.1 智能CDN故障转移代码
js

// 智能 CDN 故障转移
function loadBackupCDN(resource, reason = 'error') {
  // window.CDN_CONFIG 是一个全局配置对象,包含了 CDN 资源、提供商信息和性能指标等数据。
  const config = window.CDN_CONFIG;
  const resourceConfig = config.resources[resource]; // 获取指定资源的配置
  
  // 清除超时定时器为了避免在资源加载失败后仍然执行超时处理,确保资源加载逻辑的准确性
  if (resourceConfig.timeoutId) {
    clearTimeout(resourceConfig.timeoutId);
    resourceConfig.timeoutId = null;
  }
  
  // 记录错误
  const currentProvider = config.activeProvider;
  config.metrics.errors.push({
    resource: resource,
    provider: currentProvider,
    reason: reason,
    timestamp: Date.now()
  });
  
  // 更新当前提供商状态
  const providerIndex = config.providers.findIndex(p => p.name === currentProvider);
  if (providerIndex !== -1) {
    config.providers[providerIndex].status = 'failed';
    
    // 添加到失败列表
    if (!config.failedProviders.includes(currentProvider)) {
      // failedProviders 是一个数组,用于存储加载失败的 CDN 提供商
      config.failedProviders.push(currentProvider);
    }
  }
  
  console.warn(`[CDN故障] ${currentProvider} 加载 ${resource} 失败,原因: ${reason}`);
  
  // 如果已经尝试过所有提供商,则不再重试
  if (config.failedProviders.length >= config.providers.length) {
    console.error(`[CDN故障] 所有 CDN 提供商加载 ${resource} 失败`);
    return;
  }
  
  // 使用指数退避策略进行重试
  if (config.retryCount < config.maxRetries) {
    config.retryCount++;
    
    // 计算退避时间,使用指数退避策略可以有效减少对失败提供商的重复请求,避免网络拥堵
    const backoffTime = config.retryDelay * Math.pow(2, config.retryCount - 1);
    
    console.info(`[CDN故障] 将在 ${backoffTime}ms 后尝试下一个提供商 (${config.retryCount}/${config.maxRetries})`);
    
    setTimeout(() => {
      // 选择下一个未失败的提供商,确保在可用的提供商中进行切换,以提高成功加载的概率
      const nextProvider = config.providers.find(p => !config.failedProviders.includes(p.name));
      
      if (nextProvider) {
        config.activeProvider = nextProvider.name;
        console.info(`[CDN故障] 切换到 ${nextProvider.name} 提供商`);
        
        // 动态加载资源
        loadResource(resource, nextProvider.name);
      }
    }, backoffTime);
  }
}

这里我们还是和动态加载资源一样,先拿到window.CDN_CONFIG这个全局配置对象,之后对他进行一番处理。既然资源加载失败了,那我们要思考资源加载过程中有没有一些事件还在运行呢?对,定时器。我们在动态资源函数中用到了定时器,既然加载失败了,就要将定时器来做一个清理。 做好这些前置工作后,我们也要开始真正的故障转移啦,首先是记录错误,获取当前的活动者并将其存储在currentProvider中。错误记录完之后就更新当前供应商的状态,防止重复尝试,我们需要将他添加到失败列表。当我们试过所有的提供商,那我们就需要return这个函数,不要让他再执行了,为了方便调试,可以log一下。 刚才我们提到了尝试完所有的供应商, 现在就是说说重试过程啦,也就是 指数退避策略进行重试,这里实现时要考虑一点,就是在重试的时候,次数一定不要超过我们规定的重试次数,所以要加上判断。之后计算退避时间:这里我们使用指数退避策略可以有效减少对失败提供商的重复请求,也一定程度上避免了网络拥堵const backoffTime = config.retryDelay * Math.pow(2, config.retryCount - 1);,在根据backoffTime的时间来设置一个定时器做供应商的切换,以提高成功加载的概率。

三、性能追踪分析与检查加载

3.1 资源加载性能追踪

我们在引言部分提到的Performance API就是在这部分使用啦,主要是用于获取加载时间。

3.1.1 资源加载性能追踪代码
js
function trackResourceTiming(resource, provider, url) {
  // config 是一个包含 CDN 配置信息的对象,具体包括资源的加载状态和性能指标等
  const config = window.CDN_CONFIG;
  const resourceConfig = config.resources[resource];
  
  // 清除超时计时器
  if (resourceConfig.timeoutId) {
    clearTimeout(resourceConfig.timeoutId);
    resourceConfig.timeoutId = null;
  }
  
  // 标记资源已加载
  if (resourceConfig) {
    resourceConfig.loaded = true;
  }
  
  // 更新提供商状态
  const providerIndex = config.providers.findIndex(p => p.name === provider);
  if (providerIndex !== -1) {
    config.providers[providerIndex].status = 'success';
  }
  
  // 使用 Performance API 获取加载时间
  // getEntriesByName 方法用于返回与指定名称匹配的所有性能条目
  if (window.performance && window.performance.getEntriesByName) {
    try {
      const entries = performance.getEntriesByName(url);
      if (entries && entries.length > 0) {
        const entry = entries[0];
        
        // 记录加载时间
        if (providerIndex !== -1) {
          config.providers[providerIndex].loadTime = entry.duration;
        }
        
        // 存储详细性能数据
        config.metrics.resourceTiming[resource] = {
          provider: provider,
          duration: entry.duration,
          transferSize: entry.transferSize,
          decodedBodySize: entry.decodedBodySize,
          startTime: entry.startTime
        };
        
        console.info(`[CDN性能] ${provider} 加载 ${resource}: ${entry.duration.toFixed(2)}ms`);
      }
    } catch (e) {
      console.error('性能数据收集失败:', e);
    }
  }
  
  // 检查是否所有资源都已加载
  checkAllResourcesLoaded();
}

3.2 检查资源加载

在上一段代码中我们用到了资源加载的检查,现在让我们来实现这段代码吧,开始也是先拿到相应的配置对象,后用Object.keys来获取对象的所有可枚举属性名称,并返回一个数组,filter过滤那些未加载完成的数组,并返回一个新的数组,若加载完的数组和所有可枚举属性数组的长度一致,则加载完成~

3.2.1 检查资源加载代码
js
  // 检查所有资源是否已加载完成(用于性能加载追踪方法中)
  function checkAllResourcesLoaded() {
    const config = window.CDN_CONFIG;
    // Object.keys 方法用于获取对象的所有可枚举属性的名称,并返回一个数组
    const allResources = Object.keys(config.resources);
    const loadedResources = allResources.filter(r => config.resources[r].loaded);
    
    if (loadedResources.length === allResources.length) {
      const totalTime = Date.now() - config.metrics.loadStart;
      console.info(`[CDN性能] 所有资源加载完成,总耗时: ${totalTime}ms`);
      
      // 分析最佳 CDN 提供商
      analyzeProviderPerformance();
    }
  }

3.3 分析CDN供应商性能

为了后续的优化,我们也要对供应商的性能进行分析,这里也是通过fileter过滤不成功的对象,之后在成功的对象中对他们的加载时间进行一个分析排序,并把最佳供应商存入本地,便于下次使用。

3.3.1 分析供应商性能代码
js
// 分析 CDN 提供商性能(用于检查所有资源是否加载完成后,分析最佳 CDN 提供商)
      function analyzeProviderPerformance() {
        const config = window.CDN_CONFIG;
        const successfulProviders = config.providers.filter(p => p.status === 'success' && p.loadTime !== null);
        
        if (successfulProviders.length > 0) {
          // 按加载时间排序
          successfulProviders.sort((a, b) => a.loadTime - b.loadTime);
          const fastestProvider = successfulProviders[0];
          
          console.info(`[CDN分析] 最快的提供商: ${fastestProvider.name}, 平均加载时间: ${fastestProvider.loadTime.toFixed(2)}ms`);
          
          // 这里可以存储最佳提供商信息到 localStorage,下次优先使用
          try {
            localStorage.setItem('preferredCdnProvider', fastestProvider.name);
          } catch (e) {
            console.warn('无法保存 CDN 偏好设置');
          }
        }
      }

四、主动加载资源与总结

在我们写好各种方法之后,我们也要对这些方法做一个运用: 具体实现如下

js
// 主动加载所有资源
(function() {
  const config = window.CDN_CONFIG;
  const resources = Object.keys(config.resources);
  const provider = config.activeProvider;
  
  resources.forEach(resource => {
    // 动态加载资源
    loadResource(resource, provider);
  });
})();

到这里这篇博客也就结束啦,希望看了这篇博客的你,也能有所收获哦~