image-view组件的封装,你学废了吗

1,251 阅读3分钟

业务中我们常常需要展示某张图片,因此,经常会写以下代码:

<img class="sku-img" :src="host + row.image" @error="handleLoadImgError"/>

 handleLoadImgError (e) {
   e.target.src = require('@/assets/image/index-logo.png') // 默认图地址
 }

如果你用的是vue + element-ui,你也可以使用element-ui的Image图片组件

通过slot = error自定义一张默认图

但是无论是哪种方法,都会遇到一个问题,就是默认图片如果也加载不出来,那就会进入死循环了,因此我们可以将图片展示封装成公用方法或组件,兼容以上特殊情况,可以给我们开发大大节省时间。

使用自定义指令:

当图片src地址能正常加载,则不做处理; 当图片加载不出,则使用默认图片, 当默认图片也加载不出,则使用默认icon

因此需要定义一个检测图片是否加载出来的方法

// 检测图片是否存在
const imageIsExist = (url) => {
  return new Promise((resolve) => {
    let img = new Image()
    img.onload = function () {
      if (this.complete === true) {
        resolve(true)
        img = null
      }
    }
    img.onerror = function () {
      resolve(false)
      img = null
    }
    img.src = url
  })
}

接下啦就是自定义指令了:

Vue.directive('error-img', async (el, binding) => {
  if (await imageIsExist(el.src)) return
  const imgURL = binding.value // 获取图片地址
  if (imgURL) {
    const exist = await imageIsExist(imgURL)
    if (exist) {
      el.setAttribute('src', imgURL)
    } else {
      const icon = document.createElement('i')
      icon.setAttribute('class', 'el-icon-picture-outline')
      el.parentNode.appendChild(icon)
      el.parentNode.removeChild(el)
    }
  } else {
    el.setAttribute('src', require('@/assets/image/index-logo.png'))
  }
})

require('@/assets/image/index-logo.png')为默认图片,当v-error-img没有绑定任何值时展示,<i class="el-icon-picture-outline"></i>为element-ui图标,当默认图加载失败时替换为原来的img标签

image-view组件

思考:无论是正常图片加载还是默认图片加载都是同样的过程,即:能加载:则直接展示;不能加载出来,则展示默认的;因此,可以做成一下递归组件:

<template>
  <el-image
    class="img-size__inherit "
    v-bind="$attrs"
    v-on="$listeners"
    :src="src"
  >
    <slot name="placeholder" slot="placeholder">
      <div class="placeholder-view" >加载中...</div>
    </slot>
    <slot name="error" slot="error">
      <!-- 默认图 -->
      <image-view
        class="img-size__inherit"
        fit="contain"
        :src="defaultSrc"
      >
        <!-- 默认图也没加载出的占位 -->
        <div class="placeholder-view" slot="error"><i class="el-icon-picture-outline"></i></div>
      </image-view>
    </slot>
  </el-image>
</template>
<script>
  export default {
    name: 'image-view',
    props: {
      // 图片路径
      src: {
         type: [String, undefined, null]
      },
      // 默认占位图
      defaultSrc: {
        type: String,
        default: require('@/assets/image/index-logo.png')
      }
    }
  }
</script>

这样的话,是否比自定义指令理解起来更简单呢

image-view组件升级

但是产品经理又有自己的想法了:能否在需要的地方移到图片有放大的效果呢

这时候我们不假思索:这个简单,只要复制一份放大的img,初始为display:none,当hover后则display:block显示出来即可。那么问题来了

  1. 这个hover后的大图应该放组件哪里呢

  2. 是否需要区分是初始图hover还是默认图hover的呢

首先hover图应该是和初始图片同级的,其次,无论是默认图还是初始图,我们都可以把hover作用在最外层的元素上,因此无需区分是初始图hover还是默认图hover

这时候我们又想到,初始图,大图加载过程都是同样的,即:能加载:则直接展示;不能加载出来,则展示默认的;因此我们也可以把大图的展示也用这个组件:

<template>
  <div class="img-relative img-size__inherit">
    <el-image
      class="img-size__inherit "
      v-bind="$attrs"
      v-on="$listeners"
      :src="src"
    >
      <slot name="placeholder" slot="placeholder">
        <div class="placeholder-view" >加载中...</div>
      </slot>
      <slot name="error" slot="error">
        <!-- 默认图 -->
        <image-view
          class="img-size__inherit"
          fit="contain"
          :hover-preview="false"
          :src="defaultSrc"
        >
          <!-- 默认图也没加载出的占位 -->
          <div class="placeholder-view" slot="error"><i class="el-icon-picture-outline"></i></div>
        </image-view>
      </slot>
    </el-image>
    <!-- hover后的大图展示图片 -->
    <image-view
      v-if="hoverPreview"
      class="img-preview"
      :hover-preview="false"
      :src="src"
      :style="hoverPreviewStyle"
    >
    </image-view>
  </div>
</template>
<script>
export default {
  name: 'image-view',
  props: {
    // 图片路径
    src: {
       type: [String, undefined, null]
    },
    // 默认占位图
    defaultSrc: {
      type: String,
      default: require('@/assets/image/index-logo.png')
    },
    // 是否开启hover展示大图效果
    hoverPreview: {
      type: Boolean,
      default: false
    },
    // hover后大图的样式,建议只传width
    hoverPreviewStyle: {
      type: [Object, String],
      default: ''
    }
  }
}
</script>
<style lang="less" scoped>
.img-size__inherit {
  width: inherit;
  height: inherit;
}

.img-relative {
  display: inline-block;
}

.img-relative > .img-preview {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 1999;
  display: none;
  width: auto;
  max-width: 60vw;
  height: auto;
  max-height: 60vh;
  background: #fff;
  transform: translate(-50%, -50%);
}

.img-relative:hover > .img-preview {
  display: block;
}

.placeholder-view {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: #eee;
}
</style>

image-view组件完善

如果说需要通过外界对图片路径做处理的话,还可以把对路径处理的方法通过props传入,因此,最终优化版的image-view组件为:

<template>
  <div class="img-relative img-size__inherit">
    <el-image
      class="img-size__inherit "
      v-bind="$attrs"
      v-on="$listeners"
      :src="usePathHandler ? pathHandler(src) : src"
    >
      <slot name="placeholder" slot="placeholder">
        <div class="placeholder-view" >加载中...</div>
      </slot>
      <slot name="error" slot="error">
        <!-- 默认图 -->
        <image-view
          class="img-size__inherit"
          fit="contain"
          :usePathHandler="false"
          :src="defaultSrc"
        >
          <!-- 默认图也没加载出的占位 -->
          <div class="placeholder-view" slot="error"><i class="el-icon-picture-outline"></i></div>
        </image-view>
      </slot>
    </el-image>
    <!-- hover后的大图展示图片 -->
    <image-view
      v-if="hoverPreview"
      class="img-preview"
      :src="src"
      :style="hoverPreviewStyle"
      :usePathHandler="usePathHandler"
      :defaultSrc="defaultSrc"
      :pathHandler="pathHandler"
    >
    </image-view>
  </div>
</template>

<script>
import ImageView from './ImageView.vue'
import { imgPathHandler } from '@/utils/util' // 图片路径处理的默认方法

/**
 * 图片视图(拥有默认占位图)
 * 本组件为对 el-image 的封装, 可传入任意 el-image 的 props 和 listener
 * 
 * slot[ placeholder ]: 加载占位
 * slot[ error ]: 错误占位
 */
export default {
  name: 'image-view',
  components: { ImageView },
  props: {
    // 图片路径
    src: {
      type: [String, undefined, null]
    },
    // 默认占位图
    defaultSrc: {
      type: String,
      default: require('@/assets/image/index-logo.png')
    },
    // 是否使用路径处理器
    usePathHandler: {
      type: Boolean,
      default: true
    },
    // 路径处理器
    pathHandler: {
      type: Function,
      default: imgPathHandler
    },
    // 是否开启hover展示大图效果
    hoverPreview: {
      type: Boolean,
      default: false
    },
    // hover后大图的样式,建议只传width
    hoverPreviewStyle: {
      type: [Object, String],
      default: ''
    }
  }
}
</script>

<style lang="less" scoped>
.img-size__inherit {
  width: inherit;
  height: inherit;
}

.img-relative {
  display: inline-block;
}

.img-relative > .img-preview {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 1999;
  display: none;
  width: auto;
  max-width: 60vw;
  height: auto;
  max-height: 60vh;
  background: #fff;
  transform: translate(-50%, -50%);
}

.img-relative:hover > .img-preview {
  display: block;
}

.placeholder-view {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: #eee;
}
</style>

实现的效果如下: