把我的个人网站推倒重来(Part 4 - Responsive Image)

Cat Chen 2018-10-08 11:55

网站整体完成后,我就可以开始做各种小优化了。其中一个优化是使用 responsive image 来适应不同分辨率和不同像素密度的屏幕,用到的是<img />新增的srcset和sizes属性以及新增的<picture />元素。因为现在有多套新旧并存的 responsive image 方案,而且它们使用的属性存在重叠,所以要搞清楚到底这些属性如何运作,还是要动手实验。

sizes属性

<img srcset="elva-fairy-320w.jpg 320w,             elva-fairy-480w.jpg 480w,             elva-fairy-800w.jpg 800w"     sizes="(max-width: 320px) 280px,            (max-width: 480px) 440px,            800px"     src="elva-fairy-800w.jpg" alt="Elva dressed as a fairy">

这是一段来自 MDN 的代码。虽然大家都是先写srcset再写sizes,但其实更符合直觉的阅读顺序是先读sizes。sizes的值是一组类似 media query 的命令,它们描述了在什么情况下这个<img />应该有多宽。拿上面这段代码举例:如果屏幕宽度是 320px 或以下,图片宽度为 280px;如果屏幕宽度是 480px 或以下(但 320px 以上),图片宽度为 440px;其它屏幕宽度,图片宽度默认为 800px。

这一切看起来都很简单,但这是因为我们只在讨论分辨率没在讨论像素密度。如果是 2 倍像素密度的 Retina Display,上述图片宽度计算是否保持不变?答案是跟 media query 一样保持不变。无论像素密度是多少,sizes关注的都是 CSS 像素而不是物理像素。我觉得这个设计是合理的,因为在描述<img />宽度时,我们的思维模式跟在写 CSS 时一样,所以应该使用 CSS 像素。

尽管在 MDN 的例子中sizes属性的取值都是固定值,但其实这里可以使用calc()表达式进行复杂的计算,如我的代码中就用到了calc(100vw - 30px),意思是100%的 viewport 宽度减去屏幕两侧各15px的margin。

srcset属性

看完sizes接着看srcset。在上面这段代码里,我们看到了一个神奇的单位叫做w,这是指代图片文件的像素宽度。文件图片的像素宽度跟<img />的 CSS 像素宽度不是 1:1 对应的吗?这需要看像素密度。如果<img />的宽度是100px,像素密度是 1 时最佳图片文件宽度是100w;像素密度是 2 时最佳图片文件宽度是200w;像素密度是 3 时最佳文件宽度是300w;如此类推。

在上面这段代码中,srcset描述了 3 个图片文件地址,它们的文件图片像素宽度分别是320w、480w、800w。这也就是说,如果在一个 1000px 宽 1 像素密度的屏幕上,根据sizes这个<img />的宽度应该是800px,因此应该选择800w的图片文件地址;如果在一个 480px 宽 2 像素密度的屏幕上,根据sizes这个<img />的宽度应该是440px,但因为像素密度为 2 所以最佳图片文件宽度是880w,由于找不到880w的图片文件地址所以选用略差一档的800w图片文件地址。

为什么sizes和srcset这两个属性要如此设计呢?因为在之前的标准(如 CSS media query)里,我们需要在代码中描述如何根据两个变量(屏幕宽度和像素密度)来选择正确的图片文件地址,这个过程超级复杂,看这篇文章就能理解为什么这样的标准不好用。为了解决这个问题,新的标准让我们把这两件事情分离开来:sizes决定图片在 CSS 布局中的大小,但跟像素密度无关;srcset提供文件图片像素宽度,浏览器会自行根据sizes的结果和像素密度作出最佳选择,我们根本不需要知道像素密度。

(如果<img />在布局中的大小永恒不变,可以不设置sizes属性,然后在srcset中使用x单位描述像素密度而不使用w单位。这时候2x、3x可以对应不同的图片文件地址,浏览器会作出正确的选择。之所以我不选择这样做,是因为我的<img />大小本身需要 responsive,所以必须用sizes。因为w和x这两种单位不能在同一个<img />上混合使用,所以我用了w。)

src属性

看完sizes和srcset这两个属性后,最后我们看src属性。这是给那些看不懂前两个属性的老浏览器看的,也就是默认的图片文件地址。

<picture />元素

上述<img />元素属性能够实现同一张图片适应不同的屏幕尺寸和像素密度,但做不到根据屏幕尺寸现实不同的图片。我的网站首页布局本身是 responsive 的:如果屏幕宽度至少有 768px,使用左右两栏布局;否则使用一栏布局。

Screenshot 2018-09-30 15.21.45

左侧栏只显示我的个人照片,所以在能够使用两栏布局时我希望显示正方形(1:1)的剪裁。同样的剪裁显示在只能显示一栏的手机屏幕上就会显得很占地方,因此我需要换个剪裁方式(3:2)减少它占用的垂直高度,把更多的首屏垂直高度留给文本信息。

Screenshot 2018-09-30 15.22.05

为了实现根据屏幕尺寸使用不同的剪裁,我必须引入<picture />元素然后在里面放入<source />和<img />。

<picture>  <source media="(max-width: 799px)" srcset="elva-480w-close-portrait.jpg">  <source media="(min-width: 800px)" srcset="elva-800w.jpg">  <img src="elva-800w.jpg" alt="Chris standing up holding his daughter Elva"></picture>

这也是一段来自 MDN 的代码。浏览器在碰到<picture />后,就开始按顺序看里面的<source />。每个<source />元素都有media属性,浏览器就如同执行 media query 一样来判断这个media属性的值是否通过,通过了就使用这个<source />来显示图片,后面的子元素都会被忽略掉。如果所有<source />都无法通过 media query 检测,最后那个<img />就会用来显示。(不兼容<picture />和<source />的老浏览器只会显示<img />。)

由于<source />支持上述<img />特有的sizes和srcset属性,所以就算是放在<source />中的图片也可以用上述方式支持不同的像素密度。考虑到大多数移动浏览器都会获得及时更新,能够支持<picture />和<source />,所以我选择了把正方形的剪裁当作默认剪裁放在<img />里面,而针对小屏幕的剪裁放在<source />里面。老旧的桌面浏览器如果打开我的网站首页,就算什么新元素和属性都不认识,至少能够根据img.src显示默认图片(像素密度为 1 的正方形剪裁)。

总结一下,如果需要针对不同的屏幕尺寸显示不同的图片(尤其是剪裁不一样的),必须使用<picture />配合<source />来选择正确的图片。一旦选定图片后,根据屏幕尺寸和像素密度设定图片尺寸只需要用到sizes和srcset属性。

我的图片优化不仅仅用到了首页的个人照片上,也用到了项目页面的截图上。在图片之后,我们还有很多其它东西可以优化的,欢迎通过邮件RSS/Atom订阅我的博客,继续关注后继文章。

[返回] [原文链接]