文章目录
- index.ts
- Container
- header
- utils
- withInstall
- withNoopInstall
- hooks
- useNamespace
- 单元测试
看源码时候做的笔记。如有错误请指出!
关于路径的省略,详见button:【ElementPlus源码】Button按钮-CSDN博客
index.ts
导入一堆组件,导出ElContainer。
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'export const ElContainer = withInstall(Container, {Aside,Footer,Header,Main,
})
... // 不全
Container
判断是否垂直,先判断props中的direction属性。
若没有props.direction,判断插槽中是否有header和footer,有则返回true,代表垂直。
const isVertical = computed(() => {if (props.direction === 'vertical') {return true} else if (props.direction === 'horizontal') {return false}// 是否有ElHeader或ElFooter组件,有就为trueif (slots && slots.default) {const vNodes: VNode[] = slots.default()return vNodes.some((vNode) => {const tag = (vNode.type as Component).namereturn tag === 'ElHeader' || tag === 'ElFooter'})} else {return false}
})
表示垂直的样式会绑定在style中:
<section :class="[ns.b(), ns.is('vertical', isVertical)]"><slot /></section>
useNamespace是一个hook,详情写在博客hooks/useNamespace下。它返回一个对象,可以生成规范的类名。
const ns = useNamespace('container')
ns.is('vertical', isVertical)
生成的就是:isVertical
container只有direction属性。
header
header只有height属性,写在props中。style会计算height属性,最终将结果绑定到header上。
<template><header :class="ns.b()" :style="style"><slot /></header>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useNamespace } from '@element-plus/hooks'import type { CSSProperties } from 'vue'defineOptions({name: 'ElHeader',
})const props = defineProps({/*** @description height of the header*/height: {type: String,default: null,},
})const ns = useNamespace('header')
const style = computed(() => {return props.height? (ns.cssVarBlock({height: props.height,}) as CSSProperties): {}
})
</script>
aside、footer、main完全相似,不赘述。
utils
withInstall
传入一个main组件和extra,返回一个添加了install方法的main组件。
这个方法将main和extra中的所有属性注册到app中。
export const withInstall = <T, E extends Record<string, any>>(main: T, // 主要的 Vue 组件extra?: E // 可选的对象,其属性值是其他 Vue 组件
) => {/* 给 `main` 组件添加一个 `install` 方法这个方法接受一个 Vue 应用作为参数并将 `main` 组件以及 `extra` 中的所有组件注册到这个应用中*/;(main as SFCWithInstall<T>).install = (app): void => {for (const comp of [main, ...Object.values(extra ?? {})]) {app.component(comp.name, comp)}}// 将extra的属性添加到main上if (extra) {for (const [key, comp] of Object.entries(extra)) {;(main as any)[key] = comp}}// SFCWithInstall<T>表示带有 `install` 方法的 Vue 单文件组件// `E` 是 `extra` 的类型。return main as SFCWithInstall<T> & E
}
在container/index.ts
中调用:
返回了一个对象ElContainer ,有一个install方法,若调用install方法,则将Container、Aside、Footer、Header、Main五个组件注册到app上,并可以通过ElContainer.Container的方法访问Container。
export const ElContainer = withInstall(Container, {Aside,Footer,Header,Main,
})
withNoopInstall
创建一个带有空操作 install 方法的 Vue 组件对象
export const withNoopInstall = <T>(component: T) => {;(component as SFCWithInstall<T>).install = NOOPreturn component as SFCWithInstall<T>
}
关于NOOP:
import { NOOP } from '@vue/shared'
NOOP 是一个常见的编程术语,代表 “No Operation”,即不执行任何操作。在许多编程语言和环境中,它通常被用作一个占位符函数,当你需要一个函数但又不希望它做任何事情时,可以使用NOOP。
调用withNoopInstall:
export const ElAside = withNoopInstall(Aside)
hooks
useNamespace
路径:hooks/use-namespace
用于生成 BEM(Block Element Modifier)命名规则的类名和 CSS 变量名.
BEM 是一种 CSS 命名方法,全称是 Block Element Modifier,即块(Block)、元素(Element)、修饰符(Modifier)。
下面代码接受两个参数:块名和可选的命名空间覆盖。
返回一个对象,包含属性如下:
namespace
:命名空间。b、e、m、be、em、bm 和 bem
:用于生成不同类型的 BEM 类名的函数。is
:用于生成状态类名的函数。cssVar、cssVarName、cssVarBlock 和 cssVarBlockName
:用于生成 CSS 变量名的函数。
const statePrefix = 'is-'const _bem = (namespace: string,block: string,blockSuffix: string,element: string,modifier: string
) => {let cls = `${namespace}-${block}`if (blockSuffix) {cls += `-${blockSuffix}`}if (element) {cls += `__${element}`}if (modifier) {cls += `--${modifier}`}return cls
}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]! : truereturn 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 blockconst 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,// csscssVar,cssVarName,cssVarBlock,cssVarBlockName,}
}
省流版:返回一个对象,可以生成规范的类名。
单元测试
单元测试写的很好啊,可以做学习单元测试的例子:
container.test.tsx:
const AXIOM = 'Rem is the best girl'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)})})
})describe('Header', () => {test('create header', () => {const wrapper = mount(() => <Header />)expect(wrapper.classes()).toContain('el-header')})test('header height', () => {const wrapper = mount(() => <Header height="100px" />)const vm = wrapper.vmexpect(getCssVariable(vm.$el, '--el-header-height')).toEqual('100px')})
})describe('Aside', () => {test('aside create', () => {const wrapper = mount(() => <Aside />)expect(wrapper.classes()).toContain('el-aside')})test('aside width', () => {const wrapper = mount(() => <Aside width="200px" />)const vm = wrapper.vmexpect(getCssVariable(vm.$el, '--el-aside-width')).toEqual('200px')})
})describe('Main', () => {test('main create', () => {const wrapper = mount(() => <Main />)expect(wrapper.classes()).toContain('el-main')})
})describe('Footer', () => {test('footer create', () => {const wrapper = mount(() => <Footer />)expect(wrapper.classes()).toContain('el-footer')})test('footer height', () => {const wrapper = mount(() => <Footer height="100px" />)const vm = wrapper.vmexpect(getCssVariable(vm.$el, '--el-footer-height')).toEqual('100px')})
})