el-avatar

2 阅读7分钟

一、加注释后的源码全文

<script>
export default {
  // 组件名,用于递归组件、devtools 里显示
  name: 'ElAvatar',
  // 定义组件的对外属性(props)
  props: {
    /**
     * 头像大小:
     * - 字符串类型:只允许 'large' | 'medium' | 'small'
     * - 数字类型:自定义像素值,如 40
     */
    size: {
      type: [Number, String],
      // 自定义验证器
      validator(val) {
        // 如果是字符串,必须是预设的三个尺寸之一
        if (typeof val === 'string') {
          return ['large', 'medium', 'small'].includes(val);
        }
        // 如果是数字,直接通过(任意数字)
        return typeof val === 'number';
      }
    },
    /**
     * 头像形状:
     * - circle:圆形
     * - square:方形
     */
    shape: {
      type: String,
      default: 'circle',
      validator(val) {
        return ['circle', 'square'].includes(val);
      }
    },
    /**
     * 图标类名,用于展示图标头像
     * 示例:icon="el-icon-user-solid"
     */
    icon: String,
    /**
     * 图片地址,用于展示图片头像
     * 示例:src="https://example.com/avatar.jpg"
     */
    src: String,
    /**
     * 图片的 alt 属性,提升可访问性
     */
    alt: String,
    /**
     * 图片的 srcset 属性,用于响应式图片
     */
    srcSet: String,
    /**
     * 图片加载失败时的回调函数
     * 返回 false 可以阻止组件内置的“隐藏图片”行为
     */
    error: Function,
    /**
     * 图片的 object-fit 样式,和 CSS object-fit 一致
     * 默认 'cover':保持比例填充容器,可能裁剪
     */
    fit: {
      type: String,
      default: 'cover'
    }
  },
  // 组件内部数据
  data() {
    return {
      /**
       * 标记当前图片是否加载成功
       * - true:显示 <img>
       * - false:隐藏 <img>,回退到 icon 或 slot
       */
      isImageExist: true
    };
  },
  computed: {
    /**
     * 计算最终要渲染在根元素上的 class 字符串
     * 根据不同的 size / shape / icon 拼接对应的类名
     */
    avatarClass() {
      const { size, icon, shape } = this;
      // 基础类名
      let classList = ['el-avatar'];
      // 如果 size 是字符串预设值,拼接尺寸修饰类
      if (size && typeof size === 'string') {
        classList.push(`el-avatar--${size}`);
      }
      // 如果传了 icon,说明是图标头像,添加图标修饰类
      if (icon) {
        classList.push('el-avatar--icon');
      }
      // 根据 shape 添加形状修饰类
      if (shape) {
        classList.push(`el-avatar--${shape}`);
      }
      // 拼接成字符串返回
      return classList.join(' ');
    }
  },
  methods: {
    /**
     * 处理图片加载失败逻辑
     * 1. 调用用户传入的 error 回调
     * 2. 根据返回值决定是否将 isImageExist 置为 false
     */
    handleError() {
      const { error } = this;
      // 如果用户传了 error 函数,执行它;否则 errorFlag 为 undefined
      const errorFlag = error ? error() : undefined;
      // 只有当 errorFlag !== false 时,才隐藏图片
      // 这样用户可以通过 error() 返回 false 来阻止“隐藏图片”的默认行为
      if (errorFlag !== false) {
        this.isImageExist = false;
      }
    },
    /**
     * 真正渲染头像内容的函数
     * 返回一个 VNode,在 render() 中被使用
     * 优先级:
     *   1. 图片存在且 src 有值 → <img>
     *   2. 否则,如果有 icon → <i class="icon">
     *   3. 否则,使用默认插槽内容
     */
    renderAvatar() {
      const { icon, src, alt, isImageExist, srcSet, fit } = this;
      // 情况 1:图片存在且 src 有值 → 渲染 <img>
      if (isImageExist && src) {
        return (
          <img
            src={src}
            onError={this.handleError} // 图片加载失败时触发
            alt={alt}
            srcSet={srcSet}
            // 使用 JSX 方式设置 style 对象
            style={{ 'object-fit': fit }}
          />
        );
      }
      // 情况 2:没有图片,但有 icon → 渲染图标
      if (icon) {
        return (<i class={icon} />);
      }
      // 情况 3:既没有图片,也没有 icon → 使用默认插槽
      return this.$slots.default;
    }
  },
  /**
   * 渲染函数
   * - 返回一个 <span> 作为根节点
   * - 根据 size 是数字时,设置行内样式(高、宽、行高)
   * - 内部调用 renderAvatar() 渲染具体内容
   */
  render() {
    const { avatarClass, size } = this;
    // 如果 size 是数字,生成一个包含尺寸样式的对象
    const sizeStyle = typeof size === 'number' ? {
      height: `${size}px`,
      width: `${size}px`,
      lineHeight: `${size}px`
    } : {};
    return (
      <span class={ avatarClass } style={ sizeStyle }>
        {
          this.renderAvatar()
        }
      </span>
    );
  }
};
</script>

二、从整体到细节

1. 组件整体在做什么?

一句话概括:

ElAvatar 是一个“头像容器”,根据 props 决定里面放:

  • 图片 <img>
  • 图标 <i>
  • 或者任意插槽内容(文字、其他组件) 并通过 size / shape / fit 等属性控制外观。 从渲染流程看,逻辑大致如下:
flowchart LR
  A[开始渲染 ElAvatar] --> B{有 src 且 isImageExist?}
  B -- 有 --> C[渲染 img]
  B -- 无 --> D{有 icon?}
  D -- 有 --> E[渲染 i.icon]
  D -- 无 --> F[渲染 this.$slots.default]

2. class 类名的作用

源码里通过 avatarClass 拼接出来的类名,主要依赖 CSS:

classList = ['el-avatar'];
if (size && typeof size === 'string') {
  classList.push(`el-avatar--${size}`);
}
if (icon) {
  classList.push('el-avatar--icon');
}
if (shape) {
  classList.push(`el-avatar--${shape}`);
}

在 Element 的样式文件中,这些类大致做了这些事(结合官方文档和 SCSS 源码):

类名作用(简化理解)
el-avatar基础类:设置 display: inline-block、基础尺寸、字体大小、文字居中、背景色等
el-avatar--large大尺寸预设:宽高变大,通常用于“大头像”
el-avatar--medium中等尺寸
el-avatar--small小尺寸
el-avatar--circle圆形:border-radius: 50%
el-avatar--square方形:border-radius 较小或无
el-avatar--icon当内容是图标时,调整图标大小、垂直居中

重点记忆:

  • 组件内部不直接写死尺寸,而是通过类名 + 预设尺寸,好处是统一风格、方便主题定制。
  • 如果 size 是数字,则不走预设类,而是用行内样式写死宽高。

3. data 中数据的作用

源码:

data() {
  return {
    isImageExist: true
  };
}
  • 作用:标记当前图片是否“存在/加载成功”。
  • 初始为 true,表示假设图片一开始是好的。
  • <img> 触发 onError 时,调用 handleError,如果 error() 没有返回 false,就把 isImageExist 置为 false,从而在 renderAvatar 中不再渲染 <img>重点理解:
  • 这是“受控状态”:组件内部维护一个“图片是否可用”的标志,再结合用户传入的 error 回调,决定是否回退到 icon 或默认插槽。
  • 一个典型场景:
    • 传入 src 加载失败 → 回退到 icon 或文字。
    • 如果用户 error() 返回 false,则组件继续保留 <img>(用户可能想自己处理错误,比如显示默认占位图)。

4. computed 方法详解:avatarClass

computed: {
  avatarClass() {
    const { size, icon, shape } = this;
    let classList = ['el-avatar'];
    if (size && typeof size === 'string') {
      classList.push(`el-avatar--${size}`);
    }
    if (icon) {
      classList.push('el-avatar--icon');
    }
    if (shape) {
      classList.push(`el-avatar--${shape}`);
    }
    return classList.join(' ');
  }
}

逐行解释:

  1. this 中解构出 sizeiconshape
  2. 初始 classList = ['el-avatar'],这是所有头像都有的基础类。
  3. 如果 size 是字符串,说明是预设尺寸(large | medium | small),拼接对应尺寸类。
  4. 如果有 icon,说明是图标头像,加 el-avatar--icon 类,方便样式针对图标做调整。
  5. 如果有 shape,拼接形状类 circle / square难点 / 重点:
  • 这里 没有处理数字 size,数字尺寸是在 render() 中用 sizeStyle 行内样式实现的。
  • 这也是很多 UI 组件库的常见设计:
    • 预设尺寸 → 类名
    • 自定义尺寸 → 行内样式 记忆点:
  • avatarClass 只负责“类名逻辑”,不负责“尺寸计算”。
  • 以后你自己在写组件时,也可以模仿:
    • 一部分样式用类名(主题化、可复用),
    • 一部分精确尺寸用行内样式。

5. methods 详解:handleError & renderAvatar

5.1 handleError

methods: {
  handleError() {
    const { error } = this;
    const errorFlag = error ? error() : undefined;
    if (errorFlag !== false) {
      this.isImageExist = false;
    }
  }
}

逻辑拆解:

  1. 取出用户传入的 error 函数。
  2. 如果有,执行 error();没有,errorFlagundefined
  3. 只有当 errorFlag !== false 时,才设置 isImageExist = false关键点:
  • 用户可以通过 error 返回 false,阻止组件隐藏图片。
  • 默认行为:加载失败 → isImageExist = false → 不再渲染 <img>典型用法示例:
<template>
  <el-avatar
    :src="avatarUrl"
    :error="handleAvatarError"
  />
</template>
<script>
export default {
  data() {
    return {
      avatarUrl: 'invalid-url.jpg'
    };
  },
  methods: {
    handleAvatarError() {
      console.log('头像加载失败,使用默认头像');
      // 返回 false,阻止 el-avatar 内部隐藏图片的行为
      return false;
    }
  }
};
</script>

5.2 renderAvatar

renderAvatar() {
  const { icon, src, alt, isImageExist, srcSet, fit } = this;
  if (isImageExist && src) {
    return <img
      src={src}
      onError={this.handleError}
      alt={alt}
      srcSet={srcSet}
      style={{ 'object-fit': fit }}/>;
  }
  if (icon) {
    return (<i class={icon} />);
  }
  return this.$slots.default;
}

逻辑拆解:

  1. 解构出需要的属性。
  2. 如果 isImageExist 为真且 src 有值 → 渲染 <img>
    • onError 绑定 handleError,处理加载失败。
    • style={{ 'object-fit': fit }} 控制图片在容器内的填充方式,和 CSS object-fit 一致。
  3. 否则,如果有 icon → 渲染 <i class="icon">
  4. 否则,返回 this.$slots.default,即默认插槽内容。 重点 & 难点:
  • 优先级:图片 > 图标 > 插槽
  • 这里用的是 JSX 写法,在 Vue 2 中需要配置 babel-plugin-transform-vue-jsx
  • srcSet 是原生 <img> 属性,用于根据设备像素密度选择不同分辨率图片。 需要记忆:
  • 如果需要“根据条件渲染不同内容”,可以模仿这种“函数返回 VNode”的模式:
    • 先写一个 renderXxx() 方法,
    • render()<template> 中调用。

6. render() 函数详解

render() {
  const { avatarClass, size } = this;
  const sizeStyle = typeof size === 'number' ? {
    height: `${size}px`,
    width: `${size}px`,
    lineHeight: `${size}px`
  } : {};
  return (
    <span class={ avatarClass } style={ sizeStyle }>
      {
        this.renderAvatar()
      }
    </span>
  );
}

逐行解释:

  1. this 中取出 avatarClass(computed)和 size(prop)。
  2. 如果 size 是数字,生成一个包含 heightwidthlineHeight 的样式对象。
    • 注意:lineHeight 也设置成相同数值,保证文字内容垂直居中。
  3. 如果 size 不是数字,sizeStyle 为空对象,只使用类名预设尺寸。
  4. 返回一个 <span>,根节点上:
    • classavatarClass
    • stylesizeStyle
    • 内部调用 this.renderAvatar() 渲染具体内容。 重点:
  • size 为数字时,使用行内样式覆盖预设尺寸,这样更灵活。
  • 根节点是 <span>,所以默认是行内块元素,适合放在文字旁边。

7. 和官方 API 的对应关系

结合 Element 官方文档(Element Plus 的 Avatar API 与 Element UI 基本一致):

官方属性源码中的体现说明
iconicon: String + renderAvatar<i class={icon}>图标类名
sizesize: [Number, String] + avatarClass / sizeStyle支持预设字符串或数字像素
shapeshape: String + avatarClassel-avatar--${shape}circle / square
srcsrc: String + <img src={src}>图片地址
src-setsrcSet: String + <img srcSet={srcSet}>原生 srcset
altalt: String + <img alt={alt}>可访问性
fitfit: String + style={{ 'object-fit': fit }}图片填充方式
errorerror: Function + handleError图片加载失败回调

8. 重点、难点、需要记忆的部分总结

8.1 重点

  1. 三个渲染优先级:图片 > 图标 > 插槽
    • 这是 renderAvatar 的核心逻辑,一定要记住。
  2. size 的两种处理方式
    • 字符串预设尺寸 → 类名 el-avatar--large/medium/small
    • 数字自定义尺寸 → 行内样式 height / width / lineHeight
  3. isImageExist 的作用
    • 控制图片是否渲染,配合 error 回调实现“错误回退”逻辑。

8.2 难点

  1. JSX 写法
    • 如果你不熟悉 JSX,可能对 <img ... /><i class={icon} /> 感到陌生。
    • 建议:先在项目中配置好 JSX,然后自己写几个简单组件练手。
  2. error 回调与默认行为的交互
    • 用户可以通过返回 false 阻止组件内部隐藏图片。
    • 这是“组件默认行为 + 用户自定义控制”的典型设计模式。

8.3 需要记忆的代码模式

  1. 类名拼接模式
    const classList = ['el-avatar'];
    if (size && typeof size === 'string') {
      classList.push(`el-avatar--${size}`);
    }
    if (icon) {
      classList.push('el-avatar--icon');
    }
    if (shape) {
      classList.push(`el-avatar--${shape}`);
    }
    return classList.join(' ');
    
    后面很多组件都会用到这种“基础类 + 条件修饰类”的模式。
  2. 条件渲染 + 回退逻辑
    if (isImageExist && src) {
      return <img ... onError={this.handleError} />;
    }
    if (icon) {
      return <i class={icon} />;
    }
    return this.$slots.default;
    
    这是“优先级渲染”的经典写法。
  3. 数字尺寸生成行内样式
    const sizeStyle = typeof size === 'number' ? {
      height: `${size}px`,
      width: `${size}px`,
      lineHeight: `${size}px`
    } : {};
    

如果你愿意,下一步可以:

  • 我可以帮你写一个“简化版 ElAvatar”,把 JSX 改成 <template> 版本,方便你在 Vue 2 项目中直接使用。
  • 或者我们再往下挖一层:看 Element 的 avatar.scss 具体样式实现,学习它是如何用 CSS 实现各种状态和尺寸的。