Component Library Architecture Design
Overview
This document explores the architectural principles and design patterns that make Element Plus a robust, scalable, and maintainable component library. We'll examine the core architecture, design decisions, and implementation strategies that enable Element Plus to serve diverse application needs.
Core Architecture Principles
1. Modular Design
Element Plus follows a modular architecture where each component is self-contained and independently deployable.
// Component module structure
// packages/components/button/
// ├── src/
// │ ├── button.vue
// │ ├── button-group.vue
// │ └── constants.ts
// ├── style/
// │ ├── css.ts
// │ └── index.ts
// ├── __tests__/
// │ └── button.test.ts
// └── index.ts
// Component entry point
export { default as ElButton } from './src/button.vue'
export { default as ElButtonGroup } from './src/button-group.vue'
export * from './src/constants'
// Style entry point
import './style/index.scss'
// Type definitions
export interface ButtonProps {
size?: 'large' | 'default' | 'small'
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
plain?: boolean
round?: boolean
circle?: boolean
loading?: boolean
disabled?: boolean
icon?: string | Component
autofocus?: boolean
nativeType?: 'button' | 'submit' | 'reset'
autoInsertSpace?: boolean
color?: string
dark?: boolean
link?: boolean
text?: boolean
bg?: boolean
tag?: string | Component
}
2. Composition API Integration
Element Plus leverages Vue 3's Composition API for better code organization and reusability.
// Core composables architecture
// packages/hooks/
// ├── use-namespace/
// ├── use-locale/
// ├── use-z-index/
// ├── use-id/
// ├── use-size/
// └── use-global-config/
// Namespace composable for consistent CSS class generation
import { computed, unref } from 'vue'
import type { MaybeRef } from '@vueuse/core'
export const defaultNamespace = 'el'
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) => {
const namespace = computed(() => defaultNamespace)
const b = (blockSuffix = '') =>
_bem(unref(namespace), block, blockSuffix, '', '')
const e = (element?: string) =>
element ? _bem(unref(namespace), block, '', element, '') : ''
const m = (modifier?: string) =>
modifier ? _bem(unref(namespace), block, '', '', modifier) : ''
const be = (blockSuffix?: string, element?: string) =>
blockSuffix && element
? _bem(unref(namespace), block, blockSuffix, element, '')
: ''
const em = (element?: string, modifier?: string) =>
element && modifier
? _bem(unref(namespace), block, '', element, modifier)
: ''
const bm = (blockSuffix?: string, modifier?: string) =>
blockSuffix && modifier
? _bem(unref(namespace), block, blockSuffix, '', modifier)
: ''
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
blockSuffix && element && modifier
? _bem(unref(namespace), 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
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
}
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,
cssVar,
cssVarBlock,
cssVarName,
cssVarBlockName
}
}
// Global configuration composable
import { computed, getCurrentInstance, inject, provide, ref, unref } from 'vue'
import { configProviderContextKey } from '@element-plus/tokens'
import type { MaybeRef } from '@vueuse/core'
import type { ConfigProviderContext } from '@element-plus/tokens'
const globalConfig = ref<ConfigProviderContext>()
export function useGlobalConfig<
K extends keyof ConfigProviderContext,
D extends ConfigProviderContext[K]
>(
key: K,
defaultValue?: D
): Ref<Exclude<ConfigProviderContext[K], undefined> | D>
export function useGlobalConfig(): Ref<ConfigProviderContext>
export function useGlobalConfig(key?: keyof ConfigProviderContext, defaultValue = undefined) {
const config = getCurrentInstance()
? inject(configProviderContextKey, globalConfig)
: globalConfig
if (key) {
return computed(() => config.value?.[key] ?? defaultValue)
} else {
return config
}
}
export const provideGlobalConfig = (
config: MaybeRef<ConfigProviderContext>,
app?: App,
global = false
) => {
const inSetup = !!getCurrentInstance()
const oldConfig = inSetup ? useGlobalConfig() : undefined
const provideFn = app?.provide ?? (inSetup ? provide : undefined)
if (!provideFn) {
console.warn('provideGlobalConfig', 'provideGlobalConfig() can only be used inside setup().')
return
}
const context = computed(() => {
const cfg = unref(config)
if (!oldConfig?.value) return cfg
return mergeConfig(oldConfig.value, cfg)
})
provideFn(configProviderContextKey, context)
if (global || !globalConfig.value) {
globalConfig.value = context.value
}
return context
}
const mergeConfig = (
a: ConfigProviderContext,
b: ConfigProviderContext
): ConfigProviderContext => {
const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])]
const obj: Record<string, any> = {}
for (const key of keys) {
obj[key] = b[key] ?? a[key]
}
return obj
}
3. Theme System Architecture
Element Plus implements a sophisticated theme system based on CSS custom properties and SCSS variables.
// Theme architecture
// packages/theme-chalk/src/
// ├── common/
// │ ├── var.scss // CSS custom properties
// │ ├── transition.scss // Animation definitions
// │ └── popup.scss // Popup utilities
// ├── mixins/
// │ ├── _button.scss // Component-specific mixins
// │ ├── config.scss // Configuration mixins
// │ ├── function.scss // Utility functions
// │ └── mixins.scss // Core mixins
// └── components/
// ├── button.scss
// ├── input.scss
// └── ...
// CSS Custom Properties System
:root {
// Color System
--el-color-primary: #409eff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-5: #a0cfff;
--el-color-primary-light-7: #c6e2ff;
--el-color-primary-light-8: #d9ecff;
--el-color-primary-light-9: #ecf5ff;
--el-color-primary-dark-2: #337ecc;
// Typography
--el-font-size-extra-large: 20px;
--el-font-size-large: 18px;
--el-font-size-medium: 16px;
--el-font-size-base: 14px;
--el-font-size-small: 13px;
--el-font-size-extra-small: 12px;
// Spacing
--el-border-radius-base: 4px;
--el-border-radius-small: 2px;
--el-border-radius-round: 20px;
--el-border-radius-circle: 100%;
// Component-specific variables
--el-button-font-weight: var(--el-font-weight-primary);
--el-button-border-color: var(--el-border-color);
--el-button-bg-color: var(--el-fill-color-blank);
--el-button-text-color: var(--el-text-color-regular);
--el-button-disabled-text-color: var(--el-disabled-text-color);
--el-button-disabled-bg-color: var(--el-fill-color-blank);
--el-button-disabled-border-color: var(--el-border-color-light);
--el-button-hover-text-color: var(--el-color-primary);
--el-button-hover-bg-color: var(--el-color-primary-light-9);
--el-button-hover-border-color: var(--el-color-primary-light-7);
--el-button-active-text-color: var(--el-button-hover-text-color);
--el-button-active-border-color: var(--el-color-primary);
--el-button-active-bg-color: var(--el-button-hover-bg-color);
}
// Theme generation mixins
@mixin genTheme($colors) {
@each $type in (primary, success, warning, danger, error, info) {
$color: map.get($colors, $type);
@include set-css-var-value(('color', $type), $color);
@for $i from 1 through 9 {
@include set-css-var-value(
('color', $type, 'light', $i),
mix(#fff, $color, math.percentage(math.div($i, 10)))
);
}
@include set-css-var-value(
('color', $type, 'dark', 2),
mix(#000, $color, 0.2)
);
}
}
// Component mixin system
@mixin button-variant($color, $background-color, $border-color) {
color: $color;
background-color: $background-color;
border-color: $border-color;
&:hover,
&:focus {
background: mix(#fff, $background-color, 20%);
border-color: mix(#fff, $border-color, 20%);
color: $color;
}
&:active {
background: mix(#000, $background-color, 10%);
border-color: mix(#000, $border-color, 10%);
color: $color;
outline: none;
}
&.is-disabled {
&,
&:hover,
&:focus,
&:active {
color: #fff;
background-color: mix(#fff, $background-color, 50%);
border-color: mix(#fff, $border-color, 50%);
}
}
}
@mixin button-size($padding-vertical, $padding-horizontal, $font-size, $border-radius) {
padding: $padding-vertical $padding-horizontal;
font-size: $font-size;
border-radius: $border-radius;
&.is-round {
border-radius: math.div($font-size + $padding-vertical * 2, 2);
}
}
4. Plugin System Architecture
Element Plus provides a flexible plugin system for extending functionality.
// Plugin system architecture
import type { App } from 'vue'
import { INSTALLED_KEY } from '@element-plus/constants'
export interface InstallOptions {
size?: ComponentSize
zIndex?: number
locale?: Language
namespace?: string
}
export interface SFCWithInstall<T> {
new (): T
install(app: App, options?: InstallOptions): void
}
export interface SFCInstallWithContext<T> {
new (): T
install(app: App, options?: InstallOptions): void
_context: AppContext | null
}
// Plugin installation utility
export const withInstall = <T, E extends Record<string, any>>(
main: T,
extra?: E
) => {
;(main as SFCWithInstall<T>).install = (app: App, options?: InstallOptions) => {
for (const comp of [main, ...Object.values(extra ?? {})]) {
app.component(comp.name, comp)
}
if (options) {
app.provide(configProviderContextKey, options)
}
}
if (extra) {
for (const [key, comp] of Object.entries(extra)) {
;(main as any)[key] = comp
}
}
return main as SFCWithInstall<T> & E
}
// Service plugin pattern
export const withInstallFunction = <T>(fn: T, name: string) => {
;(fn as SFCWithInstall<T>).install = (app: App) => {
;(fn as SFCInstallWithContext<T>)._context = app._context
app.config.globalProperties[name] = fn
}
return fn as SFCInstallWithContext<T>
}
// Directive plugin pattern
export const withInstallDirective = <T>(directive: T, name: string) => {
;(directive as SFCWithInstall<T>).install = (app: App) => {
app.directive(name, directive)
}
return directive as SFCWithInstall<T>
}
// Example component plugin
import Button from './button.vue'
import ButtonGroup from './button-group.vue'
export const ElButton = withInstall(Button, {
ButtonGroup
})
export default ElButton
export const ElButtonGroup = ElButton.ButtonGroup
// Example service plugin
import { ElMessage } from './message'
export default withInstallFunction(ElMessage, '$message')
5. Type System Architecture
Element Plus provides comprehensive TypeScript support with a well-structured type system.
// Type system architecture
// packages/utils/types.ts
// Utility types
export type Awaitable<T> = Promise<T> | T
export type Arrayable<T> = T | T[]
export type Nullable<T> = T | null
export type Mutable<T> = { -readonly [P in keyof T]: T[P] }
export type HTMLElementCustomized<T = {}> = HTMLElement & T
// Component size system
export type ComponentSize = 'large' | 'default' | 'small'
// Event handler types
export type EventHandler<T = Event> = (event: T) => void
export type MouseEventHandler = EventHandler<MouseEvent>
export type KeyboardEventHandler = EventHandler<KeyboardEvent>
export type FocusEventHandler = EventHandler<FocusEvent>
// Form validation types
export interface FormValidateCallback {
(isValid: boolean, invalidFields?: ValidateFieldsError): void
}
export interface FormValidateFailure {
errors: ValidateError[] | null
fields: ValidateFieldsError
}
export type FormValidationResult = Promise<boolean>
export type FormValidateResult = Promise<FormValidateFailure>
// Component prop types
export interface ComponentProps {
size?: ComponentSize
disabled?: boolean
loading?: boolean
}
// Theme types
export interface ThemeVars {
colorPrimary?: string
colorSuccess?: string
colorWarning?: string
colorDanger?: string
colorInfo?: string
fontSize?: string
fontFamily?: string
borderRadius?: string
}
// Global configuration types
export interface ConfigProviderContext {
a11y?: boolean
locale?: Language
size?: ComponentSize
button?: {
autoInsertSpace?: boolean
}
experimentalFeatures?: {
draw?: boolean
}
keyboardNavigation?: boolean
message?: {
max?: number
}
zIndex?: number
namespace?: string
}
// Component instance types
export interface ComponentInternalInstance {
uid: number
type: ConcreteComponent
parent: ComponentInternalInstance | null
appContext: AppContext
vnode: VNode
next: VNode | null
subTree: VNode
effect: ReactiveEffect
update: SchedulerJob
render: InternalRenderFunction | null
provides: Data
scope: EffectScope
accessCache: Data | null
renderCache: (Function | VNode)[]
components: Record<string, ConcreteComponent> | null
directives: Record<string, Directive> | null
propsOptions: NormalizedPropsOptions
emitsOptions: ObjectEmitsOptions | null
emit: EmitFn
emitted: Record<string, boolean> | null
propsDefaults: Data | undefined
inheritAttrs?: boolean
ctx: ComponentRenderContext
data: Data
props: Data
attrs: Data
slots: InternalSlots
refs: Data
setupState: Data
setupContext: SetupContext | null
suspense: SuspenseBoundary | null
suspenseId: number
asyncDep: Promise<any> | null
asyncResolved: boolean
isMounted: boolean
isUnmounted: boolean
isDeactivated: boolean
bc: SchedulerJob[] | null
c: SchedulerJob[] | null
bm: SchedulerJob[] | null
m: SchedulerJob[] | null
bu: SchedulerJob[] | null
u: SchedulerJob[] | null
um: SchedulerJob[] | null
bum: SchedulerJob[] | null
da: SchedulerJob[] | null
a: SchedulerJob[] | null
rtg: SchedulerJob[] | null
rtc: SchedulerJob[] | null
ec: SchedulerJob[] | null
sp: SchedulerJob[] | null
}
Component Design Patterns
1. Provider-Consumer Pattern
Element Plus uses the provider-consumer pattern for sharing configuration and state across component hierarchies.
// Form provider implementation
import { computed, inject, provide, reactive, ref, toRefs } from 'vue'
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
import type { FormContext, FormItemContext } from '@element-plus/tokens'
// Form provider
export const useFormProvider = (props: FormProps) => {
const fields = ref<FormItemContext[]>([])
const addField = (field: FormItemContext) => {
fields.value.push(field)
}
const removeField = (field: FormItemContext) => {
if (field.prop) {
fields.value.splice(fields.value.indexOf(field), 1)
}
}
const resetFields = (properties: string[] = []) => {
if (!properties.length) {
return fields.value.forEach(field => field.resetField())
}
const filteredFields = fields.value.filter(
field => field.prop && properties.includes(field.prop)
)
filteredFields.forEach(field => field.resetField())
}
const clearValidate = (properties: string[] = []) => {
if (!properties.length) {
return fields.value.forEach(field => field.clearValidate())
}
const filteredFields = fields.value.filter(
field => field.prop && properties.includes(field.prop)
)
filteredFields.forEach(field => field.clearValidate())
}
const validate = async (callback?: FormValidateCallback) => {
return validateField(undefined, callback)
}
const validateField = async (
modelProps: string[] = [],
callback?: FormValidateCallback
) => {
let validationErrors: ValidateFieldsError = {}
const fieldsToValidate = modelProps.length > 0
? fields.value.filter(field => modelProps.includes(field.prop!))
: fields.value
if (fieldsToValidate.length === 0) {
callback?.(true, {})
return true
}
let valid = true
let count = 0
for (const field of fieldsToValidate) {
field.validate('', (message, invalidFields) => {
if (message) {
valid = false
validationErrors = {
...validationErrors,
...invalidFields
}
}
if (++count === fieldsToValidate.length) {
callback?.(valid, validationErrors)
}
})
}
return valid
}
const context: FormContext = reactive({
...toRefs(props),
addField,
removeField,
resetFields,
clearValidate,
validate,
validateField
})
provide(formContextKey, context)
return context
}
// Form consumer
export const useFormItem = () => {
const form = inject(formContextKey, undefined)
const formItem = inject(formItemContextKey, undefined)
return {
form,
formItem
}
}
// Form item provider
export const useFormItemProvider = (props: FormItemProps) => {
const { form } = useFormItem()
const validateState = ref('')
const validateMessage = ref('')
const validateDisabled = ref(false)
const validate = (trigger: string, callback?: FormValidateCallback) => {
// Validation logic implementation
}
const resetField = () => {
// Reset field logic
}
const clearValidate = () => {
validateState.value = ''
validateMessage.value = ''
validateDisabled.value = false
}
const context: FormItemContext = reactive({
...toRefs(props),
validateState,
validateMessage,
validateDisabled,
validate,
resetField,
clearValidate
})
provide(formItemContextKey, context)
onMounted(() => {
if (props.prop) {
form?.addField(context)
}
})
onBeforeUnmount(() => {
form?.removeField(context)
})
return context
}
2. Compound Component Pattern
Element Plus uses compound components for complex UI patterns like Tables, Menus, and Forms.
// Table compound component implementation
// ElTable + ElTableColumn pattern
// Table provider
export const useTableProvider = (props: TableProps) => {
const columns = ref<TableColumnContext[]>([])
const store = reactive(new TableStore())
const insertColumn = (column: TableColumnContext, index?: number) => {
if (typeof index !== 'undefined') {
columns.value.splice(index, 0, column)
} else {
columns.value.push(column)
}
store.updateColumns(columns.value)
}
const removeColumn = (column: TableColumnContext) => {
const index = columns.value.indexOf(column)
if (index > -1) {
columns.value.splice(index, 1)
store.updateColumns(columns.value)
}
}
const context: TableContext = reactive({
...toRefs(props),
store,
columns: readonly(columns),
insertColumn,
removeColumn
})
provide(tableContextKey, context)
return context
}
// Table column consumer
export const useTableColumn = (props: TableColumnProps) => {
const table = inject(tableContextKey)
const columnId = ref(`el-table-column-${generateId()}`)
const columnConfig = computed(() => ({
id: columnId.value,
...props,
renderCell: (data: any) => {
// Cell rendering logic
},
renderHeader: (data: any) => {
// Header rendering logic
}
}))
onMounted(() => {
table?.insertColumn(columnConfig.value)
})
onBeforeUnmount(() => {
table?.removeColumn(columnConfig.value)
})
return {
columnId,
columnConfig
}
}
// Menu compound component
export const useMenuProvider = (props: MenuProps) => {
const openedMenus = ref<string[]>([])
const activeIndex = ref<string | null>(null)
const openMenu = (index: string) => {
if (!openedMenus.value.includes(index)) {
openedMenus.value.push(index)
}
}
const closeMenu = (index: string) => {
const i = openedMenus.value.indexOf(index)
if (i !== -1) {
openedMenus.value.splice(i, 1)
}
}
const handleMenuItemClick = (menuItem: MenuItemContext) => {
if (menuItem.disabled) return
activeIndex.value = menuItem.index
emit('select', menuItem.index, menuItem.indexPath, menuItem)
}
const context: MenuContext = reactive({
...toRefs(props),
openedMenus: readonly(openedMenus),
activeIndex: readonly(activeIndex),
openMenu,
closeMenu,
handleMenuItemClick
})
provide(menuContextKey, context)
return context
}
3. Render Function Pattern
Element Plus uses render functions for dynamic content and complex rendering logic.
// Virtual list implementation with render functions
import { defineComponent, computed, ref, onMounted, onUpdated } from 'vue'
export const VirtualList = defineComponent({
name: 'ElVirtualList',
props: {
data: {
type: Array,
required: true
},
itemSize: {
type: Number,
required: true
},
height: {
type: Number,
required: true
},
buffer: {
type: Number,
default: 5
}
},
setup(props, { slots }) {
const scrollTop = ref(0)
const containerRef = ref<HTMLElement>()
const visibleRange = computed(() => {
const start = Math.floor(scrollTop.value / props.itemSize)
const end = Math.min(
start + Math.ceil(props.height / props.itemSize) + props.buffer,
props.data.length
)
return {
start: Math.max(0, start - props.buffer),
end
}
})
const visibleData = computed(() => {
return props.data.slice(visibleRange.value.start, visibleRange.value.end)
})
const totalHeight = computed(() => {
return props.data.length * props.itemSize
})
const offsetY = computed(() => {
return visibleRange.value.start * props.itemSize
})
const handleScroll = (event: Event) => {
scrollTop.value = (event.target as HTMLElement).scrollTop
}
return () => (
<div
ref={containerRef}
class="el-virtual-list"
style={{ height: `${props.height}px`, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: `${totalHeight.value}px`, position: 'relative' }}>
<div
style={{
transform: `translateY(${offsetY.value}px)`,
position: 'absolute',
top: 0,
left: 0,
right: 0
}}
>
{visibleData.value.map((item, index) => {
const actualIndex = visibleRange.value.start + index
return (
<div
key={actualIndex}
style={{ height: `${props.itemSize}px` }}
>
{slots.default?.({ item, index: actualIndex })}
</div>
)
})}
</div>
</div>
</div>
)
}
})
// Table cell render function
export const TableCell = defineComponent({
name: 'ElTableCell',
props: {
column: {
type: Object,
required: true
},
row: {
type: Object,
required: true
},
rowIndex: {
type: Number,
required: true
}
},
setup(props) {
return () => {
const { column, row, rowIndex } = props
// Custom render function
if (column.renderCell) {
return column.renderCell({
row,
column,
rowIndex,
cellValue: row[column.property]
})
}
// Slot render
if (column.slots?.default) {
return column.slots.default({
row,
column,
rowIndex,
cellValue: row[column.property]
})
}
// Formatter function
if (column.formatter) {
return column.formatter(row, column, row[column.property], rowIndex)
}
// Default render
return row[column.property]
}
}
})
Performance Optimization Strategies
1. Lazy Loading and Code Splitting
// Component lazy loading
export const LazyButton = defineAsyncComponent({
loader: () => import('./button.vue'),
loadingComponent: () => h('div', 'Loading...'),
errorComponent: () => h('div', 'Error loading component'),
delay: 200,
timeout: 3000
})
// Tree-shakable imports
export { ElButton } from './button'
export { ElInput } from './input'
export { ElForm } from './form'
// ... other components
// Auto-import configuration
export const ElementPlusResolver = {
type: 'component',
resolve: (name: string) => {
if (name.startsWith('El')) {
return {
name,
from: 'element-plus',
sideEffects: `element-plus/es/components/${name.slice(2).toLowerCase()}/style/css`
}
}
}
}
2. Virtual Scrolling Implementation
// Virtual scrolling for large datasets
export const useVirtualScroll = ({
itemSize,
containerHeight,
data,
buffer = 5
}) => {
const scrollTop = ref(0)
const isScrolling = ref(false)
const scrollingTimer = ref<NodeJS.Timeout>()
const visibleRange = computed(() => {
const start = Math.floor(scrollTop.value / itemSize)
const visibleCount = Math.ceil(containerHeight / itemSize)
return {
start: Math.max(0, start - buffer),
end: Math.min(data.length, start + visibleCount + buffer)
}
})
const visibleData = computed(() => {
return data.slice(visibleRange.value.start, visibleRange.value.end)
})
const totalHeight = computed(() => data.length * itemSize)
const offsetY = computed(() => visibleRange.value.start * itemSize)
const handleScroll = (event: Event) => {
scrollTop.value = (event.target as HTMLElement).scrollTop
if (!isScrolling.value) {
isScrolling.value = true
}
clearTimeout(scrollingTimer.value)
scrollingTimer.value = setTimeout(() => {
isScrolling.value = false
}, 150)
}
return {
visibleRange,
visibleData,
totalHeight,
offsetY,
isScrolling,
handleScroll
}
}
3. Memoization and Caching
// Component memoization
export const MemoizedComponent = defineComponent({
name: 'MemoizedComponent',
props: {
data: {
type: Array,
required: true
},
config: {
type: Object,
required: true
}
},
setup(props) {
// Memoized computed properties
const processedData = computed(() => {
return expensiveDataProcessing(props.data, props.config)
})
// Cached function results
const memoizedCalculation = useMemoize(
(input: any[]) => expensiveCalculation(input),
{
maxSize: 100,
ttl: 5 * 60 * 1000 // 5 minutes
}
)
return {
processedData,
memoizedCalculation
}
}
})
// Memoization utility
export function useMemoize<T extends (...args: any[]) => any>(
fn: T,
options: {
maxSize?: number
ttl?: number
getKey?: (...args: Parameters<T>) => string
} = {}
) {
const {
maxSize = 50,
ttl = Infinity,
getKey = (...args) => JSON.stringify(args)
} = options
const cache = new Map<string, { value: ReturnType<T>; timestamp: number }>()
return (...args: Parameters<T>): ReturnType<T> => {
const key = getKey(...args)
const now = Date.now()
// Check if cached result exists and is still valid
const cached = cache.get(key)
if (cached && (now - cached.timestamp) < ttl) {
return cached.value
}
// Calculate new result
const result = fn(...args)
// Store in cache
cache.set(key, { value: result, timestamp: now })
// Cleanup old entries if cache is too large
if (cache.size > maxSize) {
const oldestKey = cache.keys().next().value
cache.delete(oldestKey)
}
return result
}
}
Testing Architecture
1. Component Testing Strategy
// Component testing utilities
import { mount, VueWrapper } from '@vue/test-utils'
import { ElConfigProvider } from 'element-plus'
export const createTestWrapper = <T>(
component: T,
options: {
props?: Record<string, any>
slots?: Record<string, any>
global?: {
plugins?: any[]
provide?: Record<string, any>
}
} = {}
): VueWrapper<any> => {
return mount(component, {
global: {
plugins: [ElementPlus],
...options.global
},
...options
})
}
// Test utilities for Element Plus components
export const getElementPlusWrapper = (component: any, props = {}) => {
return mount({
components: { TestComponent: component },
template: '<ElConfigProvider><TestComponent v-bind="props" /></ElConfigProvider>',
data() {
return { props }
}
}, {
global: {
plugins: [ElementPlus]
}
})
}
// Accessibility testing helpers
export const testA11y = async (wrapper: VueWrapper<any>) => {
const { violations } = await axe(wrapper.element)
expect(violations).toHaveLength(0)
}
// Visual regression testing
export const takeSnapshot = (wrapper: VueWrapper<any>, name: string) => {
expect(wrapper.element).toMatchSnapshot(name)
}
2. Integration Testing
// Form integration test example
describe('Form Integration', () => {
it('should validate form with multiple fields', async () => {
const wrapper = createTestWrapper({
template: `
<el-form ref="form" :model="form" :rules="rules">
<el-form-item prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item prop="email">
<el-input v-model="form.email" />
</el-form-item>
<el-button @click="submit">Submit</el-button>
</el-form>
`,
data() {
return {
form: { name: '', email: '' },
rules: {
name: [{ required: true, message: 'Name is required' }],
email: [{ required: true, message: 'Email is required' }]
}
}
},
methods: {
async submit() {
await this.$refs.form.validate()
}
}
})
const button = wrapper.find('button')
await button.trigger('click')
await nextTick()
expect(wrapper.findAll('.el-form-item__error')).toHaveLength(2)
})
})
Best Practices
1. Architecture Guidelines
- Separation of Concerns: Keep logic, styling, and templates separate
- Composition over Inheritance: Use composition API and mixins
- Type Safety: Provide comprehensive TypeScript support
- Performance: Implement lazy loading and virtual scrolling
- Accessibility: Follow WCAG guidelines
- Testing: Maintain high test coverage
2. Component Design Principles
- Single Responsibility: Each component should have one clear purpose
- Reusability: Design for reuse across different contexts
- Configurability: Provide sensible defaults with customization options
- Consistency: Follow established patterns and conventions
- Documentation: Provide clear API documentation and examples
3. Performance Considerations
- Bundle Size: Keep components lightweight and tree-shakable
- Runtime Performance: Optimize for rendering and interaction speed
- Memory Usage: Prevent memory leaks and optimize memory consumption
- Loading Performance: Implement progressive loading strategies
Conclusion
Element Plus's architecture demonstrates:
- Modular Design: Clean separation and independent components
- Flexible Plugin System: Extensible architecture for customization
- Type Safety: Comprehensive TypeScript integration
- Performance Optimization: Virtual scrolling, lazy loading, and memoization
- Testing Strategy: Comprehensive testing approach
- Best Practices: Industry-standard patterns and principles
This architecture enables Element Plus to serve as a robust foundation for building scalable, maintainable, and performant Vue.js applications.