【源码共读】| vue react 小程序 message 组件


theme: vuepress
highlight: vs2015

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

【若川视野 x 源码共读】第28期 | vue react 小程序 message 组件 点击了解本期详情一起参与

今天阅读的是:组件库的message组件

  • element
    • https://github.com/ElemeFE/element/tree/dev/packages/message
  • element plus
    • https://github.com/element-plus/element-plus/tree/dev/packages/components/message
  • antd
    • https://github.com/ant-design/ant-design/tree/master/components/message
  • antd vue
    • https://github.com/vueComponent/ant-design-vue/tree/main/components/message

相信使用组件库的对于element uiantd这两个库都很熟悉,那么,他们的message组件是怎么实现的呢?

实际用例

image-20221123141010976

image-20221123141052110

源码分析

  • 查看测试用例

  • 我们以element plus作为源码分析,跳转到对应的package,看下目录结构

image-20221123150240717

测试用例

跳转至测试用例,查看需要实现的功能点

// 渲染
describe('render', () => {
    // 基本渲染
    test('basic render test', () => {
    })
    // 支持vnode渲染
    test('should be able to render VNode', () => {
    })
    // 当 dangerouslyUseHTMLString = true 渲染html元素
    test('should be able to render raw HTML with dangerouslyUseHTMLString prop', () => {
    })
    // 当 dangerouslyUseHTMLString = false | undefined 时,不渲染html元素
    test('should not be able to render raw HTML without dangerouslyUseHTMLString prop', () => {
    })
})
// 消息类型
describe('Message.type', () => {
    // 检查几种类型的message 是否正常渲染
    test('should be able to render typed messages', () => {
    })
    // 当输入不合法的icon,不渲染
    test('should not be able to render invalid type icon', () => {
    })
})

describe('event handlers', () => {
    // 点击关闭按钮正常关闭
    test('it should be able to close the message by clicking close button', async () => {
    })
    // 延迟关闭
    test('it should close after duration', async () => {
    })
    // 当悬停时阻止关闭
    test('it should prevent close when hovered', async () => {
    })
    // 当duration设置为 0 时,不会自动关闭
    test('it should not close when duration is set to 0', () => {
    })
    // 当点击 esc 键,消息弹窗关闭
    test('it should close when esc is pressed', async () => {
    })
    // 触发close事件
    test('it should call close after transition ends', async () => {
    })
})

根据测试用例,我们可以得到几个功能点

  • 渲染

    • 基本渲染
    • 支持vnode
    • 渲染html元素
  • 消息类型

    • ‘success’ | ‘warning’ | ‘info’ | ‘error’
  • 事件

    • 关闭
    • 延迟关闭
    • 悬停不消失

源码实现

// method.ts

// 出口
export default message as Message

所以,我们需要查看的是message的构造方法

const message: MessageFn &
      Partial<Message> & { _context: AppContext | null } = (
          options = {},
          context
      ) => {
          // 检查window是否存在
          if (!isClient) return { close: () => undefined }

          // 检查max数量的类型和当前的实例数是否超出配置项最大数量
          if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
              return { close: () => undefined }
          }
          // 规范化配置选项
          const normalized = normalizeOptions(options)

          // 单例模式
          if (normalized.grouping && instances.length) {
              const instance = instances.find(
                  ({ vnode: vm }) => vm.props?.message === normalized.message
              )
              if (instance) {
                  instance.props.repeatNum += 1
                  instance.props.type = normalized.type
                  return instance.handler
              }
          }
          // 如果不存在则创建实例,并将当前的Message加入到数组中
          const instance = createMessage(normalized, context)

          instances.push(instance)
          return instance.handler
      }
const createMessage = (
    { appendTo, ...options }: MessageParamsNormalized,
    context?: AppContext | null
): MessageContext => {
    // 计算zindex
    const { nextZIndex } = useZIndex()

    // 设置唯一id
    const id = `message_${seed++}`
    // 初始化onClose
    const userOnClose = options.onClose
    // 创建容器
    const container = document.createElement('div')
    // 参数定义
    const props = {
        ...options,
        // 设置每个消息框的偏移量,保证不重叠
        zIndex: nextZIndex() + options.zIndex,
        id,
        onClose: () => {
            userOnClose?.()
            closeMessage(instance)
        },

        // clean message element preventing mem leak
        onDestroy: () => {
            // 当元素销毁时,VNode应该会被收集释放内存,但是为了避免内存泄露,手动将当前的vm设置为null
            render(null, container)
        }
    }
    // 创建vnode节点
    const vnode = createVNode(
        MessageConstructor,
        props,
        isFunction(props.message) || isVNode(props.message)
        ? {
            default: isFunction(props.message)
            ? props.message
            : () => props.message
        }
        : null
    )
    // 绑定当前context
    vnode.appContext = context || message._context

    render(vnode, container)
    // instances will remove this item when close function gets called. So we do not need to worry about it.
    appendTo.appendChild(container.firstElementChild!)

    const vm = vnode.component!

          const handler: MessageHandler = {
              // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle
              // for out component, so that all closing steps will not be skipped.
              close: () => {
                  vm.exposed!.visible.value = false
              }
          }

          const instance: MessageContext = {
              id,
              vnode,
              vm,
              handler,
              props: (vnode.component as any).props
          }
          // 返回当前实例
          return instance
}

那么,message中不同类型的消息框是怎么实现的呢

// message.ts
export const messageTypes = ['success', 'info', 'warning', 'error'] as const

export type messageType = typeof messageTypes[number]
// method.ts
// 创建不同类型的message
messageTypes.forEach((type) => {
    message[type] = (options = {}, appContext) => {
        const normalized = normalizeOptions(options)
        return message({ ...normalized, type }, appContext)
    }
})

总结

太久没更新文章了,赶紧练练手

通过上述分析,我们可以知道message是怎么创建出来的,并且对应测试用例的功能点是如何实现的。

  • 同一时间应该只存在一个message,使用单例模式
  • vnode 本身支持html元素和text,所以可以实现html元素渲染
  • 巧妙的是,使用了forEach来创建不同类型的message
  • 对元素销毁时,可能发生内存泄露的边界条件的考虑
  • 测试用例的编写
© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容