在现代前端开发中,组件化开发已成为主流,React、Vue 等框架通过组件封装和复用极大提升了开发效率。而组合模式(Composite Pattern)作为一种结构型设计模式,为组件化开发提供了强大的理论支持。组合模式通过将对象组织成树形结构,以统一的方式处理单个对象和组合对象,完美契合组件化开发中父子组件嵌套的场景。
1. 组合模式的基础
1.1 什么是组合模式?
组合模式是一种结构型设计模式,旨在将对象组织成树形结构,使得客户端可以统一处理单个对象(叶节点)和组合对象(容器节点)。其核心思想是:
- 一致性接口:单个对象和组合对象实现相同的接口。
- 树形结构:通过递归组合形成层次结构。
- 透明性:客户端无需区分操作的是单个对象还是组合对象。
在组件化开发中,组合模式体现在组件树中,父组件可以包含子组件,子组件也可以是父组件,共同构成复杂的 UI 结构。
1.2 组合模式的组成
组合模式通常包含以下角色:
- Component(组件接口):定义所有组件的公共接口。
- Leaf(叶节点):实现 Component 接口,表示没有子节点的组件。
- Composite(组合节点):实现 Component 接口,包含子组件并管理其生命周期。
- Client(客户端):通过 Component 接口操作组件树。
1.3 为什么在组件化开发中使用组合模式?
- 层次结构:UI 天然是树形结构(如 DOM 树、组件树)。
- 一致性操作:父子组件通过统一接口通信。
- 灵活性:支持动态添加或移除子组件。
- 复用性:组合模式便于封装可复用的组件逻辑。
2. 组合模式的原理
2.1 基本结构
以下是一个简单的组合模式实现:
javascript
// Component 接口
class Component {
add(child) {
throw new Error('Method must be implemented');
}
remove(child) {
throw new Error('Method must be implemented');
}
render() {
throw new Error('Method must be implemented');
}
}
// Leaf 节点
class Leaf extends Component {
constructor(name) {
super();
this.name = name;
}
render() {
return `<div>${this.name}</div>`;
}
}
// Composite 节点
class Composite extends Component {
constructor(name) {
super();
this.name = name;
this.children = [];
}
add(child) {
this.children.push(child);
}
remove(child) {
this.children = this.children.filter(c => c !== child);
}
render() {
const childrenHtml = this.children.map(c => c.render()).join('');
return `<div>${this.name}${childrenHtml}</div>`;
}
}
// 客户端
const root = new Composite('Root');
const leaf1 = new Leaf('Leaf 1');
const leaf2 = new Leaf('Leaf 2');
const composite1 = new Composite('Composite 1');
root.add(leaf1);
root.add(composite1);
composite1.add(leaf2);
console.log(root.render());
// <div>Root<div>Leaf 1</div><div>Composite 1<div>Leaf 2</div></div></div>
2.2 组合模式的关键特性
- 递归性 :Composite 节点通过递归调用子节点的
render
方法生成 HTML。 - 一致性 :Leaf 和 Composite 都实现
render
方法,客户端无需区分。 - 动态性:支持运行时添加或移除子节点。
2.3 适用场景
组合模式在组件化开发中的典型场景包括:
- UI 组件树:如 React 的组件嵌套。
- 动态表单:表单字段可以是输入框、选择框或嵌套表单。
- 菜单系统:菜单项可以是单个链接或包含子菜单的容器。
3. 组合模式在 React 中的应用
React 的组件化开发天然契合组合模式,每个组件可以是叶节点(无子组件)或组合节点(包含子组件)。
3.1 基本组件实现
创建一个简单的组件树:
javascript
import React from 'react';
const LeafComponent = ({ name }) => <div>{name}</div>;
const CompositeComponent = ({ name, children }) => (
<div>
<h2>{name}</h2>
{children}
</div>
);
const App = () => (
<CompositeComponent name="Root">
<LeafComponent name="Leaf 1" />
<CompositeComponent name="Composite 1">
<LeafComponent name="Leaf 2" />
</CompositeComponent>
</CompositeComponent>
);
渲染结果:
html
<div>
<h2>Root</h2>
<div>Leaf 1</div>
<div>
<h2>Composite 1</h2>
<div>Leaf 2</div>
</div>
</div>
3.2 动态组件树
支持动态添加子组件:
javascript
import React, { useState } from 'react';
const LeafComponent = ({ name }) => <div>{name}</div>;
const CompositeComponent = ({ name, children }) => (
<div>
<h2>{name}</h2>
{children}
</div>
);
const App = () => {
const [components, setComponents] = useState([
{ id: 1, type: 'leaf', name: 'Leaf 1' },
{
id: 2,
type: 'composite',
name: 'Composite 1',
children: [{ id: 3, type: 'leaf', name: 'Leaf 2' }],
},
]);
const renderComponent = comp => {
if (comp.type === 'leaf') {
return <LeafComponent key={comp.id} name={comp.name} />;
}
return (
<CompositeComponent key={comp.id} name={comp.name}>
{comp.children?.map(renderComponent)}
</CompositeComponent>
);
};
return <div>{components.map(renderComponent)}</div>;
};
3.3 组件组合与 Props 传递
通过 Props 传递数据和方法:
javascript
import React from 'react';
const LeafComponent = ({ name, onClick }) => (
<div onClick={() => onClick(name)}>{name}</div>
);
const CompositeComponent = ({ name, onClick, children }) => (
<div>
<h2 onClick={() => onClick(name)}>{name}</h2>
{children}
</div>
);
const App = () => {
const handleClick = name => console.log(`Clicked: ${name}`);
return (
<CompositeComponent name="Root" onClick={handleClick}>
<LeafComponent name="Leaf 1" onClick={handleClick} />
<CompositeComponent name="Composite 1" onClick={handleClick}>
<LeafComponent name="Leaf 2" onClick={handleClick} />
</CompositeComponent>
</CompositeComponent>
);
};
4. 组合模式在 Vue 中的应用
Vue 的组件化机制同样支持组合模式,组件通过插槽(Slots)和动态组件实现树形结构。
4.1 基本组件实现
javascript
// LeafComponent.vue
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
props: ['name'],
};
</script>
// CompositeComponent.vue
<template>
<div>
<h2>{{ name }}</h2>
<slot />
</div>
</template>
<script>
export default {
props: ['name'],
};
</script>
// App.vue
<template>
<CompositeComponent name="Root">
<LeafComponent name="Leaf 1" />
<CompositeComponent name="Composite 1">
<LeafComponent name="Leaf 2" />
</CompositeComponent>
</CompositeComponent>
</template>
<script>
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default {
components: { LeafComponent, CompositeComponent },
};
</script>
4.2 动态组件树
使用动态组件渲染:
javascript
// App.vue
<template>
<div>
<component
v-for="comp in components"
:key="comp.id"
:is="comp.type === 'leaf' ? LeafComponent : CompositeComponent"
:name="comp.name"
>
<template v-if="comp.children">
<component
v-for="child in comp.children"
:key="child.id"
:is="child.type === 'leaf' ? LeafComponent : CompositeComponent"
:name="child.name"
/>
</template>
</component>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default defineComponent({
components: { LeafComponent, CompositeComponent },
data() {
return {
components: [
{ id: 1, type: 'leaf', name: 'Leaf 1' },
{
id: 2,
type: 'composite',
name: 'Composite 1',
children: [{ id: 3, type: 'leaf', name: 'Leaf 2' }],
},
],
};
},
});
</script>
4.3 事件传递
通过事件实现父子通信:
javascript
// LeafComponent.vue
<template>
<div @click="$emit('custom-click', name)">{{ name }}</div>
</template>
<script>
export default {
props: ['name'],
emits: ['custom-click'],
};
</script>
// CompositeComponent.vue
<template>
<div>
<h2 @click="$emit('custom-click', name)">{{ name }}</h2>
<slot />
</div>
</template>
<script>
export default {
props: ['name'],
emits: ['custom-click'],
};
</script>
// App.vue
<template>
<CompositeComponent name="Root" @custom-click="handleClick">
<LeafComponent name="Leaf 1" @custom-click="handleClick" />
<CompositeComponent name="Composite 1" @custom-click="handleClick">
<LeafComponent name="Leaf 2" @custom-click="handleClick" />
</CompositeComponent>
</CompositeComponent>
</template>
<script>
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default {
components: { LeafComponent, CompositeComponent },
methods: {
handleClick(name) {
console.log(`Clicked: ${name}`);
},
},
};
</script>
5. 组合模式在 TypeScript 中的实现
TypeScript 的类型系统为组合模式提供更强的类型安全。
5.1 基本实现
typescript
interface Component {
add(child: Component): void;
remove(child: Component): void;
render(): string;
}
class Leaf implements Component {
constructor(private name: string) {}
add() {}
remove() {}
render(): string {
return `<div>${this.name}</div>`;
}
}
class Composite implements Component {
private children: Component[] = [];
constructor(private name: string) {}
add(child: Component): void {
this.children.push(child);
}
remove(child: Component): void {
this.children = this.children.filter(c => c !== child);
}
render(): string {
const childrenHtml = this.children.map(c => c.render()).join('');
return `<div>${this.name}${childrenHtml}</div>`;
}
}
const root = new Composite('Root');
const leaf1 = new Leaf('Leaf 1');
const leaf2 = new Leaf('Leaf 2');
const composite1 = new Composite('Composite 1');
root.add(leaf1);
root.add(composite1);
composite1.add(leaf2);
console.log(root.render());
5.2 React + TypeScript
typescript
import React, { FC, ReactNode } from 'react';
interface LeafProps {
name: string;
onClick: (name: string) => void;
}
const LeafComponent: FC<LeafProps> = ({ name, onClick }) => (
<div onClick={() => onClick(name)}>{name}</div>
);
interface CompositeProps {
name: string;
onClick: (name: string) => void;
children?: ReactNode;
}
const CompositeComponent: FC<CompositeProps> = ({ name, onClick, children }) => (
<div>
<h2 onClick={() => onClick(name)}>{name}</h2>
{children}
</div>
);
const App: FC = () => {
const handleClick = (name: string) => console.log(`Clicked: ${name}`);
return (
<CompositeComponent name="Root" onClick={handleClick}>
<LeafComponent name="Leaf 1" onClick={handleClick} />
<CompositeComponent name="Composite 1" onClick={handleClick}>
<LeafComponent name="Leaf 2" onClick={handleClick} />
</CompositeComponent>
</CompositeComponent>
);
};
5.3 Vue + TypeScript
typescript
// LeafComponent.vue
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
name: { type: String, required: true },
},
emits: ['custom-click'],
setup(props, { emit }) {
return () => (
<div onClick={() => emit('custom-click', props.name)}>{props.name}</div>
);
},
});
</script>
// CompositeComponent.vue
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
name: { type: String, required: true },
},
emits: ['custom-click'],
setup(props, { emit, slots }) {
return () => (
<div>
<h2 onClick={() => emit('custom-click', props.name)}>{props.name}</h2>
{slots.default?.()}
</div>
);
},
});
</script>
// App.vue
<script lang="ts">
import { defineComponent } from 'vue';
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default defineComponent({
components: { LeafComponent, CompositeComponent },
setup() {
const handleClick = (name: string) => console.log(`Clicked: ${name}`);
return { handleClick };
},
});
</script>
<template>
<CompositeComponent name="Root" @custom-click="handleClick">
<LeafComponent name="Leaf 1" @custom-click="handleClick" />
<CompositeComponent name="Composite 1" @custom-click="handleClick">
<LeafComponent name="Leaf 2" @custom-click="handleClick" />
</CompositeComponent>
</CompositeComponent>
</template>
6. 组合模式的性能优化
6.1 React 优化
使用 React.memo
避免不必要的重渲染:
javascript
import React, { memo } from 'react';
const LeafComponent = memo(({ name, onClick }) => (
<div onClick={() => onClick(name)}>{name}</div>
));
const CompositeComponent = memo(({ name, onClick, children }) => (
<div>
<h2 onClick={() => onClick(name)}>{name}</h2>
{children}
</div>
));
使用 useCallback
稳定回调:
javascript
import React, { useCallback } from 'react';
const App = () => {
const handleClick = useCallback(name => console.log(`Clicked: ${name}`), []);
return (
<CompositeComponent name="Root" onClick={handleClick}>
<LeafComponent name="Leaf 1" onClick={handleClick} />
<CompositeComponent name="Composite 1" onClick={handleClick}>
<LeafComponent name="Leaf 2" onClick={handleClick} />
</CompositeComponent>
</CompositeComponent>
);
};
6.2 Vue 优化
使用 defineComponent
和 reactive
优化响应式数据:
javascript
// App.vue
<script lang="ts">
import { defineComponent, reactive } from 'vue';
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default defineComponent({
components: { LeafComponent, CompositeComponent },
setup() {
const state = reactive({
components: [
{ id: 1, type: 'leaf', name: 'Leaf 1' },
{
id: 2,
type: 'composite',
name: 'Composite 1',
children: [{ id: 3, type: 'leaf', name: 'Leaf 2' }],
},
],
});
const handleClick = (name: string) => console.log(`Clicked: ${name}`);
return { state, handleClick };
},
});
</script>
<template>
<div>
<component
v-for="comp in state.components"
:key="comp.id"
:is="comp.type === 'leaf' ? LeafComponent : CompositeComponent"
:name="comp.name"
@custom-click="handleClick"
>
<template v-if="comp.children">
<component
v-for="child in comp.children"
:key="child.id"
:is="child.type === 'leaf' ? LeafComponent : CompositeComponent"
:name="child.name"
@custom-click="handleClick"
/>
</template>
</component>
</div>
</template>
7. 组合模式在动态表单中的实战
7.1 React 动态表单
实现一个动态表单,支持输入框和嵌套表单:
javascript
import React, { useState } from 'react';
const InputField = ({ name, value, onChange }) => (
<div>
<label>{name}</label>
<input value={value} onChange={e => onChange(name, e.target.value)} />
</div>
);
const FormGroup = ({ name, children, onChange }) => (
<div>
<h3>{name}</h3>
{React.Children.map(children, child =>
React.cloneElement(child, { onChange })
)}
</div>
);
const DynamicForm = () => {
const [formData, setFormData] = useState({});
const handleChange = (name, value) => {
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<FormGroup name="User Info" onChange={handleChange}>
<InputField name="name" value={formData.name || ''} />
<FormGroup name="Address">
<InputField name="street" value={formData.street || ''} />
<InputField name="city" value={formData.city || ''} />
</FormGroup>
</FormGroup>
);
};
7.2 Vue 动态表单
javascript
// InputField.vue
<template>
<div>
<label>{{ name }}</label>
<input :value="value" @input="$emit('update', name, $event.target.value)" />
</div>
</template>
<script>
export default {
props: ['name', 'value'],
emits: ['update'],
};
</script>
// FormGroup.vue
<template>
<div>
<h3>{{ name }}</h3>
<slot />
</div>
</template>
<script>
export default {
props: ['name'],
};
</script>
// DynamicForm.vue
<template>
<FormGroup name="User Info">
<InputField
name="name"
:value="formData.name"
@update="updateField"
/>
<FormGroup name="Address">
<InputField
name="street"
:value="formData.street"
@update="updateField"
/>
<InputField
name="city"
:value="formData.city"
@update="updateField"
/>
</FormGroup>
</FormGroup>
</template>
<script>
import { reactive } from 'vue';
import InputField from './InputField.vue';
import FormGroup from './FormGroup.vue';
export default {
components: { InputField, FormGroup },
setup() {
const formData = reactive({});
const updateField = (name, value) => {
formData[name] = value;
};
return { formData, updateField };
},
};
</script>
8. 组合模式在菜单系统中的实战
8.1 React 菜单
实现一个可嵌套的菜单系统:
javascript
import React, { useState } from 'react';
const MenuItem = ({ name, onClick }) => (
<li onClick={() => onClick(name)}>{name}</li>
);
const Menu = ({ name, children, onClick }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<div onClick={() => setIsOpen(!isOpen)}>{name}</div>
{isOpen && <ul>{children}</ul>}
</div>
);
};
const MenuSystem = () => {
const handleClick = name => console.log(`Clicked: ${name}`);
return (
<Menu name="Root" onClick={handleClick}>
<MenuItem name="Item 1" onClick={handleClick} />
<Menu name="Submenu 1" onClick={handleClick}>
<MenuItem name="Item 2" onClick={handleClick} />
<MenuItem name="Item 3" onClick={handleClick} />
</Menu>
</Menu>
);
};
8.2 Vue 菜单
javascript
// MenuItem.vue
<template>
<li @click="$emit('custom-click', name)">{{ name }}</li>
</template>
<script>
export default {
props: ['name'],
emits: ['custom-click'],
};
</script>
// Menu.vue
<template>
<div>
<div @click="isOpen = !isOpen">{{ name }}</div>
<ul v-if="isOpen">
<slot />
</ul>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
props: ['name'],
emits: ['custom-click'],
setup() {
const isOpen = ref(false);
return { isOpen };
},
};
</script>
// MenuSystem.vue
<template>
<Menu name="Root" @custom-click="handleClick">
<MenuItem name="Item 1" @custom-click="handleClick" />
<Menu name="Submenu 1" @custom-click="handleClick">
<MenuItem name="Item 2" @custom-click="handleClick" />
<MenuItem name="Item 3" @custom-click="handleClick" />
</Menu>
</Menu>
</template>
<script>
import Menu from './Menu.vue';
import MenuItem from './MenuItem.vue';
export default {
components: { Menu, MenuItem },
methods: {
handleClick(name) {
console.log(`Clicked: ${name}`);
},
},
};
</script>
9. 组合模式与状态管理
9.1 Redux 集成
在 React 中结合 Redux 管理组件树状态:
javascript
import React from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
const initialState = {
components: [
{ id: 1, type: 'leaf', name: 'Leaf 1' },
{
id: 2,
type: 'composite',
name: 'Composite 1',
children: [{ id: 3, type: 'leaf', name: 'Leaf 2' }],
},
],
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_NAME':
return {
...state,
components: state.components.map(comp =>
comp.id === action.payload.id
? { ...comp, name: action.payload.name }
: comp
),
};
default:
return state;
}
};
const store = createStore(reducer);
const LeafComponent = ({ id, name }) => {
const dispatch = useDispatch();
const handleClick = () =>
dispatch({ type: 'UPDATE_NAME', payload: { id, name: `${name} Updated` } });
return <div onClick={handleClick}>{name}</div>;
};
const CompositeComponent = ({ id, name, children }) => {
const dispatch = useDispatch();
const handleClick = () =>
dispatch({ type: 'UPDATE_NAME', payload: { id, name: `${name} Updated` } });
return (
<div>
<h2 onClick={handleClick}>{name}</h2>
{children}
</div>
);
};
const App = () => {
const components = useSelector(state => state.components);
const renderComponent = comp => {
if (comp.type === 'leaf') {
return <LeafComponent key={comp.id} id={comp.id} name={comp.name} />;
}
return (
<CompositeComponent key={comp.id} id={comp.id} name={comp.name}>
{comp.children?.map(renderComponent)}
</CompositeComponent>
);
};
return <div>{components.map(renderComponent)}</div>;
};
const Root = () => (
<Provider store={store}>
<App />
</Provider>
);
9.2 Vuex 集成
在 Vue 中结合 Vuex:
javascript
// store.js
import { createStore } from 'vuex';
export default createStore({
state: {
components: [
{ id: 1, type: 'leaf', name: 'Leaf 1' },
{
id: 2,
type: 'composite',
name: 'Composite 1',
children: [{ id: 3, type: 'leaf', name: 'Leaf 2' }],
},
],
},
mutations: {
UPDATE_NAME(state, { id, name }) {
state.components = state.components.map(comp =>
comp.id === id ? { ...comp, name } : comp
);
},
},
actions: {
updateName({ commit }, { id, name }) {
commit('UPDATE_NAME', { id, name });
},
},
});
// App.vue
<template>
<div>
<component
v-for="comp in $store.state.components"
:key="comp.id"
:is="comp.type === 'leaf' ? LeafComponent : CompositeComponent"
:id="comp.id"
:name="comp.name"
>
<template v-if="comp.children">
<component
v-for="child in comp.children"
:key="child.id"
:is="child.type === 'leaf' ? LeafComponent : CompositeComponent"
:id="child.id"
:name="child.name"
/>
</template>
</component>
</div>
</template>
<script>
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default {
components: { LeafComponent, CompositeComponent },
};
</script>
// LeafComponent.vue
<template>
<div @click="updateName">{{ name }}</div>
</template>
<script>
export default {
props: ['id', 'name'],
methods: {
updateName() {
this.$store.dispatch('updateName', {
id: this.id,
name: `${this.name} Updated`,
});
},
},
};
</script>
// CompositeComponent.vue
<template>
<div>
<h2 @click="updateName">{{ name }}</h2>
<slot />
</div>
</template>
<script>
export default {
props: ['id', 'name'],
methods: {
updateName() {
this.$store.dispatch('updateName', {
id: this.id,
name: `${this.name} Updated`,
});
},
},
};
</script>
10. 组合模式与微前端
10.1 Qiankun 集成
在微前端中,组合模式用于管理子应用:
javascript
import { registerMicroApps, start } from 'qiankun';
class MicroAppComponent {
constructor(name, config) {
this.name = name;
this.config = config;
}
register() {
registerMicroApps([this.config]);
}
start() {
start();
}
}
class MicroAppComposite {
constructor(name) {
this.name = name;
this.children = [];
}
add(child) {
this.children.push(child);
}
remove(child) {
this.children = this.children.filter(c => c !== child);
}
register() {
this.children.forEach(child => child.register());
}
start() {
start();
}
}
const root = new MicroAppComposite('Root');
const reactApp = new MicroAppComponent('reactApp', {
name: 'reactApp',
entry: '//localhost:3001',
container: '#reactContainer',
activeRule: '/react',
});
const vueApp = new MicroAppComponent('vueApp', {
name: 'vueApp',
entry: '//localhost:3002',
container: '#vueContainer',
activeRule: '/vue',
});
root.add(reactApp);
root.add(vueApp);
root.register();
root.start();
10.2 React 微前端
javascript
import React from 'react';
import { loadMicroApp } from 'qiankun';
const MicroAppLeaf = ({ name, config }) => {
React.useEffect(() => {
const app = loadMicroApp(config);
return () => app.unmount();
}, [config]);
return <div id={config.container.replace('#', '')} />;
};
const MicroAppComposite = ({ name, children }) => (
<div>
<h2>{name}</h2>
{children}
</div>
);
const App = () => (
<MicroAppComposite name="Root">
<MicroAppLeaf
name="React App"
config={{
name: 'reactApp',
entry: '//localhost:3001',
container: '#reactContainer',
activeRule: '/react',
}}
/>
<MicroAppLeaf
name="Vue App"
config={{
name: 'vueApp',
entry: '//localhost:3002',
container: '#vueContainer',
activeRule: '/vue',
}}
/>
</MicroAppComposite>
);
11. 测试组合模式
11.1 React 测试
使用 Jest 和 Testing Library:
javascript
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
it('renders component tree', () => {
render(<App />);
expect(screen.getByText('Root')).toBeInTheDocument();
expect(screen.getByText('Leaf 1')).toBeInTheDocument();
expect(screen.getByText('Composite 1')).toBeInTheDocument();
expect(screen.getByText('Leaf 2')).toBeInTheDocument();
});
});
11.2 Vue 测试
javascript
import { mount } from '@vue/test-utils';
import App from './App.vue';
describe('App', () => {
it('renders component tree', () => {
const wrapper = mount(App);
expect(wrapper.text()).toContain('Root');
expect(wrapper.text()).toContain('Leaf 1');
expect(wrapper.text()).toContain('Composite 1');
expect(wrapper.text()).toContain('Leaf 2');
});
});
12. 组合模式与错误处理
12.1 React 错误边界
javascript
import React from 'react';
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Something went wrong</div>;
}
return this.props.children;
}
}
const LeafComponent = ({ name }) => {
if (!name) throw new Error('Name is required');
return <div>{name}</div>;
};
const App = () => (
<ErrorBoundary>
<CompositeComponent name="Root">
<LeafComponent name="Leaf 1" />
<CompositeComponent name="Composite 1">
<LeafComponent name="" />
</CompositeComponent>
</CompositeComponent>
</ErrorBoundary>
);
12.2 Vue 错误处理
javascript
// App.vue
<template>
<div v-if="error">Something went wrong</div>
<CompositeComponent v-else name="Root">
<LeafComponent name="Leaf 1" />
<CompositeComponent name="Composite 1">
<LeafComponent name="" />
</CompositeComponent>
</CompositeComponent>
</template>
<script>
import { ref } from 'vue';
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default {
components: { LeafComponent, CompositeComponent },
setup() {
const error = ref(false);
return { error };
},
errorCaptured() {
this.error = true;
return false;
},
};
</script>
// LeafComponent.vue
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
props: ['name'],
mounted() {
if (!this.name) throw new Error('Name is required');
},
};
</script>
13. 组合模式与模块化
13.1 CommonJS
javascript
// LeafComponent.js
module.exports = function LeafComponent({ name }) {
return `<div>${name}</div>`;
};
// CompositeComponent.js
module.exports = function CompositeComponent({ name, children }) {
return `<div><h2>${name}</h2>${children.join('')}</div>`;
};
// App.js
const LeafComponent = require('./LeafComponent');
const CompositeComponent = require('./CompositeComponent');
const render = () => {
const leaf1 = LeafComponent({ name: 'Leaf 1' });
const leaf2 = LeafComponent({ name: 'Leaf 2' });
const composite1 = CompositeComponent({
name: 'Composite 1',
children: [leaf2],
});
return CompositeComponent({ name: 'Root', children: [leaf1, composite1] });
};
console.log(render());
13.2 ES Modules
javascript
// LeafComponent.mjs
export function LeafComponent({ name }) {
return `<div>${name}</div>`;
}
// CompositeComponent.mjs
export function CompositeComponent({ name, children }) {
return `<div><h2>${name}</h2>${children.join('')}</div>`;
}
// App.mjs
import { LeafComponent } from './LeafComponent.mjs';
import { CompositeComponent } from './CompositeComponent.mjs';
const render = () => {
const leaf1 = LeafComponent({ name: 'Leaf 1' });
const leaf2 = LeafComponent({ name: 'Leaf 2' });
const composite1 = CompositeComponent({
name: 'Composite 1',
children: [leaf2],
});
return CompositeComponent({ name: 'Root', children: [leaf1, composite1] });
};
console.log(render());
14. 组合模式与性能分析
14.1 性能测试
javascript
const start = performance.now();
const root = new Composite('Root');
for (let i = 0; i < 1000; i++) {
root.add(new Leaf(`Leaf ${i}`));
}
root.render();
const end = performance.now();
console.log(`Rendering took ${end - start}ms`);
14.2 React Profiler
javascript
import { Profiler } from 'react';
const App = () => (
<Profiler id="App" onRender={(id, phase, actualDuration) => {
console.log(`${id} ${phase} took ${actualDuration}ms`);
}}>
<CompositeComponent name="Root">
<LeafComponent name="Leaf 1" />
<CompositeComponent name="Composite 1">
<LeafComponent name="Leaf 2" />
</CompositeComponent>
</CompositeComponent>
</Profiler>
);
15. 组合模式与 Node.js 集成
15.1 服务端渲染
javascript
const express = require('express');
const app = express();
class LeafComponent {
constructor(name) {
this.name = name;
}
render() {
return `<div>${this.name}</div>`;
}
}
class CompositeComponent {
constructor(name) {
this.name = name;
this.children = [];
}
add(child) {
this.children.push(child);
}
render() {
const childrenHtml = this.children.map(c => c.render()).join('');
return `<div><h2>${this.name}</h2>${childrenHtml}</div>`;
}
}
app.get('/', (req, res) => {
const root = new CompositeComponent('Root');
const leaf1 = new LeafComponent('Leaf 1');
const composite1 = new CompositeComponent('Composite 1');
const leaf2 = new LeafComponent('Leaf 2');
root.add(leaf1);
root.add(composite1);
composite1.add(leaf2);
res.send(root.render());
});
app.listen(3000);
15.2 API 集成
javascript
const express = require('express');
const axios = require('axios');
const app = express();
class LeafComponent {
constructor(data) {
this.data = data;
}
render() {
return `<div>${this.data.name}</div>`;
}
}
class CompositeComponent {
constructor(name) {
this.name = name;
this.children = [];
}
add(child) {
this.children.push(child);
}
render() {
const childrenHtml = this.children.map(c => c.render()).join('');
return `<div><h2>${this.name}</h2>${childrenHtml}</div>`;
}
}
app.get('/', async (req, res) => {
const { data } = await axios.get('https://api.example.com/users');
const root = new CompositeComponent('Users');
data.forEach(user => root.add(new LeafComponent(user)));
res.send(root.render());
});
app.listen(3000);
16. 组合模式与事件处理
16.1 React 事件冒泡
javascript
const LeafComponent = ({ name, onClick }) => (
<div onClick={() => onClick(name)}>{name}</div>
);
const CompositeComponent = ({ name, onClick, children }) => (
<div onClick={() => onClick(name)}>
<h2>{name}</h2>
{children}
</div>
);
const App = () => {
const handleClick = name => console.log(`Clicked: ${name}`);
return (
<CompositeComponent name="Root" onClick={handleClick}>
<LeafComponent name="Leaf 1" onClick={handleClick} />
<CompositeComponent name="Composite 1" onClick={handleClick}>
<LeafComponent name="Leaf 2" onClick={handleClick} />
</CompositeComponent>
</CompositeComponent>
);
};
16.2 Vue 事件冒泡
javascript
// LeafComponent.vue
<template>
<div @click="$emit('custom-click', name)">{{ name }}</div>
</template>
<script>
export default {
props: ['name'],
emits: ['custom-click'],
};
</script>
// CompositeComponent.vue
<template>
<div @click="$emit('custom-click', name)">
<h2>{{ name }}</h2>
<slot />
</div>
</template>
<script>
export default {
props: ['name'],
emits: ['custom-click'],
};
</script>
// App.vue
<template>
<CompositeComponent name="Root" @custom-click="handleClick">
<LeafComponent name="Leaf 1" @custom-click="handleClick" />
<CompositeComponent name="Composite 1" @custom-click="handleClick">
<LeafComponent name="Leaf 2" @custom-click="handleClick" />
</CompositeComponent>
</CompositeComponent>
</template>
<script>
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default {
components: { LeafComponent, CompositeComponent },
methods: {
handleClick(name) {
console.log(`Clicked: ${name}`);
},
},
};
</script>
17. 组合模式与插件系统
17.1 React 插件
javascript
const withPlugin = (Component, plugin) => props => (
<Component {...props} {...plugin} />
);
const loggingPlugin = {
onClick: name => console.log(`Plugin logged: ${name}`),
};
const LeafComponent = ({ name, onClick }) => (
<div onClick={() => onClick(name)}>{name}</div>
);
const CompositeComponent = ({ name, onClick, children }) => (
<div>
<h2 onClick={() => onClick(name)}>{name}</h2>
{children}
</div>
);
const LoggedLeaf = withPlugin(LeafComponent, loggingPlugin);
const LoggedComposite = withPlugin(CompositeComponent, loggingPlugin);
const App = () => (
<LoggedComposite name="Root">
<LoggedLeaf name="Leaf 1" />
<LoggedComposite name="Composite 1">
<LoggedLeaf name="Leaf 2" />
</LoggedComposite>
</LoggedComposite>
);
17.2 Vue 插件
javascript
// Plugin.js
export const loggingPlugin = {
install(app) {
app.config.globalProperties.$logClick = name =>
console.log(`Plugin logged: ${name}`);
},
};
// App.vue
<template>
<CompositeComponent name="Root">
<LeafComponent name="Leaf 1" />
<CompositeComponent name="Composite 1">
<LeafComponent name="Leaf 2" />
</CompositeComponent>
</CompositeComponent>
</template>
<script>
import { createApp } from 'vue';
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
import { loggingPlugin } from './Plugin';
const app = createApp({
components: { LeafComponent, CompositeComponent },
});
app.use(loggingPlugin);
app.mount('#app');
</script>
// LeafComponent.vue
<template>
<div @click="$logClick(name)">{{ name }}</div>
</template>
<script>
export default {
props: ['name'],
};
</script>
// CompositeComponent.vue
<template>
<div>
<h2 @click="$logClick(name)">{{ name }}</h2>
<slot />
</div>
</template>
<script>
export default {
props: ['name'],
};
</script>
18. 组合模式与配置管理
18.1 React 配置
javascript
const createComponent = (type, config) => {
const components = {
leaf: LeafComponent,
composite: CompositeComponent,
};
const Component = components[type];
return props => <Component {...props} {...config} />;
};
const themedLeaf = createComponent('leaf', { className: 'theme-dark' });
const themedComposite = createComponent('composite', { className: 'theme-dark' });
const App = () => (
<themedComposite name="Root">
<themedLeaf name="Leaf 1" />
<themedComposite name="Composite 1">
<themedLeaf name="Leaf 2" />
</themedComposite>
</themedComposite>
);
18.2 Vue 配置
javascript
// ComponentFactory.js
export const createComponent = (type, config) => {
const components = {
leaf: 'LeafComponent',
composite: 'CompositeComponent',
};
return {
name: components[type],
props: config,
setup(props, { slots }) {
return () => (
<components[type] {...props} v-slots={slots} />
);
},
};
};
// App.vue
<template>
<ThemedComposite name="Root">
<ThemedLeaf name="Leaf 1" />
<ThemedComposite name="Composite 1">
<ThemedLeaf name="Leaf 2" />
</ThemedComposite>
</ThemedComposite>
</template>
<script>
import { createComponent } from './ComponentFactory';
import LeafComponent from './LeafComponent.vue';
import CompositeComponent from './CompositeComponent.vue';
export default {
components: {
LeafComponent,
CompositeComponent,
ThemedLeaf: createComponent('leaf', { class: 'theme-dark' }),
ThemedComposite: createComponent('composite', { class: 'theme-dark' }),
},
};
</script>