Element Plus 源码阅读 Container 组件(基础组件)前言 在平时使用Vue进行管理系统开发时候,我们
前言
在平时使用Vue进行管理系统开发时候,我们都会多多少少使用到Element Plus这个组件库,并且针对其中的组件进行二次开发,但是很多时候我们只是使用这个组件库,并不了解其中组件封装的逻辑与技巧,出于学习的目的,本篇主要结合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