1

你可以在FacebookMedium上遇到过渐进式图片,当页面滚动到视图时,模糊的低分辨率图像会被清晰的全分辨率版本替换。

图片描述

预览图片非常小(也许是20px宽的高压缩JPEG格式),该文件可以小于300字节,并立即出现快速加载的模糊轮廓,当需要的时候,会通过延迟加载的形式加载真实图像。

渐进式图像非常伟大,但是目前的解决方案比较复杂。幸运的是,我们可以用HTML5/CSS3/JavaScript构建一个示例,示例代码:

  • 快速轻量级——只需463字节的CSS和1007字节的JavaScript(经压缩)

  • 支持响应式图片加载更大或更高分辨率(Retina)屏幕的替代版本

  • 没有依赖——可与任何框架工作

  • 兼容所有现代浏览器(IE10+)

  • 在旧版浏览器中,或是当JavaScript/图片加载失败时,会渐进增强

  • 易于使用

我们的演示和GitHub代码

这是我们的demo示例
Download the code from GitHub

HTML代码

我们会以一些基础HTML来实现渐进式图片:

<a href="full.jpg" class="progressive replace">
  <img src="tiny.jpg" class="preview" alt="image" />
</a>

此处:

  • full.jpg 是清晰的大分辨率图片,地址在href

  • tiny.jpg 是轻量的预览图片

我们已经有一个小型工作系统了,没有任何的JavaScript(也许在旧的浏览器中会失效),用户可以通过点击预览完整的图片。

两个图片必须有相同的宽高比,例如,如果full.jpg是800*200,则其最终的宽高比是4:1,那么tiny.jpg可以是4:1,但是不能使用30px的宽度,因为那样高度将会是一个分数而不是7.5px。

注意链接和预览图片上使用到的classes,这些会被我们运用到JavaScript中。

内联或外联图片

预览图片可以以data URL的形式内联,例如:

<img src="data:image/jpeg;base64,/9j/4AAQSkZJ..."  class="preview" />

内联图片会立即显示,需要较少的HTTP请求,并避免额外的页面回流,但是:

  • 他需要更多资源来添加或更改内联图片(虽然构建时候, 可以借助如Gulp的帮助)

  • base-64编码效率较低,通常比二进制数据大30%(尽管这被额外的HTTP请求头抵消了)

  • 内联图片无法缓存在本地,它们只能在HTML页面中缓存,如果不提出相同的请求,它们不能在另一个页面使用

  • HTTP/2减少了对内联图片的需求

比较实用的是:如果内联图片只在单个页面使用,并且所需代码量很小(如URL比较短),内联图片是一个不错的选择。

CSS

我们先来定义container样式:

a.progressive {
  position: relative;
  display: block;
  overflow: hidden;
  outline: none;
}

这是设置container容器的布局属性,如果有需要,link可以应用其他类和样式设置尺寸或位置。

你可以考虑使用精确的尺寸或使用padding-top来强制实现固有的宽高比,这能确保在容器进行尺寸调整前避免图片加载的负载和回流,不过这要计算每个图像的大小和宽高比。我选择比较简单的方式:

  • 预览和大图必须具有相同的宽高比(见上文)

  • 预览图片将几乎立即定义容器的高度,因为它是内联的,或是快速加载的

再次提示:如果你在一个包含大量图片的网页上,定义了容器固定的宽度和高度,效果会更好,例如一个图库(所有的图片都可能具有相同的宽高比)。

当完整的大图加载完成并且点击事件停止时,容器上的'replace'类将被删除,因此,我们可以删除标准链接指针。

a.progressive:not(.replace) {
  cursor: default;
}

容器中的预览图和大图根据容器的宽度调整大小:

a.progressive img {
  display: block;
  width: 100%;
  max-width: none;
  height: auto;
  border: 0 none;
}

注意height:auto是必须的,IE10/11可能会在计算图像高度的时候出错。

预览图像使用2vw的长度模糊,确保模糊后看起来有相似的轮廓,而与页面大小无关。在container中应用overflow: hidden可为容器提供一个硬边缘。它也缩放1.05倍,防止通过图片的模糊外边缘看到图片的背景颜色。这表示我们可以使用令人愉快的缩放效果来显示完整的图像。

a.progressive img.preview {
  filter: blur(2vw);
  transform: scale(1.05);
}

最后,我们定义图片完整显示时候的样式和动画:

a.progressive img.reveal {
  position: absolute;
  left: 0;
  top: 0;
  will-change: transform, opacity;
  animation: reveal 1s ease-out;
}

@keyframes reveal {
  0% {transform: scale(1.05); opacity: 0;}
  100% {transform: scale(1); opacity: 1;}
}

大图位于预览图上方,在1秒内,不透明度从0增加到1,刻度从1.05变为1。你可以根据自己的需要增加其他的转换/过滤效果。

JavaScript

我们遵循的是渐进增强模式,所以JavaScript代码最初会向页面添加load事件监听器之前检查所需浏览器的API是否可用。

// progressive-image.js
if (window.addEventListener && window.requestAnimationFrame && document.getElementsByClassName) window.addEventListener('load', function() {

当页面和所有资源加载完成时,将触发load事件。我们不希望大型图片在基本资源(如字体、CSS、JavaScript和预览图片)加载完成之前加载(这可能发生:我们使用DOMContentLoaded事件——当DOM准备就绪时触发的事件)。

接下来,我们获取获取类名为progressivereplace的所有图像容器元素:

var pItem = document.getElementsByClassName('progressive replace'), timer;

getElementsByClassName()返回一个活动的类数组HTMLCollection,匹配到的元素将从页面中添加或删除。它的好处是很快就会显示出来。

接下来,我么将定义一个inView()函数,该函数通过其getBoundingClientRect()方法和window.pageYOffset垂直滚动位置进行比较,从而确定容器是否在视图内。

// image in view?
function inView() {
  var wT = window.pageYOffset, wB = wT + window.innerHeight, cRect, pT, pB, p = 0;
  while (p < pItem.length) {

    cRect = pItem[p].getBoundingClientRect();
    pT = wT + cRect.top;
    pB = pT + cRect.height;

    if (wT < pB && wB > pT) {
      loadFullImage(pItem[p]);
      pItem[p].classList.remove('replace');
    }
    else p++;
  }
}

当容器在视图中时,它的节点会被传递到loadFullImage()函数内,并且replace类会被删除(会立即从pItem HTMLCollection中删除节点,因此容器不会被再次重新处理)。

loadFullImage()函数创建了一个新的HTML Image()对象,并根据需要设置其值,即将容器的href的值复制到src属性中,并应用一个reveal类。

// replace with full image
function loadFullImage(item) {
  if (!item || !item.href) return;

  // load image
  var img = new Image();
  if (item.dataset) {
    img.srcset = item.dataset.srcset || '';
    img.sizes = item.dataset.sizes || '';
  }
  img.src = item.href;
  img.className = 'reveal';
  if (img.complete) addImg();
  else img.onload = addImg;

加载图片后调用内部的addImg函数:

// replace image
  function addImg() {
    // disable click
    item.addEventListener('click', function(e) { e.preventDefault(); }, false);

    // add full image
    item.appendChild(img).addEventListener('animationend', function(e) {
      // remove preview image
      var pImg = item.querySelector && item.querySelector('img.preview');
      
      if (pImg) {
        e.target.alt = pImg.alt || '';
        item.removeChild(pImg);
        e.target.classList.remove('reveal');
      }
    });
  }
}

注意:

  • 禁用容器上的点击事件

  • 将图像附加到开始淡出淡入/缩放动画的页面

  • 使用animationend监听器等待动画结束,然后复制alt
    内容。删除预览图片节点,并从完整图片中删除reveal类。这一步有助于提高性能,并且防止在调整Edge浏览器大小时出现一些奇怪的剪切问题。

最后,我们必须调用inView()函数来检查所有的渐进式图片容器在首次运行时是否在页面上可见。

inView();

我们还必须在滚动页面或调整浏览器大小时调用函数,在一些旧的浏览器(主要指IE)可以非常迅速地对这些事件作出回应,所以我们需要限制回调,以确保它不能在300毫秒内被再一次调用。

window.addEventListener('scroll', scroller, false);
window.addEventListener('resize', scroller, false);

function scroller(e) {
  timer = timer || setTimeout(function() {
    timer = null;
    requestAnimationFrame(inView);
  }, 300);
}

注意,对requestAnimationFrame的调用,它将在下一次重绘inView函数。

响应式图片

HTML 中 image的srcsetsizes属性定义了不同大小和分辨率的多个图像,浏览器可以为设备选择最合适的版本。

上面的代码支持这个功能——添加data-srcsetdata-sizes属性到link容器,例如:

<a href="small.jpg"
  data-srcset="small.jpg 800w, large.jpg 1200w"
  data-sizes="100vw"
  class="progressive replace">
  <img src="preview.jpg" class="preview" alt="image" />
</a>

加载完成后,完整的图片代码将是:

<img src="small.jpg"
    srcset="small.jpg 800w, large.jpg 1200w"
    sizes="100vw"
    alt="image" />

当视图窗口宽度为800px或更高时,现代浏览器将加载large.jpg,旧版浏览器和视图窗口较小的浏览器会加载small.jpg。详细信息,请查阅 How to Build Responsive Images with srcset

使用笔记

我会保持代码的轻量性,并且可以随时使用和易于改进,有待优化的地方有:

  • 水平方向的滚动检查。目前只检查垂直方向的滚动

  • 动态添加渐进式图像。使用JavaScript动态添加的渐进式图像只有在发生滚动或调整大小事件发生时才会被替换

  • Firefox性能。浏览器在替换大图片时可能会遇到困难(你也许可以看到明显的闪烁)

作者:Craig Buckler
原文:How to Build Your Own Progressive Image Loader


minyillee
296 声望16 粉丝

职业:打杂