weixin_39902184
weixin_39902184
2021-01-02 14:07

59.适用于 vue.js 和原生 js 的渐进式图片加载

渐进式图片加载 progressive-image

知乎Medium 都用了 progressive image (渐进式图片加载),用低分辨率的模糊图片来做预览图,代替以前懒加载图片时用的 logo 占位图。预览图大小也在平均 2KB~3KB 之间,虽然 cdn 流量上有所增加,但用户体验却非常好。

知乎和 Medium 使用的是动态绘制 canvas 这种比较复杂的方式来展现模糊效果,所以来实现一个只需要 HTML、CSS、JS 就能实现的渐进式图片加载。

代码已经封装

先看简单例子 demo

HTML

基本的 HTML 结构如下

html
<div class="progressive">
  <img class="preview lazy" data-src="origin.jpg" src="preview.jpg">
</div>

origin.jpg 就是原图,preview.jpg 是等比压缩后的预览图,比如原图是 400x200 则预览图可以使 20x10。

如果原图是 400x100 按照 4:1 的比例,预览图如果宽度是30,那么高度就是 7.5,所以图片裁剪这里会有坑,后面会遇到。

CSS

容器的基本样式

css
.progressive {
  position: relative;
  display: block;
  overflow: hidden;
}

外层的 .progressive 默认是 div 也可以是其他任何需要的包裹元素

图片容器可以是固定尺寸,也可以是固定的宽高比(用 padding-top 的方式来确保容器的固定的宽高比例),这就保证了原始图片加载之后不会出现容器尺寸的变化,然而这就必须计算每张图片的宽高比例。

知乎和 Meduim 都是获取到了原图尺寸,然后保持预览图(canvas)也是相同的尺寸,这样即使预览图被裁剪后和原图尺寸的比例不一致,也不会出现容器尺寸在原图加载之后会抖动的bug。

我们退而求其次,采取更简单粗暴的方式来做:

  1. 预览图和原始图尽量保持相同的宽高比
  2. 原图加载完后,不删除预览图,而是设置为 opacity:0

PS: 如果预览图和原图的比例不一致,同时原图加载后删除预览图,就会出现抖动,如下图

原图尺寸 800x533 裁剪后如果按照比例应该是 20x13.325 但实际是 20x13 ,所以加载原图后空间不够,往下伸展出一部分。因此为了防止抖动的简单处理就加上第二点:不删除原图并设置为透明。

容器内图片

预览图和原图都充满容器

css
.progressive img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  border: 0 none;
}

预览图模糊处理

  • blur(2vw) 是为了保持相同的模糊度,而页面的尺寸无关。
  • transform: scale(1.05); 添加图片过渡动画
css
.progressive img.preview {
    filter: blur(2vw);
    transform: scale(1.05);
  }

原图绝对定位于容器左上角,是为了在动画阶段能覆盖原图。

css
.progressive img.origin {
  position: absolute;
  left: 0;
  top: 0;
  animation: origin 1s ease-out;
}

 origin {
  0% {
    transform: scale(1.1);
    opacity: 0;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

JavaScript

js的处理就简单多了,和懒加载图片没什么区别,主要一个 checkImage 函数在 DOMReady 和 scroll 时检测可视区 view 内是否有图片需要懒加载

以及 loadImage 函数用来替换预览图,加载原图触发动画。

js
function checkImage() {
  const lazys = document.querySelectorAll('img.lazy')
  const l = lazys.length
  if(l>0){
    for (var i = 0; i < l; i++) {
      var rect = lazys[i].getBoundingClientRect()
      if (rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0) {
        loadImage(lazys[i])
      }
    }
  }else {
    events(window, false)
  }
}

响应式图片

demo 里最后一张图片用了响应式图片

data-srcset="./progressive/4.jpg 960w, ./progressive/4-m.jpg 1280w, ./progressive/4-l.jpg 1920w"

html
<div class="progressive full">
    <img class="preview lazy" data-src="./progressive/4.jpg" src="./progressive/r4.jpg">
  </div>

浏览器如果支持 img 标签的 srcset 将会根据 viewport 的宽度选取适当的图片来加载。

封装代码

JS

代码封装为 原生js 和 Vue.js 两种

原生js为一个 Class ,实例化后直接使用,封装后 140 左右的代码
index.js

Vue.js 版本为一个 v-progress 的指令,只要在需要的 img 标签上添加即可,总共 190 行左右的代码
index-vue,js

CSS

抽取用于动画的 css 样式,写成 stylus 总共才 36 行
index.styl

GitHub、demo

仓库的 README.md 文件有详细的使用说明

GitHub 地址

NPM 地址

vue-demo

原生js-demo

图片裁剪

多数 cdn 都会提供图片上传后裁剪的功能

node.js 有一个很好用的图片裁剪模块 gm

裁剪代码也很简单:

js
const gm = require('gm')

gm('./coding.jpg')
.resize(20)
.write('r.jpg', function (err) {
  err && console.log(err)
})

对于简单裁剪 PHP 直接用 gd 库, python 可以用 pillow 库解决。

该提问来源于开源项目:ccforward/cc

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

25条回答

  • weixin_39622283 weixin_39622283 3月前

    image 你好我想请问一下,在图片的同一个位置我刷新一下浏览器,为什么会产生两个一样的img标签,我觉得应该始终都是一个才对。

    点赞 评论 复制链接分享
  • weixin_39902184 weixin_39902184 3月前
    这个问题你提个 issue   我来看下吧
    
    点赞 评论 复制链接分享
  • weixin_39595320 mizore 3月前

    是不是在IOS的Safari会失效,好像图片的onload事件在Safari会失效

    点赞 评论 复制链接分享
  • weixin_39575565 weixin_39575565 3月前

    原图局对定位于容器左上角,是为了在动画阶段能覆盖原图。

    发现一个错字,原图绝对定位,抱歉。

    点赞 评论 复制链接分享
  • weixin_39689297 weixin_39689297 3月前

    图片裁剪是指把原图通过插件处理了,等于需要两个链接吗. 不是说一张图片链接就能搞定. 我学习不好, 我不太理解

    点赞 评论 复制链接分享
  • weixin_39902184 weixin_39902184 3月前

    是需要两张图片的 一张原图 一张宽为20px等比缩小的缩略图

    点赞 评论 复制链接分享
  • weixin_39918145 weixin_39918145 3月前

    你好,我vue的项目用了您的组件,遇到这样的问题:首页大图用了这个效果,然后路由切换到其他页面后,再切换回首页,首页大图就不加载了,一直是模糊的初始状态。怎么样才能让它再次触发加载呢。

    点赞 评论 复制链接分享
  • weixin_39902184 weixin_39902184 3月前

    不好意思 可能是个bug 我来复现下看看先

    点赞 评论 复制链接分享
  • weixin_39918145 weixin_39918145 3月前

    好的,辛苦。

    点赞 评论 复制链接分享
  • weixin_39878247 weixin_39878247 3月前

    牛逼啊

    点赞 评论 复制链接分享
  • weixin_39975900 weixin_39975900 3月前

    您好,我也在用路由的时候,遇到了跟 一样的问题,切换路由后,再切换回来的时候,图片不加载一直处于模糊的状态,请问您这个问题解决了吗?

    点赞 评论 复制链接分享
  • weixin_39902184 weixin_39902184 3月前

    bug 已经修复 版本号 1.2.0

    点赞 评论 复制链接分享
  • weixin_39902184 weixin_39902184 3月前

    bug 已经修复 版本号 1.2.0

    点赞 评论 复制链接分享
  • weixin_39975900 weixin_39975900 3月前

    是的,已经试过,非常棒!

    点赞 评论 复制链接分享
  • weixin_39872222 weixin_39872222 3月前

    我的vue项目遇到这样一个问题,同一个页面我用到两个v-progressive指令,分别有两个板块的图片,但是最多只能加载出一个屏幕区域的图片,其它图片就会一直保持模糊状态不在加载,请问这该怎么解决呢?

    点赞 评论 复制链接分享
  • weixin_39962675 weixin_39962675 3月前

    请问如果是网络动态加载图片该如何处理呢

    点赞 评论 复制链接分享
  • weixin_39588432 weixin_39588432 3月前

    get 以后懒加载尝试下的 体验棒棒的

    点赞 评论 复制链接分享
  • weixin_39829497 weixin_39829497 3月前

    啊,还想知道 canvas 是如何做的

    点赞 评论 复制链接分享
  • weixin_39672194 jck????? 3月前
    点赞 评论 复制链接分享
  • weixin_39689870 weixin_39689870 3月前

    你不觉得眼睛被晃花了吗?大图片才希望有这种体验吧

    点赞 评论 复制链接分享
  • weixin_39929715 weixin_39929715 3月前

    get 新技能

    点赞 评论 复制链接分享
  • weixin_39829497 weixin_39829497 3月前

    tks!

    点赞 评论 复制链接分享
  • weixin_39771969 weixin_39771969 3月前

    老师您好,我是sdk.cn编辑于洋,请问可以将这篇文章转载到我们平台吗,请您放心我们会严格遵守转载规范,保护您的权益。

    点赞 评论 复制链接分享
  • weixin_39902184 weixin_39902184 3月前

    可以

    点赞 评论 复制链接分享
  • weixin_39771969 weixin_39771969 3月前

    谢谢!

    点赞 评论 复制链接分享

为你推荐