2018年10月7日星期日

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

网站整体完成后,我就可以开始做各种小优化了。其中一个优化是使用 responsive image 来适应不同分辨率和不同像素密度的屏幕,用到的是 <img /> 新增的 srcsetsizes 属性以及新增的 <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,但其实更符合直觉的阅读顺序是先读 sizessizes 的值是一组类似 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 宽度减去屏幕两侧各 15pxmargin

srcset 属性

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

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

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

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

src 属性

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

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

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