Element-UI源码剖析(三)—— 图片组件及其预览功能

本篇文章是 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

关于属性、事件透传的简单介绍,想深入了解的请自行查阅:

  1. attrs :包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外),可以通过 v-bind="$attrs" 传入内部组件

  2. 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;
        }
    });
}

总的来说:图片预览的各种功能,其实就是在修改图片的样式属性,关键是要知道样式属性怎么变、变了多少!

相关推荐
zwjapple4 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20206 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem6 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊7 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术7 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理7 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
GISer_Jing7 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止7 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall7 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴7 小时前
简单入门Python装饰器
前端·python