前端开发者学堂 - fedev.cn

响应式图片使用指南(Part3)

发布于 大漠

这是有关于响应式图片使用的第三部分,在第一部分中主要和大家一起聊了些和响应式布局相关的概念和术语;第二部分主要和大家聊的是<img>新增属性srcsetsizes如何让我们根据用户代理环境加载不同尺寸的图片。而今天将和大家一起探讨 HTML5中的<picture>元素又是如何帮助我们根据用户代理的环境加载不同的图片。如果你对这方面感兴趣的话,请继续往下阅读。

使用 <picture>

我们已经知道了,使用 <img src="" srcset="" sizes="" alt=""> 是用来提供同一图片的不同大小的版本,这个不同尺寸指的是图片宽高。比如:

<!-- HTML -->

<!-- 根据设备DPR提供不同尺寸的图片 -->
<img 
    src="srcset-x1.png" 
    srcset=" 
        srcset-x1.png 1x, 
        srcset-x2.png 2x, 
        srcset-x3.png 3x" 
    alt="srcset for img" 
/>

<!-- 根据视窗大小提供不同尺寸的图片 -->
<img 
    alt="A baby smiling with a yellow headband." 
    src="/baby-640w.jpg" 
    srcset=" 
        /baby-320w.jpg 320w, 
        /baby-640w.jpg 640w, 
        /baby-960w.jpg 960w, 
        /baby-1280w.jpg 1280w, 
        /baby-1920w.jpg 1920w, 
        /baby-2560w.jpg 2560w" 
    sizes=" 
        (max-width: 500px) calc(100vw - 2rem), 
        (max-width: 700px) calc(100vw - 6rem), 
        calc(100vw - 9rem - 200px) " 
/>

不管是根据设备DPR还是视窗大小选择不同尺寸的图片,他们都有着一个共性:图片是同一张,只是尺寸大小不同,最终呈现给用户的效果只是图片的缩放

除此之外,HTML 中的 <picture> 元素 也可以做到这一点。但这里的区别是,浏览器遵循你的设置。当你想改变加载的图片的分辨率适应用户的情况时,这很有用。这种有意改变图片的做法通常称为 艺术指导(Art Direction)!

艺术指导

艺术指导 是指:

当你想为不同布局提供不同剪裁的图片。即更改显示的图片以适应不同的图片是示尺寸。

这个时候使用 <picture> 要比使用 <img>srcset + sizes好得多。来看一个简单地示例:

<picture>
    <source media="(max-width: 799px)" srcset="https://static.fedev.cn/sites/default/files/blogs/2021/2112/elva-480w-close-portrait.jpeg">
    <source media="(min-width: 800px)" srcset="https://static.fedev.cn/sites/default/files/blogs/2021/2112/elva-800w.jpeg">
    <img src="https://static.fedev.cn/sites/default/files/blogs/2021/2112/elva-800w.jpeg" alt="Chris standing up holding his daughter Elva">
</picture>

效果如下:

调整浏览器视窗大小,看到的效果如下:

呈现给用户的图片会有两个阶段:

  • 在大屏幕上,显示一张大的图片
  • 在小屏幕上,显示一张小的图片(基于大图片裁剪过的)

浏览器会根据CSS媒体查询设置的断点切换图片。它和<img>最大不同之处就是可以在不同的视窗大小(断点)或像素比定义不同图片资源,其明显的好处是,适当尺寸的图片资源被提供,这最终可以节省大量的带宽,而且图片呈现效果比直接缩放效果要更佳。

特别声明,在 Web 开发中,<picture> 也常称艺术指导(Art Direction)。为此,接下来的内容中的“艺术指导”指的就是在 HTML 中使用 <picture> 来引入图片的方式!

艺术指导可以做很多事情,而不仅仅是剪裁

虽然像这样裁剪和缩放图片是迄今为止最常见的艺术指导形式,但它起的作用绝不仅此而以,它还可以帮助你做更多的事情。

如果你阅读过小站上关于暗黑模式相关的文章,不知道是否还记得,为了能给用户一个更好的体验,希望让Web上的图片在Light和Dark两种模式下略有不同。大多数,开发者喜欢使用 CSS 滤镜媒体查询来处理图片:

@media (prefers-color-scheme: dark) { 
    img:not([src*=".svg"]) { 
        filter: brightness(.8) contrast(1.2); 
    } 
}

不过,我们使用<picture>给用户提供的体验会更佳:

media属性中prefers-color-scheme的条件值为 dark 时引入 dark.png 图片,在prefers-color-schemelight 时引入 light.png 图片,并且在不支持 prefers-color-scheme 设备上使用 light.png 图片:

<picture>
    <source srcset="https://static.fedev.cn/sites/default/files/blogs/2021/2112/dark.png" media="(prefers-color-scheme: dark)">
    <source srcset="https://static.fedev.cn/sites/default/files/blogs/2021/2112/light.png" media="(prefers-color-scheme: light)">
    <img src="https://static.fedev.cn/sites/default/files/blogs/2021/2112/light.png" alt="" />
</picture>

你可以直接在浏览器的开发者工具上调整prefers-color-scheme的值来模拟设备的暗黑模式的切换,将能看到不同模式下加载的不同的资源的图片:

有关于 prefers-color-scheme 更多的介绍,可以阅读:

“艺术指导”还有利于 Web 可访问性(A11Y)的提高,比如说动效的禁用,有些用户会开启动效的偏好设置:

在 CSS 中使用媒体查询来查询 prefers-reduced-motion 条件值是否为 reduce,如果匹配,动效不播放:

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

虽然说现在Web上的动效大多可以使用 CSS 或 JavaScript 来完成,但有的时候还是会使用 .apng.gif 格式的动画图。如果希望在用户开启了禁用动效的设置,不使用.apng.gif,而使用一张静态图,那么“艺术指导”就非常有用了:

<picture>
    <source srcset="https://static.fedev.cn/sites/default/files/blogs/2021/2112/animation.jpg" media="(prefers-reduced-motion: reduce)">
    </source>
    <img srcset="https://static.fedev.cn/sites/default/files/blogs/2021/2112/animation.gif" alt="" />
</picture>

<!-- 或者 -->

<picture>
    <source srcset="https://static.fedev.cn/sites/default/files/blogs/2021/2112/animation.gif" media="(prefers-reduced-motion: no-preference)">
    </source>
    <img srcset="https://static.fedev.cn/sites/default/files/blogs/2021/2112/animation.jpg" alt="" />
</picture>

同样可以通过浏览器来模拟 prefers-reduced-motion 开启和关闭的效果:

为什么要使用 prefers-reduced-motion: reduce 来减弱或禁用动效呢?主要原因:

  • 有些用户面对闪烁的动画可能会诱发癫痫病发作
  • 有些用户会因为动效产生类似晕车的反应

如果你对这方面感兴趣,还可以阅读下面这几篇文章:

有些用户为了节省数据流量会开启另一个偏好设置:

我们可以使用媒体查询的 prefers-reduced-data 来判断,当其值为 reduce 时加载一些体积小的或新的格式的图片:

.image {
    background-image: url(image/heavy.jpg)
}

@media (prefers-reduced-data: reduce) {
    .image {
        background-image: url(image/light.jpg)
    }
}

/* 或者 */
.image {
    background-image: url(image/light.jpg)
}

@media (prefers-reduced-data: no-preference) {
    .image {
        background-image: url(image/heavy.jpg)
    }
}

但这种方式仅适合于背景图的使用。不过,和前面两个场景类似,艺术指导也可以使用媒体查的prefers-reduced-data为开启节约数据设置的用户提供一个较小的图片:

<picture>
    <source srcset="light.jpg" media="(prefers-reduced-data: reduce)" />
    <img src="heavy.jpg" alt="" srcset="heavy@2x.jpg 2x" />
</picture>

<!-- 或 -->
<picture>
    <source
        srcset="/light.jpg 200w,
                /heavy.jpg 400w"
        sizes="(prefers-reduced-data: reduce) 200px,
            (min-width: 400px) 400px,
            200px" />
    <img src="heavy.jpg" alt="" />
</picture>

我曾在《图片的优化》一文提到过,使用艺术指导还可以提供不同格式的图片类型。换句话说,<picture> 同时具备提升 LCP 速度。比如:

<picture>
    <source srcset="photo.jxl" type="image/jxl">
    <source srcset="photo.avif" type="image/avif">
    <source srcset="photo.webp" type="image/webp">
    <source srcset="photo.jpg" type="image/jpeg">
    <img src="photo.jpg" alt="" />
</picture>

在这个示例中,浏览器将开始解析源,当它找到第一个支持的匹配时就会停止。如果没有找到匹配的源,浏览器会加载 <img> 中指定的源作为备用源。这种方法可以很好地服务于并非所有浏览都支持的现代图片格式。千万要注意 <source> 元素的顺序,因为这个顺序非常重要。不要把现代图片格式的源放在传统格式之后,要把它们放在前面。简单地说,越先进,越还未得众多浏览器支持的图片格式越要放在最前面;越传统的越应该放在末位。得到浏览器支持的,浏览器就会选这个,未得到浏览器支持的,浏览器会自动忽略,并且开始往下寻找匹配的图片源。

使用浏览器开发者工具检测,你可以看到效果如下:

当你禁用 "AVIF" 格式时,浏览器会加载 WebP格式图片,当你把 "AVIF" 和 "WebP" 格式都禁用时,浏览器会加载“JPG”格式图片。

这里只是列出了 <picture> 常见的使用场景,并且能给用户带来实际收益的。当然,可能还有其他更有意思的地方我没有想到,如果你有更好的建议或想法,欢迎一起分享和探讨。接一来,我们来聊聊 <picture> 的使用。

source 和 img

<picture> 最大的特色是同时包含 <source><img>。其中<source>元素允许开发者为<img>元素指定多个备选源集。它本身并不代表任何东西。比如:

<picture>
    <source srcset="small-200.jpg 200w"  sizes="(max-width: 639px) 100vw">
    <source srcset="medium-650.jpg 650w" sizes="(min-width: 640px) and (max-width: 1023px) 50vw">
    <source srcset="large-850.jpg 850w"  sizes="(min-width: 1024px) 25vw">
    <img src="my-image.jpg" alt="My image">
</picture>

<picture> 中同时可以出现多个 <source> 元素来指定图片源,并且会从第一个开始索引。如果第二个符合条件,将会采用第一个引入的图片源;如果第一个不符合条件,会根据第二个<source>,依此类推,直到找到符合条件的图片源。如果所有<source>的图片源都不符合,则会采用<img> 备用,最终呈现给用户的是<img>src引入的图片源。

不过,我们使用<picture>时,用户实际上看到的是<img>元素。如果<picture>中没有<img>,用户就看不到图像,因为<picture>和其所有的<source>子元素只是为了给<img>提供一个来源。它的来源(<source>)只是让浏览器知道从哪里提取图片的来源,用来替代<img>中的src引入的图片源。这也意味着,任何想应用于渲染图片的样式(比如,max-width: 100%)都需要应用于<img>元素上,而不是<picture>上:

/* 不会被运用到图片上 */
picture {
    max-width: 100%;
    height: auto;
}

/* 会运用到图片上 */
img {
    max-width: 100%;
    height: auto;
}

上面说的只是 <picture><source><img> 元素最基本的使用方试。事实上,在<source><img> 上都可以像前面介绍的 <img> 一样,使用srcsetsizes指定图片尺寸和相关条件(如上面代码所示)。

需要注意的是:<picture>中的<source>元素必须有一个srcset属性,并且只能有一个。如果srcset属性值中有描述宽度的描述符,比如w,那么sizes属性也必须存在,反之,sizes属性可以不存在。比如下面这个示例:

<!-- srcset没有带任何宽度描述符,sizes属性可以缺省 -->
<picture>
    <source srcset="light.jpg" media="(prefers-reduced-data: reduce)" />
    <img src="heavy.jpg" alt="" srcset="heavy@2x.jpg 2x" />
</picture>

<!-- srcset带有宽度描述符,比如200w,那么sizes属性必不可少 -->
<picture>
    <source srcset="small-200.jpg 200w"  sizes="(max-width: 639px) 100vw">
    <source srcset="medium-650.jpg 650w" sizes="(min-width: 640px) and (max-width: 1023px) 50vw">
    <source srcset="large-850.jpg 850w"  sizes="(min-width: 1024px) 25vw">
    <img src="my-image.jpg" alt="My image">
</picture>

<source>还可以和mediatype一起使用:

  • 如果 media 属性存在,该值必须包含一个有效的媒体查询列表,比如 (prefers-reduced-data: reduce)。如果该值与环境不匹配,用户代理将跳到一下个<source>元素
  • 如果 type 属性存在,该值将会指定<source> 图片源的类型,比如type="image/avif",表示<source>指定了引入图片的格式是AVIF,其主要用来让用户代理在不支持给定类型时跳到下一个<source>元素。如果没有显式指定type属性,用户代理将在获取图片格式后再做判断,要是发现自己(浏览器)不支持该类型图片,将不会选择,同时会跳到下一个<source>

<source><img>不同的是,它没有src属性,它是由srcset引入图片源的。

source 和 srcset + sizes的相结合

前面我们深度探讨了<img> 中的 srcsetsizes 属性。同样的,<picture><source>标签上的 srcsetsizes 属性的使用和 <img> 的一样,不同的是:

<source>没有src属性,而且只要 srcset 属性带有w这样的宽度描述符,sizes属性就不可或缺,否则可以省略

来看一个简单地示例:

<picture>
    <source 
        media="(min-width: 36em)"
        srcset="
            large.jpg  1024w,
            medium.jpg 640w,
            small.jpg  320w"
        sizes="33.3vw" 
    />
    <source 
        srcset="cropped-large.jpg 2x,
        cropped-small.jpg 1x" 
    />
    <img src="small.jpg" alt="A rad wolf" />
</picture>

示例中的<picture>元素包含了两个<source>和一个<img>。其中 <source> 代表着两个独立的艺术指导(图片源)版本,<img>是备用图片源,只有<source>都不符合条件才会被采用。你可能发现了,<source>上有 srcsetsizes属性,正因为这两个属性的存在,<picture>在幕后做了很多事情。

首先看第一个<source>

<source 
    media="(min-width: 36em)"
    srcset="
        large.jpg  1024w,
        medium.jpg 640w,
        small.jpg  320w"
    sizes="33.3vw" 
/>

这个<source>引入的图片源代表了图片的完整的未裁剪的版本。只有当视窗宽度大于36em时,<source>引入的图片源才会被采用,否则就会跳到下一个<source>。在这个<source>中设置了srcsetsizes

  • srcset指定了不同的图片源路径以及对应的图片源宽度
  • sizes 指定图片要显示的尺寸,即 33.3vw

注意,这里因为在<source>使用media属性指定了一个媒体判断条件(min-width: 36em),其实我们也可以像在<img>中的sizes一样使用判断条件,比如:

<source 
    srcset=" 
        /baby-320w.jpg 320w, 
        /baby-640w.jpg 640w, 
        /baby-960w.jpg 960w, 
        /baby-1280w.jpg 1280w, 
        /baby-1920w.jpg 1920w, 
        /baby-2560w.jpg 2560w" 
    sizes=" 
        (max-width: 500px) calc(100vw - 2rem), 
        (max-width: 700px) calc(100vw - 6rem), 
        calc(100vw - 9rem - 200px) " 
>

有关于<source>中的media属性,我们稍后再聊。

回到上面这个示例中,如果第一个<source>不符合条件,用户代理(比如浏览器)就会跳到第二个<source>

<source 
    srcset="
        square-large.jpg 2x,
        square-small.jpg 1x" 
/>

这个<source>中的srcset没有宽度的描述符w,因此,在代码中并没有sizes属性的设置。这个<source>很简单,表示引入的图片源将根据用户设备的 DPR来做图片源的选择,在dpr=2的时候会选择square-large.jpg,在dpr=1的时候会选择square-small.jpg

特别声明,你只要理解了<img> 中的 srcsetsizes属性,就能掌握<source>上的srcsetsizes属性的使用。这里就不再重述!

source 中的 type 和 media 属性

相对于 <source>srcsetsizes 属性来说,它的typemedia要简单地多,而且它们也不像srcset是一个必须的属性。

先来看<source>type属性,它的作用就是用来指定图片来源的类型(图片的格式),这样允许用户代理在不支持给定在型的情况下跳到一个一个<source>。比中前面的示例:

<picture>
    <source srcset="photo.jxl" type="image/jxl">
    <source srcset="photo.avif" type="image/avif">
    <source srcset="photo.webp" type="image/webp">
    <source srcset="photo.jpg" type="image/jpeg">
    <img src="photo.jpg" alt="" />
</picture>

依次指定了图片格式为.jxl.avif.webpjpeg。假设浏览器支持.jxl就会使用photo.jxl图片,否岀会去跳到一下<source>,并且按同样的方式来做判断。比如Chrome浏览器,它支持AVIF格式的图片,所以浏览器会引用photo.avif图片。

如果<source>同有显式指定type属性,用户代理会在获取图片格式后再做判断,如果发现不支持该图片格式,将会跳到下一个<source>

media属性和type相似,不是<source>必备属性,他的使用和CSS的@media相似,只不过在这里是HTML标签元素的一个属性,正如前面示例所示,我们可以根据用户对设备的喜好来选择不同的图片源:

<!-- 根据 prefers-color-scheme 为不同模式选择不同图片 -->
<picture>
    <source srcset="dark.png" media="(prefers-color-scheme: dark)">
    <source srcset="light.png" media="(prefers-color-scheme: light)">
    <img src="light.png" alt="" />
</picture>

<!-- 根据 prefers-reduced-motion 为用户呈现动图或静态图 -->
<picture>
    <source srcset="animation.jpg" media="(prefers-reduced-motion: reduce)">
    </source>
    <img srcset="animation.gif" alt="" />
</picture>

<!-- 根据 prefers-reduced-data 为用户选择不同的图片 -->
<picture>
    <source srcset="light.jpg" media="(prefers-reduced-data: reduce)" />
    <img src="heavy.jpg" alt="" srcset="heavy@2x.jpg 2x" />
</picture>

注意,其他@media判断条件也可以像上面这样在<source>中使用,比如:

<picture>
    <source 
        media="(max-width: 639px)"
        srcset="small-200.jpg 200w" 
        sizes="100vw">
    <source 
        media="(min-width: 640px) and (max-width: 1023px)"
        srcset="medium-650.jpg 650w"
        sizes="50vw">
    <source 
        media="(min-width: 1024px)"
        srcset="large-850.jpg 850w"
        sizes=" 25vw">
    <img src="my-image.jpg" alt="My image">
</picture>

简单小结一下

<picture><source>中的srcsetxwsizesmediatype属性为<picture>的图片源(<source>)提供更多的选择,使<picture>真正具有响应式,能在灵活的布局和广泛的设备中更好的工作,更有效的工作。

虽然,<picture>已经非常强大了,但也并不意味着要使用 <picture> 来替代 <img>。如果说,你只希望让用户在不同的环境中加载不同尺寸的图片,那么<img>足够了(带有srcsetsizes<img>);如果你想实现艺术指导的效果,那<picture>将是首选方案。

不管是<img>还是<picture>聊的还是 HTML 中图片的使用。事实上,我们在 CSS 也会使用 background-image 来加载图片,那么在 CSS中是否也可以像 <picture><img>一样,根据用户环境来加载不同的图片源。

回答是肯定的,那怎么做呢?如果你感兴趣的话,请继关注后续的更新。