likes
comments
collection
share

Element Plus 源码阅读 Container 组件(基础组件)前言 在平时使用Vue进行管理系统开发时候,我们

作者站长头像
站长
· 阅读数 48

前言

在平时使用Vue进行管理系统开发时候,我们都会多多少少使用到Element Plus这个组件库,并且针对其中的组件进行二次开发,但是很多时候我们只是使用这个组件库,并不了解其中组件封装的逻辑与技巧,出于学习的目的,本篇主要结合Container组价进行源码解读,学习优秀组件的封装技巧。

源代码

Container组件

入口文件

packages/components/container/index.ts

import { withInstall, withNoopInstall } from '@element-plus/utils'

import Container from './src/container.vue'
import Aside from './src/aside.vue'
import Footer from './src/footer.vue'
import Header from './src/header.vue'
import Main from './src/main.vue'
import type { SFCWithInstall } from '@element-plus/utils'

export const ElContainer: SFCWithInstall<typeof Container> & {
  Aside: typeof Aside
  Footer: typeof Footer
  Header: typeof Header
  Main: typeof Main
} = withInstall(Container, {
  Aside,
  Footer,
  Header,
  Main,
})

export default ElContainer
export const ElAside: SFCWithInstall<typeof Aside> = withNoopInstall(Aside)
export const ElFooter: SFCWithInstall<typeof Footer> = withNoopInstall(Footer)
export const ElHeader: SFCWithInstall<typeof Header> = withNoopInstall(Header)
export const ElMain: SFCWithInstall<typeof Main> = withNoopInstall(Main)

export type ContainerInstance = InstanceType<typeof Container>
export type AsideInstance = InstanceType<typeof Aside>
export type FooterInstance = InstanceType<typeof Footer>
export type HeaderInstance = InstanceType<typeof Header>
export type MainInstance = InstanceType<typeof Main>

在这个入口文件中,我们将着重讲解withInstall, withNoopInstall这两个工具方法,withInstall用来将一个 Vue 组件进行包装,使其具备安装插件的能力,并且还能附带一些额外的组件或属性。通过这个函数为Vue组件添加 install 方法,使其能够通过 app.use() 方法安装。

export type SFCWithInstall<T> = T & Plugin

将 Vue 组件类型 T 扩展为具有插件功能的类型

export const withInstall = <T, E extends Record<string, any>>(
  main: T,
  extra?: E
) => {
  ;(main as SFCWithInstall<T>).install = (app): void => {
    for (const comp of [main, ...Object.values(extra ?? {})]) {
      app.component(comp.name, comp)
    }
  }

  if (extra) {
    for (const [key, comp] of Object.entries(extra)) {
      ;(main as any)[key] = comp
    }
  }
  return main as SFCWithInstall<T> & E
}
export const withNoopInstall = <T>(component: T) => {
  ;(component as SFCWithInstall<T>).install = NOOP

  return component as SFCWithInstall<T>
}

withNoopInstall 是一个函数,它的作用是为一个组件添加一个“空操作”(NOOP)的 install 方法,这样这个组件就可以被 Vue 的插件系统识别为一个插件,但实际上不会执行任何安装操作。

container.vue组件代码

<template>
  <section :class="[ns.b(), ns.is('vertical', isVertical)]">
    <slot />
  </section>
</template>
<script lang="ts" setup>
import { computed, useSlots } from 'vue'
import { useNamespace } from '@element-plus/hooks'

import type { Component, VNode } from 'vue'

defineOptions({
  name: 'ElContainer',
})
const props = defineProps({
  /**
   * @description layout direction for child elements
   */
  direction: {
    type: String,
  },
})
const slots = useSlots()

const ns = useNamespace('container')

const isVertical = computed(() => {
  if (props.direction === 'vertical') {
    return true
  } else if (props.direction === 'horizontal') {
    return false
  }
  if (slots && slots.default) {
    const vNodes: VNode[] = slots.default()
    return vNodes.some((vNode) => {
      const tag = (vNode.type as Component).name
      return tag === 'ElHeader' || tag === 'ElFooter'
    })
  } else {
    return false
  }
})
</script>

其中ns是useNameSpace hook返回的一个对象,对象中保存着一个个函数方法,主要生成一个 BEM(Block Element Modifier)样式类名,以下是useNameSpace的源代码

export const useNamespace = (
  block: string,
  namespaceOverrides?: Ref<string | undefined>
) => {
  const namespace = useGetDerivedNamespace(namespaceOverrides)
  const b = (blockSuffix = '') =>
    _bem(namespace.value, block, blockSuffix, '', '')
  const e = (element?: string) =>
    element ? _bem(namespace.value, block, '', element, '') : ''
  const m = (modifier?: string) =>
    modifier ? _bem(namespace.value, block, '', '', modifier) : ''
  const be = (blockSuffix?: string, element?: string) =>
    blockSuffix && element
      ? _bem(namespace.value, block, blockSuffix, element, '')
      : ''
  const em = (element?: string, modifier?: string) =>
    element && modifier
      ? _bem(namespace.value, block, '', element, modifier)
      : ''
  const bm = (blockSuffix?: string, modifier?: string) =>
    blockSuffix && modifier
      ? _bem(namespace.value, block, blockSuffix, '', modifier)
      : ''
  const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
    blockSuffix && element && modifier
      ? _bem(namespace.value, block, blockSuffix, element, modifier)
      : ''
  const is: {
    (name: string, state: boolean | undefined): string
    (name: string): string
  } = (name: string, ...args: [boolean | undefined] | []) => {
    const state = args.length >= 1 ? args[0]! : true
    return name && state ? `${statePrefix}${name}` : ''
  }

  // for css var
  // --el-xxx: value;
  const cssVar = (object: Record<string, string>) => {
    const styles: Record<string, string> = {}
    for (const key in object) {
      if (object[key]) {
        styles[`--${namespace.value}-${key}`] = object[key]
      }
    }
    return styles
  }
  // with block
  const cssVarBlock = (object: Record<string, string>) => {
    const styles: Record<string, string> = {}
    for (const key in object) {
      if (object[key]) {
        styles[`--${namespace.value}-${block}-${key}`] = object[key]
      }
    }
    return styles
  }

  const cssVarName = (name: string) => `--${namespace.value}-${name}`
  const cssVarBlockName = (name: string) =>
    `--${namespace.value}-${block}-${name}`

  return {
    namespace,
    b,
    e,
    m,
    be,
    em,
    bm,
    bem,
    is,
    // css
    cssVar,
    cssVarName,
    cssVarBlock,
    cssVarBlockName,
  }
}

组件测试

describe('Container.vue', () => {
  test('container render test', async () => {
    const wrapper = mount(() => <Container>{AXIOM}</Container>)
    expect(wrapper.text()).toEqual(AXIOM)
  })

  test('vertical', () => {
    const wrapper = mount(() => (
      <Container>
        <Header />
        <Main />
      </Container>
    ))
    expect(wrapper.classes('is-vertical')).toBe(true)
  })

  test('direction', () => {
    const wrapper = mount({
      data: () => ({ direction: 'horizontal' }),
      render() {
        return (
          <Container direction={this.direction}>
            <Header />
            <Main />
          </Container>
        )
      },
    })

    expect(wrapper.vm.$el.classList.contains('is-vertical')).toBe(false)
    wrapper.vm.direction = 'vertical'
    wrapper.vm.$nextTick(() => {
      expect(wrapper.vm.$el.classList.contains('is-vertical')).toBe(true)
    })
  })
})

主要是用vitest进行组件的测试,使用test函数编写一个一个测试用例进行测试, 比如说

test('container render test', async () => { const wrapper = mount(() => <Container>{AXIOM}</Container>) expect(wrapper.text()).toEqual(AXIOM) })

在这个测试用例中我们主要测试container的渲染,挂载一个内容为AXIOM的container组件,然后expect(wrapper.text()).toEqual(AXIOM)是期望的结果,test函数返回布尔值。

总结

element-plus组件在对组件类型以及参数做了处理,对ts具有很好的支持,阅读组件源码需了解基础的ts语法,尤其是参数类型的定义,类型推导,断言等ts常识

转载自:https://juejin.cn/post/7408037561237028875
评论
请登录