前言

网页在前端的展示主要集中在文字、图片、表格、公式、音频、视频等元素。笔者准备以元素为基准写一个系列,以期以点成线,用这样的方式“重走前端之路”。本文是系列的第二篇。

图片是网页的重要组成部分,业务场景中常见的图片应用无外乎图片上传、图片加载、图片压缩、图片裁剪、DOM 转图片、图片缩放等等,本文以“图片”为基点,讲述图片在不同应用场景下的解决方案。

图片格式

资源优化是性能优化中收益最高的,简单且见效快,因此你需要知道不同格式图片的适用场景。

  • jpg:适用于内容图片多为照片之类的。
  • png:适用于装饰图片,通常更适合用无损压缩。
  • gif:基本上除了 gif 动画外不要使用。
  • webP:大大减小图片的体积,但是移动端有兼容性问题。

图片上传

图片的形式可以是 base64 的字符形式也可以是 File 的形式。但因为 base64 有长度的限制,因此较大的图片推荐使用 File 形式。不仅仅是在上传的场景中,图片的裁剪等场景也适用。

base 64

根据 base64 的编码原理,base64图片大小比原文件大小大 1/3,所以说 base64 一般用于上传一些非常小的小图。

FileReader 将上传的本地图片转成 base64

function file2DataUrl(file, callback) {
  var reader = new FileReader();
  reader.onload = function () {
    callback(reader.result);
  };
  reader.readAsDataURL(file); // 转换成 base64
}

file 对象

正常情况,file 对象数据是放在POST请求的 body 里面,并且是 form-data 编码。

function upload(url, file, callback) {
  var xhr = new XMLHttpRequest();
  var fd = new FormData();
  fd.append('file', file);
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
      // 上传成功
      callback && callback(xhr.responseText);
    } else {
      throw new Error(xhr);
    }
  }
  xhr.open('POST', url, true);
  xhr.send(fd);
}

图片懒加载

前端性能优化很重要的一个手段就是图片懒加载。图片懒加载有利于减少网络加载,提升用户感知体验。

懒加载的原理

图片的 src 属性决定了图片是否加载。图片懒加载就是 src 的按需获取。可以在 src 上放占位图的地址,将图片的真实路径存储到自定义数据属性 data-src 上,然后在需要的时候按需加载。通常会有滚动到页面底部加载图片的应用场景。

滚动监听

其主要实现的原理是对滚动事件的监听,当发现滚动条高度 + 滚动条高度 = document 的高度时,就是达到页面的底部了,是需要重新加载图片的时机。这种方法的缺点是,由于scroll事件密集发生,计算量很大,容易造成性能问题。

window.onscroll= function(){
    //文档内容实际高度(包括超出视窗的溢出部分)
    var scrollHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
    //滚动条滚动距离
    var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
    //窗口可视范围高度
    var clientHeight = window.innerHeight || Math.min(document.documentElement.clientHeight,document.body.clientHeight);

    if(clientHeight + scrollTop >= scrollHeight){
        console.log("===加载图片……===");
    }

}
function lazyload() {
  var images = document.getElementsByTagName('img')
  var len = images.length
  var n = 0 //存储图片加载到的位置,避免每次都从第一张图片开始遍历
  return function () {
    var seeHeight = document.documentElement.clientHeight
    for (var i = n; i < len; i++) {
      if (images[i].getBoundingClientRect().top < seeHeight) {
        当图片的视口 top出现在视口中
        if (images[i].getAttribute('src') === 'images/default.jpg') {
          images[i].src = images[i].getAttribute('data-src')
        }
        n = n + 1
      }
    }
  }
}

原生 API

通过依赖浏览器原生 API IntersectionObserver 来检测元素是否进入视图,Chrome 51+ 已经支持。

<img src="placeholder.png" src="img-1.jpg">
<img src="placeholder.png" src="img-2.jpg">
<img src="placeholder.png" src="img-3.jpg">

上面代码中,图像默认显示一个占位符,data-src属性是惰性加载的真正图像。

function query(selector) {
  return Array.from(document.querySelectorAll(selector));
}

var observer = new IntersectionObserver(
  function(entries) {
    entries.forEach(function(entry) {
      entry.target.src = entry.target.dataset.src;
      observer.unobserve(entry.target); // 停止观察
    });
  }
);

query('.lazy-loaded').forEach(function (item) {
  observer.observe(item); // 开始观察
});

图片加载失败

图片会有加载失败的情况,一方面我们需要考虑用户体验上的优化,另一方面开发需要感知到线上问题,能够进一步去核实原因,这个通过监控去做就好。

HTML 方案

<img src="xxx" onerror="javascript:this.src='images/logoError.png';">

// 避免 logoError 图片也不存在时循环调用
<img src="images/logo.png" onerror="notfound();"/>
<script type="text/javascript">
    function notfound() {
        var img = event.srcElement;
        img.src = "images/logoError.png";
        img.onerror = null; //解绑onerror事件
    }
</script>

CSS 方案

<img src="xxx" class="logo">
.logo{
    position:relative;
    width:100px;
    height:130px;
}
 
// 只有 image 没有加载出来的时候 以下才有效果
.logo:after{
    position:absolute;
    width:100px;
    height:130px;
    content:"";
    display:block;
    top:0;
    left:0;
    background:url('/img/default.png');
    background-size:100%;
}

图片压缩

压缩图片原理

图片压缩是一个比较复杂的事情,想要有好的压缩效果,一般是由后端大佬通过压缩算法来实现的。然而对于比较简单的需求,前端的压缩思路通常是通过 canvas 来操作图片压缩。将图片转化成 Image 对象,再将写入到 Canvas 画布,最后由 Canvas 对象 API 对图片的大小和尺寸输出调整,以实现压缩目的。
图片的格式的复杂性也是影响图片压缩效果的因素之一:

  • gif压缩后一般都会失真
  • svg通常用在矢量图标上很少用在场景图片上
  • webp由于兼容性问题很少被使用
  • 前端图片大多是jpg和png

了解这些特性之后,可以权衡利弊及成本,大舍大拿有的时候方能干大事。

手动压缩

工具 开源 收费 API 免费体验 压缩原理
TinyJpg TinyPng 可压缩类型较少,压缩质感很好,有数量限制,有体积限制
  • 上传下载全靠手动
  • 只能压缩jpg和png
  • 每次只能压缩20张
  • 每张体积最大不能超过5M
  • 可视化处理信息不是特别齐全

自动压缩

compression.js
使用简单,参数只有输入图片,压缩比例,输出图片。很少的代码量即可实现压缩和预览的效果。

webpack loader & plugin

image-webpack-loader 及 imagemin-webpack-plugin
它们都是基于 imagemin 开发的。imagemin的某些依赖托管在国外服务器,在 npm i xxx 安装它们时会默认走 GitHub Releases 的托管地址。压缩质感损失得比较严重,图片体积越大越明显,压缩出来的图片总有几张是失真的,而且总体压缩率不是很高。

tinyimg-webpack-plugin
利用的就是 TinyJpg|TinyPng 的API,只不过封装成了插件。

PPDuck3
客户端压缩工具。

DOM 转图片

Dom 转图片某种程度上和屏幕截图是一个道理,最终都会通过 Canvas 去转成图片。
关于截图,这里讲个题外话。iframe 有个特别大的缺点就是无法支持跨域屏幕截图,所以这也是为什么微前端方案里面不提倡用 iframe 的原因之一。

html2canvas

html2canvas 能够实现在用户浏览器端直接对整个或部分页面进行截屏。html2canvas 脚本将当页面渲染成一个Canvas 图片,通过读取 DOM 并将不同的样式应用到这些元素上实现。转换过程是DOM→Canvas→Image。
PS:注意这个库无法转换 svg 的 DOM 结构。
它不需要来自服务器任何渲染,整张图片都是在客户端浏览器创建。当浏览器不支持Canvas时,将采用Flashcanvas 或 ExplorerCanvas 技术代替实现。以下浏览器能够很好的支持该脚本:Firefox 3.5+, Google Chrome, Opera新的版本, IE9 以上的浏览器。

rasterizehtml

通过遍历 DOM 克隆一份副本,利用 SVG 的 foreignObject把 DOM 作为外部资源嵌套在 SVG 中,将此SVG 在 Canvas 上重新绘制,并根据 DOM 的样式应用在对应的绘制元素上,再通过 Canvas 生成图片。转换过程是:DOM→SVG的ForeignObject→Canvas→Image。
PS:无法渲染如lazyload等通过JS加载的资源以及内联background-image或JS操作background-image

phantomjs

phantomjs 是一个运行在服务端的无界面的浏览器。
PhantomJS是一个基于 webkit 的 JavaScript API。它使用 QtWebKit 作为核心浏览器的功能,使用webkit 来编译解释执行JavaScript代码。任何你可以在基于 webkit 浏览器做的事情,它都能做到。它不仅是个隐形的浏览器,提供了诸如CSS选择器、支持Web标准、DOM操作、JSON、HTML5、Canvas、SVG等,同时也提供了处理文件I/O的操作,从而使你可以向操作系统读写文件等。PhantomJS 的用处可谓非常广泛,诸如网络监测、网页截屏、无需浏览器的 Web 测试、页面访问自动化等。
需要在服务端进行截屏,所以还需要一个可以访问的 h5 页面,另外页面如果是服务端渲染,最好不要用 js 渲染,截图可能有问题。

另外一个在服务端的字体需要预先设置,比如微软雅黑、 华文苹方 服务器上如果没有就需要进行安装。不然截图的字体和你的效果图不一致。

优点:服务端截屏,可以做成通用的服务。
缺点:会增加前端同学的工作量,因为需要做服务,还需要做 h5 页面布局。当然也看怎么设计架构,如果足够通用也只需要写一次就够了。剩下的就是做 h5 页面了,这个就快多了。

图片裁剪

Croppie
这个库还有vue版本及原生版本的,相对轻量,也是一个老牌库。
你可能不知道的图片应用解决方案_Web
react-cropper
鼠标左键拖拽选中切割区域
文档见 cropper.js(同一个作者,react-cropper属于cropper.js的移植)

你可能不知道的图片应用解决方案_Web_02

react-image-crop
这个组件,提供了兼容 pc 与移动设备的图片裁剪区域选择功能。直接支持 react。

antd-img-crop
这个是熟悉的 antd 提供的配套组件,直接包裹 Upload 使用。

ucrop
安卓原生裁剪库。
你可能不知道的图片应用解决方案_Web_03

图片缩放

react-zmage
一个基于 React 的可缩放图片控件

你可能不知道的图片应用解决方案_Web_04
react-viewer | react-viewer-mobile
同一个作者基于 viewer 搞了两套,如果你有同时支持PC和Mobile的需求是一个比较好的选择,API 都是一样的。

图片性能

雪碧图

雪碧图,CSS Sprites,是一种 CSS 图像合成技术,主要用于小图片显示。雪碧图的优点是把诸多小图片合成一张大图,利用backround-position属性值来确定图片呈现的位置,这样就能减少 http 请求,达到性能优化的效果。

iconfont

iconfont(字体图标),即通过字体的方式展示图标,多用于渲染图标、简单图形、特殊字体等。
使用 iconfont 时,由于只需要引入对应的字体文件即可,这种方法可有效减少 HTTP 请求次数,而且一般字体体积较小,所以请求传输数据量较少。与直接引入图片不同,iconfont 可以像使用字体一样,设置大小、颜色及其他样式,且不存在失真的情况。

 

写在最后

本文对图片的常见场景的解决方案进行了梳理,对于库的使用由于篇幅有限不做说明,使用也都非常简单,详情可参考官方文档。下一篇讲“公式”。

以前觉得终身学习是知道越来越多的东西,后来才发现其实更多的成长是认知的成长。一本书《红楼梦》不同年龄段的人去读认知高度是不一样的。就像闭包在今天去看它,很快会想到 hooks,但是几年前这个概念都没有...我的文章也仅做抛砖引玉...下期见!