CDN 实战总结
引言
在之前的文章中我们也讲述了什么是 CDN 以及 CDN 的选择方式,现在我们可以开始一番实践了,只有在实践中才成长的更快嘛。我将带大家一起来实现多CDN动态择优加载策略,集成超时检测、指数退避重试和本地缓存优化,通过Performance API构建可视化性能监控体系,确保第三方资源的高可靠加载
。
一、核心代码全解析
1.1 预连接优化
首先,我们找到项目的index.html
文件,在文件中做好预连接,以及相应的资源初始化配置,为后续我们编写CDN代码打下基础。 1.1.1预连接的代码
<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资源配置初始化
// 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 动态加载资源代码
// 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故障转移代码
// 智能 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 资源加载性能追踪代码
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 检查资源加载代码
// 检查所有资源是否已加载完成(用于性能加载追踪方法中)
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 分析供应商性能代码
// 分析 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 偏好设置');
}
}
}
四、主动加载资源与总结
在我们写好各种方法之后,我们也要对这些方法做一个运用: 具体实现如下
// 主动加载所有资源
(function() {
const config = window.CDN_CONFIG;
const resources = Object.keys(config.resources);
const provider = config.activeProvider;
resources.forEach(resource => {
// 动态加载资源
loadResource(resource, provider);
});
})();
到这里这篇博客也就结束啦,希望看了这篇博客的你,也能有所收获哦~