Skip to content
本页目录

Image 图片

HTMLimg 的优化,是项目中绕不过的问题,涉及到图片上传、图片裁剪、图片的懒加载、文章图片占位的计算、图片类型的支持与优化、移动端不同分辨率的图片选择、图标的合并、图片加载失败的处理等等。

1. 图片类型与区别

常用的图片类型与优缺点:

类型MIME 类型是否有损压缩压缩率优点缺点
jpegimage/jpeg-色彩丰富、支持渐变多次压缩会失真
pngimage/png-支持透明体积较大
gifimage/gif-支持动画与透明色彩支持少
webpimage/webp-约 70%压缩率高,支持透明与动画,支持有损与无损压缩兼容性差
svgimage/svg+xml--矢量不失真,一般用于图标写法不友好

2. 图片懒加载

需求:图片资源出现在视口区域内时再进行图片请求。
原理:监听图片资源容器是否出现在视口区域内来决定图片自营是否需要加载。

注:日常 Vue 项目中,更多使用 vue-lazyload 插件来实现。

2.1 通过监听滚动实现

监听 scroll 事件,通过 getBoundingClientRect API 获取元素图片距离视口顶部的距离,配合当前可视区域的位置实现图片的懒加载。
因为 scroll 事件是高频事件(它与 getBoundingClientRect 都会引起浏览器的回流),我们需要对其做节流处理。

2.2 使用 IntersectionObserver 接口实现

通过 HTML5IntersectionObserver API,配合监听元素的 isIntersecting 属性,判断元素是否在可视区内。该方案比监听 scroll 性能要好。

2.3 使用 loading=lazy 属性

这是一个标签的原生属性(H5 新增),实现图片进入可视区域时,再进行加载。

HTML
<img src="xxx.png" loading="lazy">
<img src="xxx.png" loading="lazy">

此方式还有个 decoding 属性,可以设置对图片异步解码 decoding=async,即先渲染页面内容,再解码图片,可以提升 html 的显示速度。

目前主流浏览器最新版本都已支持,后期肯定能取代 JS 的实现方案。

3. 图片加载失败处理

3.1 标签的绑定 onerror 方法

HTML
<img src="images/logo.png" onerror="this.src='images/errorLogo.png';">
<img src="images/logo.png" onerror="this.src='images/errorLogo.png';">

3.2 监听标签元素的 onerror 事件

这个实际就是上面的翻版,改为在 js 中进行监听

js
document.getElementById('#img').onerror = function(){  
    this.src = "images/errorLogo.png";  
}
document.getElementById('#img').onerror = function(){  
    this.src = "images/errorLogo.png";  
}

3.3 Vue 中使用指令实现

4. 图片占位

一些文章页面,在图片未加载时会显示适合图片大小的一个盒子空间,目前未找到相关解决方案的文章。
我目前的实现方法是,在文章中添加图片时,需要再 img 标签中添加上图片真实宽高的属性,待页面呈现时,读取该宽高,根据文章内容区域的实际宽度,来换算出图片展示的高度,以此来占住图片区域空间。例如:

HTML
<img src="xxx.jpg" data-raw-width="1000" data-raw-height="1200" />
<img src="xxx.jpg" data-raw-width="1000" data-raw-height="1200" />
JS
// 读取图片原图宽高,并根据内容区域宽度进行换算出需要显示的图片高度
// 读取图片原图宽高,并根据内容区域宽度进行换算出需要显示的图片高度

5. 图片合并

即常说的 雪碧图,原理是将多个小图标合并为一张大图,利用 background-position 等属性调整其位置。
现在项目中一般对小图标采用字体图标或者 svg 来实现,优点是支持颜色、矢量不失真,替换也较为方便。

7. 移动端不同分辨率图片选择

根据设备 dpr 设置对应倍率的图片。

7.1 背景图实现

通过媒体查询,对不同 dpr 设置对应的背景图;

7.2 标签图实现

img 标签提供了 srcset 属性用于指定对应 dpr 的图片地址,如下:

HTML
<img src="foo-640w.jpg" 
     srcset="foo-320w.jpg,
             foo-480w.jpg 1.5x,
             foo-640w.jpg 2x"
>
<img src="foo-640w.jpg" 
     srcset="foo-320w.jpg,
             foo-480w.jpg 1.5x,
             foo-640w.jpg 2x"
>

更多属性查看文档吧,数字 + x 的写法就是其对应的 dpr 显示对应的图片地址

8. 图片上传

主要是一些对象存储的文件直传的实现,每个服务器厂商提供的 sdk 各不相同,需要查看对应的文档进行开发。
大体流程是上传前,前端判断文件大小以及格式等信息,再通过后端提供的接口获取到文件上传的 token,携带上该 token 调用 sdk 提供的方法,监听上传状态,一般 api 会提供 progress 回调,可以利用这个回调显示当前上传进度等。

9. 图片裁剪

项目中最常用的是 cropperjs 插件以及它的对应框架的封装版。
原理:

  • 在图片上层做一个可拖拽大小的区域,用户拖拽后记录对应的左上角坐标,以及区域的宽高;
  • 将图片生成 canvas,使用以上的坐标与宽高对 canvas 进行裁剪;
  • 将裁剪后的 canvas 转为图片。

cropperjs 在移动端(比如在微信浏览器中)处理大图时(现在相机拍摄的图片尺寸都很大),最终 canvas 转为图片时要注意其尺寸,我之前项目中设置最大宽高不超过 2048,更大的尺寸很容易造成浏览器内存占用过高导致被杀掉,使得转换失败。

10. webp 的使用

目前 webp 格式在安卓端可以无障碍使用了,IOS 端还需要做判断,IOS14 以下不支持。
常用的判断是否支持 webp 格式的方法是:将1像素的一张 webp 格式图片转为 base64 ,然后使用 js 动态加载这张图,如果成功回调中能获取到这张图片的宽高值,说明支持 webp 格式,否则不支持。

JS
// 检测是否支持 webp
function check_webp_feature(feature, callback) {
    var kTestImages = {
        lossy: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA",
        lossless: "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==",
        alpha: "UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==",
        animation: "UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA"
    }

    var img = new Image()
    img.onload = function () {
        var result = (img.width > 0) && (img.height > 0)
        callback(feature, result)
    };

    img.onerror = function () {
        callback(feature, false)
    }
    img.src = "data:image/webp;base64," + kTestImages[feature]
}

// 使用
check_webp_feature('lossy', function (feature, isSupported) {
    if (isSupported) {
      // todo
    } else {
      // todo
    }
});
// 检测是否支持 webp
function check_webp_feature(feature, callback) {
    var kTestImages = {
        lossy: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA",
        lossless: "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==",
        alpha: "UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==",
        animation: "UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA"
    }

    var img = new Image()
    img.onload = function () {
        var result = (img.width > 0) && (img.height > 0)
        callback(feature, result)
    };

    img.onerror = function () {
        callback(feature, false)
    }
    img.src = "data:image/webp;base64," + kTestImages[feature]
}

// 使用
check_webp_feature('lossy', function (feature, isSupported) {
    if (isSupported) {
      // todo
    } else {
      // todo
    }
});