JS 组合模式在组件化开发中的应用:从原理到实战

在现代前端开发中,组件化开发已成为主流,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 优化

使用 defineComponentreactive 优化响应式数据:

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>
相关推荐
xjt_09019 分钟前
浅析Web存储系统
前端
一只叫煤球的猫28 分钟前
手撕@Transactional!别再问事务为什么失效了!Spring-tx源码全面解析!
后端·spring·面试
foxhuli2291 小时前
禁止ifrmare标签上的文件,实现自动下载功能,并且隐藏工具栏
前端
青皮桔1 小时前
CSS实现百分比水柱图
前端·css
失落的多巴胺1 小时前
使用deepseek制作“喝什么奶茶”随机抽签小网页
javascript·css·css3·html5
DataGear1 小时前
如何在DataGear 5.4.1 中快速制作SQL服务端分页的数据表格看板
javascript·数据库·sql·信息可视化·数据分析·echarts·数据可视化
影子信息1 小时前
vue 前端动态导入文件 import.meta.glob
前端·javascript·vue.js
青阳流月1 小时前
1.vue权衡的艺术
前端·vue.js·开源
样子20181 小时前
Vue3 之dialog弹框简单制作
前端·javascript·vue.js·前端框架·ecmascript
kevin_水滴石穿1 小时前
Vue 中报错 TypeError: crypto$2.getRandomValues is not a function
前端·javascript·vue.js