动态切换布局方案 方案优势
- 插槽组件可以更换
- 布局只管最外层布局,解耦组件和布局,可以被不同场景应用
- 可拓展布局和新区域,可适配场景广
- 布局可配置化,拓展新布局成本低
约束,组件需实现对应位置插槽的组件接口,且适配容器大小。 开发规范,注册的组件是展示组件。
效果
组件接口
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在有些场景并不适用,需求简单还是可以用用。可以定义接口,视图层依赖接口,后续可以切新的渲染引擎。代码是比较简单的,配置写的比较简单,主要是给各位前端的同学们提供动态布局的思路,当然方案还有很多完善的地方,但插槽思路和组件协议还是让方案具备一定可行性。