在Web开发社区,响应式图片已经成为最大的挫败之一。原因也很简单:页面平均大小产品能从去年的1MB达到了惊人的1.5MB。其中图片大小的增长比例就占了页面大小增长的60%或更多,并且这个比例还在不断攀升。
绝大多数的页面是可以降低页面大小的,如果你借助基于设备宽度、像素密度和现代图像格式(例如WebP)等优化条件的话。这些减重的方法可以加快载入时间,让用户参与更多、停留更长时间。不过这里不是争论是否应该为不同的设备优化图像,而是关于应该怎么去做。
理想情况下,我们应该继续使用img
标签,然后浏览器会下载适配设备宽度、适配页面布局的图片。然而,这样的功能其实并不存在。实现接近这种功能的方法之一是在javascript执行过程中改变img
元素的src
属性,但超前的预解析器(或预加载)扼杀了它的可能性。
克服这个问题的第一步是创建一个基于标记的解决方案,该方案允许基于设备的功能来替换图像的来源。随着W3C响应式图片交流社区创造的picture
元素(尽管目前还没有浏览器实现了它)的引入,这个问题已经被解决了。
不过,picture
元素的引入也带来了新问题:
开发人员现在必须在每一个断点为每一个图片生成一个独立的asset。而开发者真正需要的是一个能够将一张高分辨率图片自动转化为适配设备的小图片的方案。理想情况下,这个自动化解决方案能够让每张图片只请求一次,并且充分语义化和向后兼容。 Mobify.js的图像API提供了该方案。
<picture>
元素成为未来的最佳实践
picture
元素是当前代替img
元素的先行者,因为它使得开发者能够为不同的屏幕分辨率指定不同的图片,解决了性能和art direction(尽管新提出的srcN提议值得考虑)问题。典型的设置包括定义断点,为不同断点生成图像,然后为图像写入picture
标记。我们看看如何通使下面的图片变得响应性:
我们使用了320、512、1024和2048像素基线。
首先,我们需要为每张图片生成不同的分辨率的副本,可以通过命令行工具,例如 Image Optim或使用Photoshop的“存储为web所用格式”功能。接下来,我们使用下面的标记:
<picture>
<source src="responsive-obama-320.png">
<source src="responsive-obama-512.png" media="(min-width: 512px)">
<source src="responsive-obama-1024.png" media="(min-width: 1024px)">
<source src="responsive-obama-2048.png" media="(min-width: 2048px)">
<noscript><img src="responsive-obama-320.png"></noscript>
</picture>
上面的代码有个问题,在它的配置中,我们的图片可能不会对移动设备优化。下面是一张缩放到320像素宽的图片:
图片中的人物已经很难区分。为了更好的适应小屏幕,我们需要借助art direction的力量,
为小屏幕裁切这张图片。
因为这个文件不是原始图片的简单缩放版本,需要给它一个特殊的命名结构(所以,用responsive-obama-mobile.png
替代了responsive-obama-320.png
)
<picture>
<source src="responsive-obama-mobile.png">
<source src="responsive-obama-512.png" media="(min-width: 512px)">
<source src="responsive-obama-1024.png" media="(min-width: 1024px)">
<source src="responsive-obama-2048.png" media="(min-width: 2048px)">
<noscript><img src="responsive-obama-512.png"></noscript>
</picture>
但是如果我们要支持高DPI(点每英寸)设备呢?picture
元素的规范中有一个srcset
属性,该属性能让我们很容易为不同分辨率指定不同的图片。以下是我们使用picture
元素后的代码:
<picture>
<source srcset="responsive-obama-mobile.png 1x, responsive-obama-mobile-2x.png 2x">
<source srcset="responsive-obama-512.png 1x, responsive-obama-1024.png 2x" media="(min-width: 512px)">
<source srcset="responsive-obama-1024.png 1x, responsive-obama-1024.png 2x" media="(min-width: 1024px)">
<source srcset="responsive-obama-2048.png 1x, responsive-obama-4096.png 2x" media="(min-width: 2048px)">
<noscript><img src="responsive-obama-512.png"></noscript>
这里引入了一对新文件(esponsive-obama-mobile-2x.png
和 responsive-obama-4096.png
),它们也必须生成。到目前为止,我们拥有同一个图片的6个不同版本的副本。
让我们更进一步。如果我们想使用现代化的格式,例如WebP
来读取我们的图像,需要通过判断浏览器是否支持吗?突然,我们需要生成的文件数量从6个增加到12个。老实说,没人愿意因为不同的分辨率而给每个图片生成多个版本的图片副本,并且需要在代码标记上更新这些图像版本。
我们需要让它自动化!
理想的响应式图片工作流程
理想的工作流程是这样的,它允许开发者上传分辨率尽可能高的图片同时仍然使用img
元素来达到自动为不同的浏览器重置图片大小和压缩图片的目的。img
元素很伟大,这个简单的标签解决了简单的问题:为互联网的用户展示图片。理想情况下,我们能通过一种高效的方法继续沿用这个元素并且向后兼容。然后,当art direction arises 和衡量图片下限的需求不能满足时,我们可以使用picture
元素,它语法中内建的分支逻辑很完美。
这个理想的工作流可以通过Mobify.js的响应式图像API来实现。Mobify.js是一款改善响应式网站的开源库,提供了响应式图片、javascript和css的优化,自适应模板等等。它的图像API能够自动重置大小和压缩img
和picture
元素,必要情况下,在后端甚至不需要改变任何一行标记代码。简单的上传高分辨率资料然后让API去关心接下来的事情。
不修改后端代码自动让图片响应化
响应式图片之所以成为一个难以解决的问题是由于超前的预解析器,它阻止我们通过javascript改变img元素的src属性。预解析器是浏览器的一个功能,通过生成一个独立的线程(独立于渲染线程之外),定位资源并使其并行下载,从而让资源尽快的开始下载。预解析器的工作在响应式设计出现之前很有意义,但在现在的多设备的情况下,代码标记的图像不一定是我们希望用户看到的;因此,我们需要思考一种允许开发者控制资源加载的同时不牺牲预加载的好处的API。在这个问题上,如果想要了解更多细节,可以考虑看看teve Souders的“I <3 Image Bytes.”
很多开发者为了避免预加载采用的一种方法是通过img的data-src
属性手动改变src
属性,巧妙地让预加载器忽略了这些图像,然后使用javascript将src
属性的值用data-src
的替换。
通过Mobify.js 的 捕获 API,我们避免了上面的方法,让我们在保证性能的情况下又不失语义(无需 <noscript>
或 data-src
)。捕获技术阻止了初始页面资源的预加载,但这并不阻碍并行下载。使用Mobify.js的图像API和捕获技术相结合,我们能通过一个javascript标签实现图像的自动响应化。
下面是调用API的的代码:
Mobify.Capture.init(function(capture){
var capturedDoc = capture.capturedDoc;
var images = capturedDoc.querySelectorAll('img, picture');
Mobify.ResizeImages.resize(images, capturedDoc)
capture.renderCapturedDoc();
});
它获取了页面的所有图片,然后重写的src的值如下:
http://ir0.mobify.com/<format><quality>/<maximum width>/<maximum height>/<url>
例如,如果该API在安卓最新版的chrome下运行,设备的css像素为320,像素比例为2,那么图片就会从
<img src='cdn.mobify.com/mobifyjs/examples/assets/images/forest.jpg'>
变为
<img src='//ir0.mobify.com/webp/640/http://cdn.mobify.com/mobifyjs/examples/assets/images/forest.jpg'>
forest图片会被调整到640px宽,并且,因为chrome支持WebP,我们可以因此受益,未来可以降低图片的体积。在第一次请求之后,图片就会被Mobify的CDN缓存,以备下次使用。因为这个图片不需要任何art direction,我们可以继续使用img
元素。
你可以看看这个自动调整图片大小的例子。打开网站调试工具,确定原始图片没有被下载!
使用这个方案,我们简化了工作流程。我们仅仅上传了一个高分辨率的图片,然后让API实现图片的自动化调整大小。中间不需要代理过程,没有改变任何属性-只是一断javascript。去吧,试着复制下面的代码粘贴在head
元素里(请注意它必须写在其他资源标签之前)。
<script>!function(a,b,c,d,e){function g(a,c,d,e){var f=b.getElementsByTagName("script")[0];a.src=e,a.id=c,a.setAttribute("class",d),f.parentNode.insertBefore(a,f)}a.Mobify={points:[+new Date]};var f=/((; )|#|&|^)mobify=(\d)/.exec(location.hash+"; "+b.cookie);if(f&&f[3]){if(!+f[3])return}else if(!c())return;b.write('<plaintext style="display:none">'),setTimeout(function(){var c=a.Mobify=a.Mobify||{};c.capturing=!0;var f=b.createElement("script"),h="mobify",i=function(){var c=new Date;c.setTime(c.getTime()+3e5),b.cookie="mobify=0; expires="+c.toGMTString()+"; path=/",a.location=a.location.href};f.onload=function(){if(e)if("string"==typeof e){var c=b.createElement("script");c.onerror=i,g(c,"main-executable",h,mainUrl)}else a.Mobify.mainExecutable=e.toString(),e()},f.onerror=i,g(f,"mobify-js",h,d)})}(window,document,function(){var a=/webkit|msie\s10|(firefox)[\/\s](\d+)|(opera)[\s\S]*version[\/\s](\d+)|3ds/i.exec(navigator.userAgent);return a?a[1]&&+a[2]<4?!1:a[3]&&+a[4]<11?!1:!0:!1},
// path to mobify.js
"//cdn.mobify.com/mobifyjs/build/mobify-2.0.1.min.js",
// calls to APIs go here
function() {
var capturing = window.Mobify && window.Mobify.capturing || false;
if (capturing) {
Mobify.Capture.init(function(capture){
var capturedDoc = capture.capturedDoc;
var images = capturedDoc.querySelectorAll("img, picture");
Mobify.ResizeImages.resize(images);
// Render source DOM to document
capture.renderCapturedDoc();
});
}
});
</script>
(请注意这段脚本不存在单点故障。如果Mobify.js载入失败,那么脚本会退出,你的网站依然会正常载入。如果图片调整服务器挂了或者你处于一个开发环境中并且图片在外网不可访问,那么就会载入原始的图片。)
你也可以利用完整的文档。支持上面代码的浏览器如下:所有的基于Webkit/Blink内核的浏览器,火狐4+,Opera 11+, and Internet Explorer 10+。
对于我们大多数的用例来说,自动化调整img
元素实在是太棒了。但是,就像演示的奥巴马的例子,art direction对于某些特定类型的图片是必要的。 那么我们如何才能继续使用picture
元素来支持art direction而不是添加同个图片的6个版本呢?图像API同样会调整picture
元素,也就是说你可以使用picture元素实现art direction,而把调整大小的功能交给API。
调整 <picture>
元素
当然不同的浏览器自动化调整图片大小是可行的,而自动化的art direction确实不可能。picture元素是最可能的方案来实现在不同的断点指定不同的图片,因为它内置的分支逻辑定义语法很健壮(尽管前文也提到了, srcN也提供了非常接近的功能)。不过,为picture元素添加标记和为每个图片创建6个版本让人感觉十分复杂:
<picture>
<source srcset="responsive-obama-mobile.png 1x, responsive-obama-mobile-2x.png 2x">
<source srcset="responsive-obama-512.png 1x, responsive-obama-1024.png 2x" media="(min-width: 512px)">
<source srcset="responsive-obama-1024.png 1x, responsive-obama-1024.png 2x" media="(min-width: 1024px)">
<source srcset="responsive-obama-2048.png 1x, responsive-obama-4096.png 2x" media="(min-width: 2048px)">
<noscript><img src="responsive-obama-512.png"></noscript>
</picture>
当使用图像API和picture元素相结合, 代码标记明显简化了:
<picture>
<source src="responsive-obama-mobile.png">
<source src="responsive-obama.png" media="(min-width: 512px)">
<img src="responsive-obama.png">
</picture>
这里的source
元素会自动重写img元素(像前面的例子一样)。同时,注意上面的标记不需要使用noscript来阻止第二次请求,因为捕捉(Capturing)允许你保留标记的语义。
Mobify.js同样能用于已变更的picture元素,便于显式的定义不同的断点需要多宽的图片,而不再一览与设备的宽度。 举个例子, 你有一张平板电脑一半宽的图片,那么根据浏览器的最大宽度指定该图片的宽度所生成的图片,它会比实际需要的尺寸大一些:
为了解决这个问题,图像API可以改变picture
标记,使得我们可以重新设定每个source
元素的宽度,而不是为每个断定指定不同的src
属性。例如,我们可以写成下面这样:
<picture data-src="responsive-obama.png">
<source src="responsive-obama-mobile.png">
<source media="(min-width: 512px)">
<source media="(min-width: 1024px)" data-width="512">
<source media="(min-width: 2048px)" data-width="1024">
<img src="responsive-obama.png">
</picture>
注意那个使用了data-src属性的picture元素,这里定义了一个高分辨率的原始图片作为一个起点,这个图片会被用于其他断点的尺寸调整。
它在浏览器中是怎么工作的
如果浏览器宽度介于0到511像素(例如一部智能手机), 那么使用responsive-obama-mobile.png (出于对art
direction的考虑)。如果浏览器宽度介于512到1023像素,那么使用responsive-obama.png,因为该媒体查询下source没有指定src。自动的决定宽度因为
data-width没有指定。如果浏览器宽度介于1024到2047像素,那么使用responsive-obama.png,
因为该媒体查询下source没有指定src。调整宽度为512像素,它被定义在data-width属性中。如果浏览器宽度为2048像素或更大,那么使用responsive-obama.png,
因为该媒体查询下source没有指定src。调整宽度为1024像素,它被定义在data-width属性中。如果不支持javascript,恢复为旧的img标签。
图像API会遍历所有的picture元素,并将它转变如下:
<picture data-src="http://cdn.mobify.com/mobifyjs/examples/assets/images/responsive-obama-mobile.jpg">
<source src="//ir0.mobify.com/webp/400/http://cdn.mobify.com/mobifyjs/examples/assets/images/responsive-obama-mobile.jpg">
<source src="//ir0.mobify.com/webp/400/http://cdn.mobify.com/mobifyjs/examples/assets/images/responsive-obama.jpg" media="(min-width: 512px)">
<source src="//ir0.mobify.com/webp/512/http://cdn.mobify.com/mobifyjs/examples/assets/images/responsive-obama.jpg" media="(min-width: 1024px)" data-width="512">
<source src="//ir0.mobify.com/webp/512/http://cdn.mobify.com/mobifyjs/examples/assets/images/responsive-obama.jpg" media="(min-width: 2048px)" data-width="1024">
<img src="responsive-obama.jpg">
</picture>
Picture polyfill (包含在Mobify.js里)会被执行然后根据媒体查询选择适当的图片。当浏览器厂商支持了原生picture后,它也能运行良好。
这里有一个使用已变更picture元素标记( the modified picture element ) 的例子,你可以看看。
不通过捕获的方式使用图像API
使用捕获需要将脚本写在head
标签里,这会阻塞javascript的调用并且会推迟资源的初始化下载。该延迟的时间长度在3G链接下大概为0.5秒(例如dns解析和资源的捕获和下载),在4G或wifi下耗时会相对少些,大约有60毫秒的延迟。这个延迟的小代价换到的是易于使用,和向下兼容以及语义化。
不通过捕获的方式使用图像API来避免阻塞javascript请求,你需要将所有img元素src属性改为x-src属性(你也可以适当的添加noscript编辑来检测浏览器javascript的支持情况)并且在head标签内粘贴下面的异步脚本:
<script src="//cdn.mobify.com/mobifyjs/build/mobify-2.0.1.min.js">
<script>
var intervalId = setInterval(function(){
if (window.Mobify) {
var images = document.querySelectorAll('img[x-src], picture');
if (images.length > 0) {
Mobify.ResizeImages.resize(images);
}
// When the document has finished loading, stop checking for new images
if (Mobify.Utils.domIsReady()) {
clearInterval(intervalId)
}
}
}, 100);
</script>
这段脚本会异步载入Mobify.js ,当载入完成后,开始正常载入图片就像载入html文档一样。
在WebApp中使用图像API
如果你正在使用一个客户端javascript MVC框架,例如Backbone 或 AngularJS,你仍然可以使用Mobify.js的图像API。首先,将Mobify.js库包含到你的app中:
<script src="//cdn.mobify.com/mobifyjs/build/mobify-2.0.1.min.js"></script>
然后,使用Mobify.js文档中列出的方法重写图片的URL。
Mobify.ResizeImages.getImageUrl(url)
该方法接受一个绝对URL然后返回图像调整后的URL。最简单的方式将图片传入这个方法是通过创建 模板辅助函数(template helper) (例如, {{image_resize '/obama.png' }} 在 Handlebars.js)执行了getImageUrl 方法来自动生成图像的URL。
在后端使用你的图像尺寸调整(Image-Resizing)
图像已经被Mobify's Performance Suite 尺寸调整服务器 调整了,它支持自动化调整WebP,CDN缓存等等。不过每个月免费调整的数量有一个默认的限制,不过别担心,如果你对流量的需求比较大,那么大声喊Mobify 一声,我们会尽可能的给予帮助。该API也允许你使用不同的图像调整服务,例如Sencha.io Src,或是你自己的后端服务。
浏览器厂商怎么样能更好的支持相应式图像?
Webkit团队最近已经实现了src-set
属性,那么Blink和Gecko也将在不久实现。这是正确道路上前进的一大步,这意味着浏览器厂商已经严重重视了相应式图像的问题。然而,它不能解决 art direction问题,也不能避免生成多个分辨率版本文件的步骤。
开发社区最近也一起讨论了响应式图片的问题。其中有一个有趣的提案是Ilya Grigorik提出的客户端提醒,该提议包括在每个请求头部发送设备属性例如DPR和高度。我喜欢这个方案,因为它能让我们继续使用img
标签,然后在我们需要创建分支逻辑实现art direction
只需要使用picture
(或srcN
)即可。尽管添加额外的HTTP头和使用内容协商来解决这个问题已经被证明有效,对与拥有成千上万图像的网站来说,使用该方案来定位图像恐怕不太可行。通过服务层或代理重写图像,可以解决这个问题,不过这两张方式都可能被设置的有问题。在我看来,如果在客户端能更好的控制资源加载,那这些个问题我们是能够处理解决的。
如果开发者更好的控制了资源的加载,那么响应式图片应该会被当作一个简单的问题来处理。之所以如此多的响应图像解决方案是基于代理实习,是因为在文档到达浏览器之前,图像必须被重写。这是对预加载器尝试尽快下载图片行为的适应。不过代理的方案可能在安全性和扩展性方面会有很多问题,如果我们有一个简单的方式来和预加载器交互,那么很多基于代理的方案就显得冗余了。
那么如何在更好的控制资源加载的同时,仍然享受预加载器带来的好处呢?这里关键点是我们不希望简单的关闭预加载器-它并行下载的能力是一个巨大的胜利,并且已经被浏览器实现了。(请注意捕获API不会阻塞并行下载。)其中一个方法是提供一个beforeload
事件,该事件在资源载入之前触发。该事件在safari浏览器中通过一个扩展就可以实现了,在某些浏览器中也可以使用,不过容量有限。如果现在能够使用该方案的话,那么就不再需要捕获了。下面是一个使用beforeload
事件的一个基础例子:
function rewriteImgs(event) {
if (event.target === "IMG") {
var img = event.target;
img.src = "//ir0.mobify.com/" + screen.width + "/" + img.src;
}
}
document.addEventListener("beforeload", rewriteImgs, true);
关键的挑战在于以某种方式在执行构图循环时候(in the main rendering loop)让预加载器和javascript交互良好。有一个当前正在浏览器中开发的一个新系统,叫做 Service Worker,
它的目的是允许开发者截获网络请求来协助构建离线web应用。然而,当前的实现不允许拦截初始请求。这是因为载入一个额外的脚本会阻塞其他资源的载入-不过我相信这个情况会改变,例如通过不牺牲性能的内联脚本的方式。如果你有如下的需求,可以考虑使用 Mobify.js 来自动处理响应式图片:
总结
响应式图片方面的问题有许多的解决方案,工作自动化的同时仍然可以art direction的方案驱动未来的Web开发的解决方案。
- 只需要为每个资源提供一份高版本的图片,让API实现提供基于设备条件(宽度,WebP支持情况等等)的小图片。
- 每个图片之发生一次请求。
- 需要100%的语义和向下兼容旧标记并且不需要改变后端(使用捕获的情况)。
- 已经开始使用picture元素来自动调整大小,只专注与使用它来实现art direction。
原文 Automate Your Responsive Images With Mobify.js
作者 Shawn Jansepar
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。