《前端之路》之 前端图片 类型 & 优化 & 预加载 & 懒加载 & 骨架屏

09: 前端图片 类型 & 优化 & 预加载 & 懒加载 & 骨架屏

这是一篇关于在前端开发中 与图片相关的一些常见问题,回想一下,我们在日常的开发过程中前端与图片打交道的次数可以说是比所有开发职位都要多吧。特别是在 nodeJs 盛行以后。

从我们最开始学习前端的那一天,我们是不是认识了 一个叫 <img /> 的 标签,这个标签的 src 属性可以引用对应路径的图片,然后手动刷新页面,我们的图片就显示在了页面上了, 哇~ 大学的教师里大家都不约而同的发出了哇的声音,回想起来还是历历在目啊~

那么 从业前端 这个岗位也这么多年了,总结一下在前端中与图片打交道的一些经验或者总结吧

一、 前端图片的类型

jpg、png、gif、base64、字体图标。貌似日常开发中,我们常常会用到的就说这些了。

那我们来纵向的来统计一下图片的类型 和 这些类型的图片在不同场景下有哪些优缺点。

( 因为本身对于图片的理解度还是不够的,所以询问了公司的 UI设计师的小姐姐们来帮忙答疑解惑 )
1.1、矢量图 和 位图
先上和UI部门的小姐姐的聊天~

其实我觉得 小姐姐的回答 可以说是非常容易懂了

矢量图: 一般来说矢量图表示的是几何图形,文件相对较小,并且放大缩小不会失真。

用途:SVG,图标字体font-awesome

位 图: 位图又叫像素图或栅格图,它是通过记录图像中每一个点的颜色、深度、透明度等信息来存储和显示图像。 放大会失真(变模糊)

用途:png,gif,jpg,canvas
1.2、有损压缩 和 无损压缩

  • 有损压缩是对图像数据进行处理,去掉那些图像上会被人眼忽略的细节,然后使用附件的颜色通过渐变或其他形式进行填充。适用于: JPG。 从字面意义上理解就是 对图片会有一定的损伤。像素的损伤,从而压缩了 图片的体积。

  • 无损压缩是先判断图像上哪些区域的颜色是相同的,哪些是不同的,然后把这些相同的数据信息进行压缩记录,而把不同的数据另外保存。适用于: PNG。对于图片的压缩也会造成一定的损伤,但是相对有限。

  • 看完上面的 对比,仿佛发现了上帝是公平的 开了一扇门,也关上了一扇窗。

1.3、透明度
  • 索引透明

    即布尔透明,类似于GIF,某一个像素只有全透和全不透明两者效果,不能对透明度进行设置。

  • Alpha透明

    半透明,可以设置 0~100 的透明度。

1.4、常用图片格式
  • JPN 、PNG、 GIF
1.5、新图片格式 - WebP
  • 出自于谷歌,是一种支持有损压缩和无损压缩的图片文件格式,派生自图像编码格式VP8。

    具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量。

    具备了无损和有损的压缩模式

    支持Alpha透明以及动画的特性

    在JPEG和PNG的转化效果都非常优秀,稳定和统一。

1.6、Base64 图片格式
如何生成 Base64 格式的图片
var reader = new FileReader(),htmlImage;
reader.onload = function(e){
    //e.target.result 就是base64编码
    htmlImage = '<img src="' + e.target.result + '"/>';
}
reader.readAsDataURL(file);
优缺点:

优点
减少HTTP请求
没有图片更新要重新上传,清理缓存的问

缺点
增加了CSS文件的尺寸
编码成本

二、 前端中图片相关的优化处理

2.1 经常会用到的方法:
  • 图片大小与展示区一致 (图片大小合适不过多浪费下载资源)
  • GIF转为PNG8 (减少gif 体积)
  • 缩略图(大图片,先加载一张缩略图)(避免页面中图片的位置出现长时间的空白,影响用户体验)

三、预加载

3.1、原理

 通过CSS或者JavaScript,先请求图片到本地,再利用浏览器的缓存机制,当要使用图片时(图片路径一致),浏览器直接从本地缓存获取到图片,加快图片的加载速度。

3.2、场景

背景,幻灯片,相册等,将要展示的前一张和后一张优先下载

3.3、优缺点

如果都在首页进行预加载肯定会加长首页加载时间,首屏加载变慢,影响体验。
但是在 http2 来临的时候这个问题,应该可以很有效的进行一个解决。

3.4、实现方法

CSS

#preload{ backgroud: url(./01.png) no-repeat -9999px -9999px;}

使用这个方法加载图片会同页面的其他内容一起加载,增加了页面的整体加载时间

JS


let img = document.createElement('img')
img.src = './02.png'

let img = new Image()
img.src = './01.png'

// 但是这种方法是无法添加的 DOM 树中去的。

四、 懒加载

4.1、原理

当要使用到图片时,再加载图片,而不是一下子加载完所有的图片的方式,来提高页面其他图片的加载速度。

4.2、场景

当前页面的图片数量过多,且页面长度很长。

4.3、JS 实现

思路很简单,一般都是在页面上添加一个滚动条事件,判断图片位置与浏览器顶部的距离是否小于(可视高度+滚动距离),如果小于则优先加载。

下面我们就基于 react 来进行懒加载的实现。

或者可以查看 github 地址:

github传送门

代码如下:

    componentDidMount() {

        const lazyload = (options) => {
            // 获取图片外部dom
            let doc = options.id ? document.getElementById(options.id) : document
            if (doc === null) return
            // 获取当前dom 内,所有的图片标签
            let tmp = doc.getElementsByTagName('img')
            let tmplen = tmp.length
            let imgobj = []

            // 判断当前 元素是否到了应该显示的 位置
            const isLoad = (ele) => {
                let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop
                if (typeof ele === 'undefined') return false
                let edit = ~~ele.getAttribute('data-range') || options.lazyRange
                let clientHeight = scrollTop + document.documentElement.clientHeight + edit
                let offsetTop = 0
    
                while (ele.tagName.toUpperCase() !== 'BODY') {
                    offsetTop += ele.offsetTop
                    ele = ele.offsetParent
                }
                return (clientHeight > offsetTop)
            }

            // 给已经到了可以显示图片位置的 img 标签添加 src 值
            const setimg = (ele) => {
                ele.src = ele.getAttribute('data-src')
            }

            // 遍历当前 dom 内所有要显示的 img 标签
            for (let i = 0; i < tmplen; i++) {
                var _tmpobj = tmp[i]
                if (_tmpobj.getAttribute('data-src') !== null) {
                    if (isLoad(_tmpobj)) {
                        setimg(_tmpobj)
                    } else {
                        imgobj.push(_tmpobj)
                    }
                }
            }

            // 滚动的时候动态 判断当前 元素的是否 可以赋值
            let len = imgobj.length
            const handler = () => {
                for (let i = 0, end = len; i < end; i++) {
                    let obj = imgobj[i]
                    if (isLoad(obj)) {
                        _setimg(obj)
                        imgobj.splice(i, 1)
                        len--
                        if (len === 0) {
                            loadstop()
                        }
                    }
                }
            }
    
            // 根据上下文要求动态低进行 图片 src 赋值
            const _setimg = (ele) => {
                if (options.lazyTime) {
                    setTimeout(function () {
                        setimg(ele)
                    },
                    options.lazyTime + ~~ele.getAttribute('data-time'))
                } else {
                    setimg(ele)
                }
            }
            
            // 去除 滚动事件监听
            const loadstop = () => {
                window.removeEventListener ? window.removeEventListener('scroll', handler, false) : window.detachEvent('onscroll', handler)
            }
    
            loadstop()
            // 添加滚动事件监听
            window.addEventListener ? window.addEventListener('scroll', handler, false) : window.attachEvent('onscroll', handler)
        }

        lazyload({
            id: 'imgs',
            lazyTime: 200,
            lazyRange: 100
        })
    }

在以上的基础上,其实可以进行很好的 组件化 的操作。 是一个 很好的面向对象的一个 JS 代码的实现的例子。后面的文章当中。我们也会加大 JS 中 OOP 相关文章的篇幅,敬请期待~

五、 骨架屏(首屏加载优化)

今天终于要讲到我们的主角了, 骨架屏。 终于不是首页进去就是加载一个 菊花了,让你成为 菊外人了,而是一个 科技感满满的的 骨架屏 ,好了,话不多说,开始今天的讨论吧!

5.1、原理

在 H5 中,骨架屏已经不是什么新奇的概念了,在我们常用的很多网站中都有关于这方面的介绍,而且我们在实际的应用中也能查找到一些案例,比如说: 饿了么、小米、掘金等 他们的 H5 端都有做骨架屏,让我们的体验不会显得那么的单薄,而是满满都科技感。

至于实现的原理的话,其实也很简单,就是在页面还未加载渲染出来之前,在页面的空白处先展示出来一个简单的类似页面原型的html。(小程序除外,后面也会介绍到小程序的骨架屏)

5.2、场景

先上图:

图片来源网络,侵删

在对前端技术比较依赖的大小厂当中,都已经使用骨架屏来改善自家的首屏加载、模块加载,那我们是不是也应该折腾的搞起来!

5.3、JS实现

目前前端大的框架、模式大致可以分为三类: Vue、 React、 小程序。(为什么没有 Angular ? 你可以去问大漠穷秋撒~😄)

那么我们今天就来讲讲这三个方向的 骨架屏 优化!

5.3.1 React

React 实现方式一、

    <div id="root">
        <div class="skeleton page">
            <div class="skeleton-nav"></div>
            <div class="skeleton-swiper"></div>
            <ul class="skeleton-tabs">
              <li class="skeleton-tabs-item"><span></span></li>
              <li class="skeleton-tabs-item"><span></span></li>
              <li class="skeleton-tabs-item"><span></span></li>
              <li class="skeleton-tabs-item"><span></span></li>
              <li class="skeleton-tabs-item"><span></span></li>
              <li class="skeleton-tabs-item"><span></span></li>
              <li class="skeleton-tabs-item"><span></span></li>
              <li class="skeleton-tabs-item"><span></span></li>
            </ul>
            <div class="skeleton-banner"></div>
            <div class="skeleton-productions"></div>
            <div class="skeleton-productions"></div>
            <div class="skeleton-productions"></div>
            <div class="skeleton-productions"></div>
            <div class="skeleton-productions"></div>
            <div class="skeleton-productions"></div>
          </div>
    </div>
    <style>
        .skeleton {
          position: relative;
          height: 100%;
          overflow: hidden;
          padding: 15px;
          box-sizing: border-box;
          background: #fff;
        }
        .skeleton-nav {
          height: 45px;
          background: #eee;
          margin-bottom: 15px;
        }
        .skeleton-swiper {
          height: 160px;
          background: #eee;
          margin-bottom: 15px;
        }
        .skeleton-tabs {
          list-style: none;
          padding: 0;
          margin: 0 -15px;
          display: flex;
          flex-wrap: wrap;
        }
        .skeleton-tabs-item {
          width: 25%;
          height: 55px;
          box-sizing: border-box;
          text-align: center;
          margin-bottom: 15px;
        }
        .skeleton-tabs-item span {
          display: inline-block;
          width: 55px;
          height: 55px;
          border-radius: 55px;
          background: #eee;
        }
        .skeleton-banner {
          height: 60px;
          background: #eee;
          margin-bottom: 15px;
        }
        .skeleton-productions {
          height: 20px;
          margin-bottom: 15px;
          background: #eee;
        }
    </style>

在我们 最初创建的html文件中的 id = #root Dom 内,写入我们的骨架屏的 html 和 css 的代码。

id = #root Dom 的 VD 还未被渲染出来之前, 就会先渲染出写死在骨架屏中的html代码。

这个就是 骨架屏 最原始的原理。

但是!!! 这么写前端的代码是不是特别LOW呐?前端早已过了刀耕火种的年代了,再这么不科学的写代码就
会被做 code review 的同学砍死的吧。

那么我们就来写一些高级一点,且适合维护的前端代码。就是下面介绍到的第二种实现的方式。

React 实现方式二、

上面已经介绍到了,骨架屏实际最为真实的原理,那么我们现在就需要让 这个真实的原理穿上盔甲,变得强硬起来。

1、 先定义一个 skeleton 的组件。
2、 通过 react-dom 的 renderToStaticMarkup 方法获取到当前组件的 html 代码。
3、 在 build 构建 项目的时候 通过 fs 读取到 index.html 文件的全部内容,并将提前写好在html 的注释 进行替换。

5.3.2 Vue

Vue 实现骨架屏的第一种方式也是上面那样。

但是第二种纯粹的代码实现就 相对于 react 的要困难一些,需要用到 node服务去做 服务端渲染,然后在输出index.html 的时候 先显示 骨架屏组件的内容,等到 app.js 中的内容加载完毕以后便自动替换了html 中的文件。

但是!

vue 有一点优点就是 任何组件都可以来进行骨架屏的优化。

具体如下:

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

通过异步组件加载的机制,来变相的实现 骨架屏也是一种不错的思路。时间有限,大家可以自己下去思考下。

5.3.2 小程序

这里的原理可以同 上面 H5的方法一样,文章中也是简单介绍2中方法。

方法一: 用原生的api 写的小程序
先写一个 skeleton 的组件。

放在首页的第一渲染的位置。通过 wx-if 来控制 skeleton 组件的显示和 小程序 index 渲染。
方法二: 用 wepy 框架写的小程序
方法二: 其实也是通过方法一来同样实现的。但是小程序首页加载的问题,并不是通过 骨架屏就能来解决的。

其实  首页分包 + 骨架屏 同时来使用,首页加载的问题应该可以得到相应的解决。

5.4、尾声

写到最会,其实骨架屏没有多大的难度,如果有效的来管理这些骨架屏才是难点之一。
文章后面写到的 骨架屏并未做过多的阐述,后面有机会的话再来拿实际的项目数据来解释这个不难的技术。

Github传送门,欢迎 Star - -

Github地址,欢迎 Star

posted @ 2018-08-03 18:32  SmallW  阅读(1729)  评论(5编辑  收藏  举报