前端全局替换图片服务cdn的解决方案

前言

主要介绍下当我们的产品中依赖的某一个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,但是这样性能会成为问题。

nginx转发

这种方法应该是一劳永逸的。直接是服务器层面全部拦截。

但是这样的方法有几个场景是不适用的。

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的介绍这里不再重复了。在开发云课堂首页优化的时候已经有过相关整理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的方法。

如果有更好的解决方案,或者有我没有考虑到的问题,欢迎联系我交流技术。

原文链接,欢迎讨论

你可能感兴趣的:(Web工程化)