主要介绍下当我们的产品中依赖的某一个cdn如图片服务下线后,有哪几种方案来解决和处理这种情况。分别就手动代码层替换、全局异常监控、样式图片资源处理、nginx映射修改和serviceworker的各种方法进行对比。
首先,一个cdn服务如果即将下线,我们需要把资源批量导到另一个cdn中。如果资源本身都不存在了,也没有办法解决这个问题了。
cdn服务的图片,我们在代码中一般都会去指定url的引用。
比如:
<img class="f-fl" src="http://img1.ph.126.net/fJDziquhVcOupiyTYX9MDg==/3359685322118823946.png">
如果项目维护性高,可能会将前缀设置为全局统一变量。
当我们要下线img1.ph.126.net
,而转到img-ph-mirror.nosdn.127.net
时。
我们可以使用全局代码替换的方式。这样的做法缺点就是:
1、如果前期开发人员维护意识差,需要每个位置都手动修改,并进行相关测试,工作量大。
2、由于只修改了代码层面,数据库内已有的数据无法修改,接口返回的动态数据加载资源依旧无法访问。
在前端全局异常监控,拿到加载失败的图片,进行替换再手动操作dom来完成图片的替换。
核心逻辑如下:
window.addEventListener(
"error",
function(e) {
// 当前异常是由图片加载异常引起的
var target = e.target;
if (
target &&
target.tagName &&
target.tagName.toUpperCase() === "IMG" &&
// 判断是否是已失效的图片服务
_isImgph.test(target.src)
) {
// 进行相应的图片替换规则
var newSrc = scaleImage(e.target.src);
// 用于异常上报
console.warn("img加载图片失败:" + e.target.src);
// 再手动赋值
e.target.src = newSrc;
}
},
true
);
这种方式看起来可以解决数据库和接口里来的动态数据。
但是它的缺陷也很明显:
1、存在资源加载顺序问题,如果资源先加载未捕获到(比如首屏渲染的数据),则失效;要解决必须把这块逻辑放最头部加载。
2、js只能监听js中的图片资源加载失败。如果是样式中的资源加载失败,这种方案毫无效果。
针对样式中的图片资源监控是个比较麻烦的问题,因为目前浏览器并没有提供相应的API来捕获其失败。
但是hack风格的解决方法也是有的,这里需要用到一个库imagesloaded的思路。其源码分析可以参见imagesloaded源码分析。
核心源码如下:
// 处理背景图片的情况
ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
var style = getComputedStyle( elem );
if ( !style ) {
// Firefox returns null if in a hidden iframe https://bugzil.la/548397
return;
}
// get url inside url("...")
var reURL = /url\((['"])?(.*?)\1\)/gi;
var matches = reURL.exec( style.backgroundImage );
while ( matches !== null ) {
var url = matches && matches[2];
if ( url ) {
// 拿到url
this.addBackground( url, elem );
}
matches = reURL.exec( style.backgroundImage );
}
};
// 背景图加载中对象
function Background( url, element ) {
this.url = url;
this.element = element;
// 通过new Image来进行js操作。
this.img = new Image();
}
// 检查url是否能正常加载
Background.prototype.check = function() {
this.img.addEventListener( 'load', this );
this.img.addEventListener( 'error', this );
this.img.src = this.url;
// check if image is already complete
var isComplete = this.getIsImageComplete();
if ( isComplete ) {
this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
this.unbindEvents();
}
};
主要思路就是根据你注册的dom,用getComputedStyle()拿到节点的样式,再去通过new Image来判断是否加载异常。异常则手动拼接dom样式。
这种方法的缺点是:
1、需要注册dom列表,虽然我们可以通过document.all
拿到所有的dom,但是这样性能会成为问题。
这种方法应该是一劳永逸的。直接是服务器层面全部拦截。
但是这样的方法有几个场景是不适用的。
1、当需要替换的cdn是一对多时,不能直接修改。比如网易云课堂需要img1.ph.126.net
->img-ph-mirror.nosdn.127.net
,而网易考拉需要img1.ph.126.net
->kaola-mirror.nosdn.127.net
时,不能直接修改img1.ph.126.net
。
2、当替换规则比较复杂时,需要依赖工程内业务util方法时。
利用serviceworker可以拦截全局请求的功能,在前端实现全局拦截并替换。
关于serviceworker的介绍这里不再重复了。在开发云课堂首页优化的时候已经有过相关整理serviceworker使用实践。
核心逻辑如下:
self.addEventListener("fetch", function(event) {
if (
// 拦截失效的cdn
(_isImgph.test(event.request.url) ||
_isImgSize.test(event.request.url)) &&
event.request.method === "GET"
) {
// 进行相应替换
var url = scaleImage(event.request.url);
// 新建request
var req = new Request(url);
event.respondWith(
fetch(req).then(function(response) {
return response;
})
);
}
});
这种方法的缺点是:
1、兼容性问题,由于是相对较新的特性,ie全线和老版本移动wap不支持。
2、不能跨域,所以多域名产品需要每个域去注册。
笔者最终考虑了兼容性需求和急迫性,使用了serviceworker的方法。
如果有更好的解决方案,或者有我没有考虑到的问题,欢迎联系我交流技术。
原文链接,欢迎讨论