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>
相关推荐
PAK向日葵1 小时前
【算法导论】如何攻克一道Hard难度的LeetCode题?以「寻找两个正序数组的中位数」为例
c++·算法·面试
灵感__idea1 小时前
JavaScript高级程序设计(第5版):好的编程就是掌控感
前端·javascript·程序员
烛阴2 小时前
Mix
前端·webgl
代码续发2 小时前
前端组件梳理
前端
试图让你心动3 小时前
原生input添加删除图标类似vue里面移入显示删除[jquery]
前端·vue.js·jquery
陈不知代码3 小时前
uniapp创建vue3+ts+pinia+sass项目
前端·uni-app·sass
小王码农记3 小时前
sass中@mixin与 @include
前端·sass
陈琦鹏3 小时前
轻松管理 WebSocket 连接!easy-websocket-client
前端·vue.js·websocket
hui函数4 小时前
掌握JavaScript函数封装与作用域
前端·javascript
行板Andante4 小时前
前端设计中如何在鼠标悬浮时同步修改块内样式
前端