install.js

/**
 * @file vue指令插件-图片加载器
 */

import validation from '@~lisfan/validation'
import VueLogger from '@~lisfan/vue-logger'

import ImageElementShell from './image-element-shell'

const PLUGIN_TYPE = 'directive'  // 插件类型
const DIRECTIVE_NAMESPACE = 'image-loader' // 指令名称

// 私有方法
const _actions = {
  /**
   * 计算宽高值
   *
   * @since 1.2.0
   *
   * @param {object} binding - 指令对象
   * @param {number} remRatio - rem与px的比例,默认值为100,表示1rem=100px
   *
   * @returns {object}
   */
  getSize(binding, remRatio) {
    // 获取自定义高宽值
    let sizeList = binding.arg
      ? binding.arg.split('x')
      : []

    // 只截取前两个的值
    let [width, height] = sizeList.slice(0, 2)

    // 高不存在时则同样的宽值
    height = height || width

    return {
      width: (width / remRatio ) + 'rem',
      height: (height / remRatio ) + 'rem',
    }
  },
  /**
   * 获取匹配到的占位图片
   *
   * @since 1.1.0
   *
   * @param {object} binding - 指令对象
   * @param {object} placeholders - 占位图片枚举集合
   *
   * @returns {string}
   */
  getPlaceholder(binding, placeholders) {
    let placeholderImage

    Object.keys(binding.modifiers).some((key) => {
      return (placeholderImage = placeholders[key])
    })

    return placeholderImage
  },
}

export default {
  /**
   * 图片加载器注册函数
   *
   * @since 1.1.0
   *
   * @function install
   *
   * @param {Vue} Vue - VUE类
   * @param {object} [options={}] - 配置选项
   * @param {string} [options.name='directive-image-loader'] - 日志打印器命名空间
   * @param {boolean} [options.debug=false] - 打印器调试模式是否开启
   * @param {number} [options.remRatio=100] - rem与px的比例,表示1rem=100px
   * @param {object} [options.placeholders={}] - 全局配置占位图片,key名会转换为修饰符
   * @param {string} [options.loadingPlaceholder=''] - 全局配置图片载入中占位图片
   * @param {number} [options.loadingDelay=300] - 载入中占位图片的延迟加载时间,避免出现载入中图片瞬间切换为真实图片的闪烁问题
   * @param {number} [options.animationClassName=''] - 动效的样式类
   * @param {boolean} [options.animate=true] - 是否启用动效载入,全局性动效开关,比如为了部分机型,可能会关闭动效的展示
   * @param {boolean} [options.force=false] - 是否强制开启每次指令绑定或更新进行动效展示。若关闭,则图片只在初次加载成功进行特效载入,之后不进行特效加载
   */
  install(Vue, {
    name = `${PLUGIN_TYPE}-${DIRECTIVE_NAMESPACE}`,
    debug = ImageElementShell.options.placeholders,
    remRatio = 100,
    placeholders = ImageElementShell.options.placeholders,
    loadingDelay = ImageElementShell.options.loadingDelay,
    loadingPlaceholder = ImageElementShell.options.loadingPlaceholder,
    animationClassName = ImageElementShell.options.animationClassName,
    animate = ImageElementShell.options.animate,
    force = ImageElementShell.options.force,
  } = {}) {
    /**
     * vue指令:image-loader
     * 该指令会从元素节点属性上读取以下值
     * - src - 读取原图片值,如果设置了该值,则loading-placeholder的设置将无效(为了应对一些场景而设立)
     * - image-src - 设置了图片'真正'需要加载的图片
     * - loading-placeholder - 设置了图片加载中的占位图片
     * - placeholder - 设置了当图片加载失败时,使用的占位图片。也可以不设置该值,而是通过读取指令的modifiers进行快捷指定全局配置的占用图片
     *
     * @since 1.2.0
     *
     * @function image-loader
     *
     * @param {string} [arg=false] - 参数图片宽度尺寸
     * @param {string} [value=false] - 动效样式
     * @param {object} [modifiers] - 修饰符对象,除了force值外,其他值都将当成占位符的快捷指定
     * @param {boolean} [modifiers.debug=false] - 是否启用单独启用调试日志
     * @param {boolean} [modifiers.animate=false] - 是否启用单独启用动效
     * @param {boolean} [modifiers.force=false] - 是否启用启用单独启用强制重新进行动效展示修饰符
     */
    Vue.directive(DIRECTIVE_NAMESPACE, {
      /**
       * 初始化绑定
       *
       * @since 1.0.0
       *
       * @ignore
       *
       * @param {element} $el - 目标dom元素
       * @param {object} binding - 指令对象
       * @param {VNode} vnode - Vue节点对象
       */
      bind($el, binding, vnode) {
        // 在目标节点上绑定该指令标识
        $el.setAttribute(`v-${DIRECTIVE_NAMESPACE}`, '')

        const vueLogger = new VueLogger({
          name,
          debug: binding.modifiers.debug || debug,
          vm: vnode.context,
        })

        vueLogger.log('emit bind hook!')

        // 因需要获取$el的属性,所以必须放在下一帧dom刷新才可以获取到样式
        Vue.nextTick().then(() => {

          // 设置目标元素的高宽
          const { width, height } = _actions.getSize(binding, remRatio)

          const shell = new ImageElementShell({
            debug: vueLogger.$debug,
            name: vueLogger.$name,
            el: $el,
            width: width || '',
            height: height || '',
            originSrc: $el.getAttribute('src') || '',
            placeholder: $el.getAttribute('placeholder') || _actions.getPlaceholder(binding, placeholders) || '',
            loadingPlaceholder: $el.getAttribute('loading-placeholder') || loadingPlaceholder || '',
            loadingDelay,
            originClassName: $el.getAttribute('class') || '',
            animationClassName: binding.value || animationClassName || '',
            animate: binding.modifiers.animate || animate,
            force: binding.modifiers.force || force,
          })

          const actualSrc = $el.getAttribute('image-src') || ''

          if (validation.isEmpty(actualSrc)) {
            // 若不存在真实图片地址,请求空白图片占位
            vueLogger.log('image src no existed, request placeholder image resource!')
            shell.load(shell.$placeholder)
          } else {
            // 若存在真实图片地址,请求空白图片占位
            vueLogger.log('image src existed, request image resource!')
            shell.load(actualSrc)
          }
        })
      },
      /**
       * 值进行了更新
       *
       * @since 1.0.0
       *
       * @ignore
       *
       * @param {element} $el - 目标dom元素
       * @param {object} binding - 指令对象
       * @param {VNode} vnode - Vue节点对象
       */
      update($el, binding, vnode) {
        const vueLogger = new VueLogger({
          name,
          debug: binding.modifiers.debug || debug,
          vm: vnode.context,
        })

        vueLogger.log('emit update hook!')

        // 获取绑定在节点上的shell实例
        const shell = $el._shell

        const actualSrc = $el.getAttribute('image-src') || ''

        // 当图片地址未变化时,则不进行处理
        if (shell.$actualSrc === actualSrc) {
          return
        }

        // 重新请求图片
        // 若强制启用了动效,则每次图片显示,都会执行动效
        vueLogger.log('image src updated, request image resource!')

        shell.load(actualSrc)
      }
    })
  }
}