动态布局方案,组件接口和插槽思想

29 阅读2分钟

动态切换布局方案 方案优势

  • 插槽组件可以更换
  • 布局只管最外层布局,解耦组件和布局,可以被不同场景应用
  • 可拓展布局和新区域,可适配场景广
  • 布局可配置化,拓展新布局成本低

约束,组件需实现对应位置插槽的组件接口,且适配容器大小。 开发规范,注册的组件是展示组件。

效果

组件接口

export interface IHeaderAreaComponentProps {
  title: string;
}
export type HeaderAreaComponentType = React.ComponentType<IHeaderAreaComponentProps>;

插槽编程思想

export enum AreaName {
  Header = "header",
  Tools = "tools",
  Main = "main",
}

export enum SlotName {
  Header = "header",
  Tools = "tools",
  Main = "main",
}

注册表

export const ComponentRegistryMap: Record<AreaName, IComponentRegistry> = {
  [AreaName.Header]: {
    component: Header,
    areaName: AreaName.Header,
    slotName: SlotName.Header,
  },
  [AreaName.Tools]: {
    component: Tools,
    areaName: AreaName.Tools,
    slotName: SlotName.Tools,
  },
  [AreaName.Main]: {
    component: Main,
    areaName: AreaName.Main,
    slotName: SlotName.Main,
  },
};

布局配置表

export const leftConfig: ILayoutConfig = {
  gridColumns: "230px 1fr",
  gridRows: "60px 1fr",
  gridGap: "20px",
  areasConfig: [
    [ComponentRegistryMap.header, ComponentRegistryMap.header],
    [ComponentRegistryMap.tools, ComponentRegistryMap.main],
  ],
};

export const rightConfig: ILayoutConfig = {
  gridColumns: "1fr 230px",
  gridRows: "60px 1fr",
  gridGap: "20px",
  areasConfig: [
    [ComponentRegistryMap.header, ComponentRegistryMap.tools],
    [ComponentRegistryMap.main, ComponentRegistryMap.tools],
  ],
};

布局引擎代码

class LayoutGridEngine {
  private _config = {} as ILayoutConfig;
  private slotList: IComponentRegistry[] = [];

  constructor(config: ILayoutConfig) {
    this._config = cloneDeep(config);
    this.slotList = [...new Set(config.areasConfig.flat())];
    makeAutoObservable(this, {}, { autoBind: true });
  }

  get config() {
    return this._config;
  }

  changeConfig(config: ILayoutConfig) {
    this._config = config;
  }

  render(areaData: EngineData, props: IProps) {
    const { config, slotList } = this;
    return (
      <div
        className={props.className}
        style={{
          display: "grid",
          gridTemplateColumns: config.gridColumns,
          gridTemplateRows: config.gridRows,
          gridTemplateAreas: config.areasConfig
            .map((area) => area.map((item) => item.areaName).join(" "))
            .reduce((prev, cur) => prev + `"${cur}"`, ""),
          gap: config.gridGap,
        }}
      >
        {slotList.map((slot) => {
          const { component, areaName, slotName } = slot;
          const Component = component;
          return (
            <SlotContainer
              key={slotName}
              className={classNames(areaName, "min-w-0 min-h-0")}
              style={{ gridArea: areaName }}
            >
              <Component {...areaData[slotName]} />
            </SlotContainer>
          );
        })}
      </div>
    );
  }
}

视图层

function App() {
  const layoutEngine = useMemo(() => new LayoutGridEngine(leftConfig), []);
  return (
    <div className="w-900px mx-auto">
      <div className="h-60px flex items-center justify-center gap-6">
        <Button
          onClick={() => {
            layoutEngine.changeConfig(leftConfig);
          }}
        >
          Left
        </Button>
        <Button
          onClick={() => {
            layoutEngine.changeConfig(rightConfig);
          }}
        >
          Right
        </Button>
      </div>
      <div>
        {layoutEngine.render(
          {
            [AreaName.Header]: {
              title: "this is header.",
            },
            [AreaName.Tools]: {
              title: "this is tools.",
            },
            [AreaName.Main]: {
              title: "this is main.",
            },
          },
          {
            className: "w-full h-full",
          }
        )}
      </div>
    </div>
  );
}

动态布局用grid做还是有些问题的,兼容性问题还好,但grid在有些场景并不适用,需求简单还是可以用用。可以定义接口,视图层依赖接口,后续可以切新的渲染引擎。代码是比较简单的,配置写的比较简单,主要是给各位前端的同学们提供动态布局的思路,当然方案还有很多完善的地方,但插槽思路和组件协议还是让方案具备一定可行性。