本篇文章是 Element-UI 源码剖析系列的第三篇,这篇文章主要分析图片组件,以及实现图片的预览功能,重点是图片的预览功能,希望对大家有所帮助!
Element-UI 源码剖析系列文章:
1.Element-UI源码剖析(一)------ Message组件
2.Element-UI源码剖析(二)------ PopupManager的具体实现
一、图片组件
Iamge 图片组件提供的属性、事件、插槽:

js
<template>
<div class="el-image">
<!-- 插槽------图片未加载的占位内容 -->
<slot v-if="loading" name="placeholder">
<div class="el-image__placeholder"></div>
</slot>
<!-- 插槽------加载失败的内容 -->
<slot v-else-if="error" name="error">
<div class="el-image__error">{{ t('el.image.error') }}</div>
</slot>
<!-- 图片主体 -->
<img
v-else
class="el-image__inner"
v-bind="$attrs"
v-on="$listeners"
@click="clickHandler"
:src="src"
:style="imageStyle"
:class="{ 'el-image__inner--center': alignCenter, 'el-image__preview': preview }">
<!-- 预览图片的组件 -->
<template v-if="preview">
<image-viewer :z-index="zIndex" :initial-index="imageIndex" v-if="showViewer" :on-close="closeViewer" :url-list="previewSrcList"/>
</template>
</div>
</template>
1. 属性/事件透传 <math xmlns="http://www.w3.org/1998/Math/MathML"> a t t r s 和 attrs 和 </math>attrs和listeners
关于属性、事件透传的简单介绍,想深入了解的请自行查阅:
-
attrs :包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外),可以通过 v-bind="$attrs" 传入内部组件
-
listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素
如果我们往 el-image 组件传入一些属性,并且想要将这些属性传给 img,最简单的方式就是在组件中一个个的去定义 props
,然后再传给 img,但是这种方法非常麻烦,毕竟 img
标签自带很多属性,比如:alt width height title align 等,因此使用 v-bind="$attrs" 可以完美的解决这个问题。
2. 实现图片懒加载

首先要获取监听 Scroll 事件的容器
开启懒加载:监听滚动事件(采用节流的方式),当前图片处于 scroll 容器的可见区域时,再去加载图片:
js
handleLazyLoad() {
// 该方法用于判断当前元素是否处于 scroll 容器的可见区域
if (isInContainer(this.$el, this._scrollContainer)) {
// 控制图片的加载
this.show = true;
// 移除监听
this.removeLazyLoadListener();
}
},
addLazyLoadListener() {
const { scrollContainer } = this;
let _scrollContainer = null;
if (isHtmlElement(scrollContainer)) {
_scrollContainer = scrollContainer;
} else if (isString(scrollContainer)) {
_scrollContainer = document.querySelector(scrollContainer);
} else {
_scrollContainer = getScrollContainer(this.$el);
}
if (_scrollContainer) {
this._scrollContainer = _scrollContainer;
this._lazyLoadHandler = throttle(200, this.handleLazyLoad);
on(_scrollContainer, 'scroll', this._lazyLoadHandler);
this.handleLazyLoad();
}
},
移除监听:
js
removeLazyLoadListener() {
const { _scrollContainer, _lazyLoadHandler } = this;
if (this.$isServer || !_scrollContainer || !_lazyLoadHandler) return;
off(_scrollContainer, 'scroll', _lazyLoadHandler);
this._scrollContainer = null;
this._lazyLoadHandler = null;
},
方法 on / off 相当于 document.addEventListener 以及 document.removeEventListener
3. ObjectFit属性兼容处理

判断当前浏览器环境是否支持 object-fit 属性:
js
computed: {
imageStyle() {
const { fit } = this;
if (fit) {
return isSupportObjectFit()
? { 'object-fit': fit }
: this.getImageStyle(fit);
}
return {};
}
}

为了兼容一些不支持 object-fit 属性的浏览器,比如 IE11,需要做一些额外的处理:主要是对比图片的宽高比以及容器的宽高比,来设置不同的属性。
js
getImageStyle(fit) {
// 图片的宽高
const { imageWidth, imageHeight } = this;
// 容器的宽高
const {
clientWidth: containerWidth,
clientHeight: containerHeight
} = this.$el;
if (!imageWidth || !imageHeight || !containerWidth || !containerHeight) return {};
// 计算两个宽高比
const imageAspectRatio = imageWidth / imageHeight;
const containerAspectRatio = containerWidth / containerHeight;
if (fit === ObjectFit.SCALE_DOWN) {
const isSmaller = imageWidth < containerWidth && imageHeight < containerHeight;
fit = isSmaller ? ObjectFit.NONE : ObjectFit.CONTAIN;
}
switch (fit) {
case ObjectFit.NONE:
return { width: 'auto', height: 'auto' };
case ObjectFit.CONTAIN:
return (imageAspectRatio < containerAspectRatio) ? { width: 'auto' } : { height: 'auto' };
case ObjectFit.COVER:
return (imageAspectRatio < containerAspectRatio) ? { height: 'auto' } : { width: 'auto' };
default:
return {};
}
},
二、图片预览
Element-UI 的图片预览效果:具备图片缩放、旋转等功能

先来看下图片预览组件的页面布局以及 HTML 代码结构:
js
<template>
<transition name="viewer-fade">
<div tabindex="-1" ref="el-image-viewer__wrapper" class="el-image-viewer__wrapper" :style="{ 'z-index': viewerZIndex }">
<!-- 灰色遮罩层 -->
<div class="el-image-viewer__mask" @click.self="handleMaskClick"></div>
<!-- 右上角关闭按钮 -->
<span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
<i class="el-icon-close"></i>
</span>
<!-- 左右箭头 -->
<template v-if="!isSingle">
<span
class="el-image-viewer__btn el-image-viewer__prev"
:class="{ 'is-disabled': !infinite && isFirst }"
@click="prev">
<i class="el-icon-arrow-left"/>
</span>
<span
class="el-image-viewer__btn el-image-viewer__next"
:class="{ 'is-disabled': !infinite && isLast }"
@click="next">
<i class="el-icon-arrow-right"/>
</span>
</template>
<!-- 下方的操作按钮 -->
<div class="el-image-viewer__btn el-image-viewer__actions">
<div class="el-image-viewer__actions__inner">
<i class="el-icon-zoom-out" @click="handleActions('zoomOut')"></i>
<i class="el-icon-zoom-in" @click="handleActions('zoomIn')"></i>
<i class="el-image-viewer__actions__divider"></i>
<i :class="mode.icon" @click="toggleMode"></i>
<i class="el-image-viewer__actions__divider"></i>
<i class="el-icon-refresh-left" @click="handleActions('anticlocelise')"></i>
<i class="el-icon-refresh-right" @click="handleActions('clocelise')"></i>
</div>
</div>
<!-- 图片主体画布 -->
<div class="el-image-viewer__canvas">
<img
v-for="(url, i) in urlList"
v-if="i === index"
ref="img"
class="el-image-viewer__img"
:key="url"
:src="currentImg"
:style="imgStyle"
@load="handleImgLoad"
@error="handleImgError"
@mousedown="handleMouseDown">
</div>
</div>
</transition>
</template>
定义 transform 对象来表示图片的样式,包括缩放的倍数、旋转的角度、水平方向以及垂直方向的偏移量等,当其中的值发生改变时,重新给图片设置样式。

下面我们来学习下预览图片中的一些难点以及亮点,方便扩展我们的前端知识库,以及提升前端研发水平:
1. 图片展示模式的更改
图片的展示有两种模式:

第一种:contain
点击底部操作栏的中间按钮,会使图片的展示模式切换为:original

具体的实现代码如下:
js
// 定义两种模式
const Mode = {
CONTAIN: {
name: 'contain',
icon: 'el-icon-full-screen'
},
ORIGINAL: {
name: 'original',
icon: 'el-icon-c-scale-to-original'
}
};
// 点击切换模式
toggleMode() {
if (this.loading) return;
const modeNames = Object.keys(Mode);
const modeValues = Object.values(Mode);
const index = modeValues.indexOf(this.mode);
const nextIndex = (index + 1) % modeNames.length;
this.mode = Mode[modeNames[nextIndex]];
this.reset(); // 重置样式
}
// 修改图片的样式
imgStyle() {
......
if (this.mode === Mode.CONTAIN) {
style.maxWidth = style.maxHeight = '100%';
}
return style;
}
关于 maxWidth = maxHeight = '100%':图片按比例填充画布
2. 点击缩放、旋转图片
这里规定了每次放缩时增减 0.2,可以自己定义,不断修改 transform 中的 scale 属性即可;旋转的话,将 transform 中的 deg 属性每次增减 90
js
handleActions(action, options = {}) {
if (this.loading) return;
const { zoomRate, rotateDeg, enableTransition } = {
zoomRate: 0.2,
rotateDeg: 90,
enableTransition: true,
...options
};
const { transform } = this;
switch (action) {
// 缩小
case 'zoomOut':
if (transform.scale > 0.2) {
transform.scale = parseFloat((transform.scale - zoomRate).toFixed(3));
}
break;
// 放大
case 'zoomIn':
transform.scale = parseFloat((transform.scale + zoomRate).toFixed(3));
break;
// 逆时针旋转
case 'clocelise':
transform.deg += rotateDeg;
break;
// 顺时针旋转
case 'anticlocelise':
transform.deg -= rotateDeg;
break;
}
transform.enableTransition = enableTransition;
}
3. 滚动滚轮缩放图片
这个功能比较核心,用到了鼠标的滚轮事件,但是这里要做好兼容处理,因为火狐浏览器不支持 mousewheel 事件,而是使用 DOMMouseScroll 事件,具体的区别请自行查阅,这里不再说明。
js
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel';
在 mounted 中初始化监听鼠标的滚轮事件以及键盘事件,在关闭预览的时候要取消事件的监听


监听键盘事件,当按下某些键时实现上面的一些功能:

js
on(document, 'keydown', this._keyDownHandler);
接下来是监听鼠标的滚轮事件(采用节流的方式,避免频繁触发):

分析下这段代码:
js
this._mouseWheelHandler = rafThrottle(e => {
const delta = e.wheelDelta ? e.wheelDelta : -e.detail;
if (delta > 0) {
// 放大图片
this.handleActions('zoomIn', {
zoomRate: 0.015, // 每次增大0.015
enableTransition: false
});
} else {
// 缩小图片
this.handleActions('zoomOut', {
zoomRate: 0.015, // 每次减小0.015
enableTransition: false
});
}
})
e.wheelDelta 与 e.detail 表示鼠标滚轮的滚动方向,e.detail 用在火狐浏览器上,e.wheelDelta 用在谷歌、IE等浏览器;当 delta 大于零时,放大图片,反之缩小图片

4. 鼠标按下拖拽图片

js
// 鼠标按下事件
handleMouseDown(e) {
if (this.loading || e.button !== 0) return;
const { offsetX, offsetY } = this.transform;
const startX = e.pageX;
const startY = e.pageY;
// 拖拽处理方法
this._dragHandler = rafThrottle(ev => {
this.transform.offsetX = offsetX + ev.pageX - startX;
this.transform.offsetY = offsetY + ev.pageY - startY;
});
// 监听鼠标的移动
on(document, 'mousemove', this._dragHandler);
// 鼠标离开时移除鼠标移动事件的监听
on(document, 'mouseup', ev => {
off(document, 'mousemove', this._dragHandler);
});
e.preventDefault();
},
主要是监听鼠标的移动、离开事件,在鼠标移动的过程中,重新计算 offsetX、offsetY,这两个值就相当于是外边距


还有一个要注意的点:每次切换图片的时候,要判断图片是否加载完成,通过 complete 属性:
js
currentImg(val) {
this.$nextTick(_ => {
const $img = this.$refs.img[0];
if (!$img.complete) {
this.loading = true;
}
});
}
总的来说:图片预览的各种功能,其实就是在修改图片的样式属性,关键是要知道样式属性怎么变、变了多少!