【发布时间】:2019-06-23 14:41:26
【问题描述】:
我正在构建一个用户可以动态更改页面布局的应用程序。有两个接口。它们一起提供了可用于构建布局的组件集合:
export interface IComponentProvider {
key : string;
displaylabel : string;
description ?: string;
render(): React.ReactNode;
}
export interface IComponentCollection {
forEach(callbackfn: (value: IComponentProvider, key: string) => void, thisArg?: any): void;
get(key: string): IComponentProvider | undefined;
has(key: string): boolean;
readonly size: number;
}
注意事项:
-
IComponentProvider可以通过React.Component实现。它实际上是一个特殊的组件,有一个键和一个显示标签。密钥用于标识集合中的组件。 displaylabel 用于给它一个友好的名称。 (因此用户将看到“用户详细信息”而不是“userDetailPanelId132”)。 -
Map<string, React.Component>实现IComponentCollection。实际上,在大多数情况下,我会使用 Map 来实现它。
应该使用两个类来呈现布局。有一个DynamicLayoutConfiguration 类定义了布局的结构。它存储组件类型和组件引用的树。例如:
{
type: "tabs"
items: [
{type:"tab", title:"First tab",
items:[
{
type:"component",
key:"masterComponent1"}
]},
{type:"tab", title:"Second tab",
items:[
{
type:"component",
key:"detailPanel1"}
]},
]
}
布局配置不是一个组件,它不包含实际的组件。只有它们是这些组件的关键。可以保存/加载配置。这为布局提供了持久性。
最后一部分是DynamicLayout组件,它获取一个集合和一个配置,它应该能够渲染布局:
interface IDynamicLayoutProps {
configuration: DynamicLayoutConfiguration;
collection: IComponentCollection;
}
export class DynamicLayout extends React.PureComponent<IDynamicLayoutProps> {
public render() {
/* Here we should render, using this.props.configuration and this.props.components */
return null; // ???
}
}
上面的示例配置应该呈现如下内容:
<Tabs>
<TabList>
<Tab>First tab</Tab>
<Tab>Second tab</Tab>
</TabList>
<TabPanel>{this.props.components.get('masterComponent1').render()}</TabPanel>
<TabPanel>{this.props.components.get('detailPanel1').render()}</TabPanel>
</Tabs>
为什么我需要这个:因为相同的数据必须以各种方式呈现给用户,并且我希望用户能够定义自己的布局。有一个单独的布局编辑器,用户可以在其中创建带有容器、行、列、选项卡、手风琴等的 UI 组件树结构,并且他们可以将组件插入到此布局中(来自组件集合)。
问题来了。显然,在用户创建布局配置之前需要创建组件集合。例如,如果我有一个名为 DetailPanel 的组件:
export class DetailPanel extends React.Component<...>{...}
那么理论上,我可以这样做:
const components = new Map<string,IComponentProvider>();
const detailPanel = new DetailPanel(detailPanelProps);
components.set(detailPanel.key, detailPanel);
然后为用户打开布局编辑器,他可以在其中使用 DetailPanel(如果他愿意的话)。但请注意,DetailPanel 是使用 new 关键字创建的。
如何渲染以这种方式创建的组件?显然,如果我只是从渲染方法返回该对象,那么我会得到一个“对象作为反应子无效”的错误。我试图返回render()的结果:
<TabPanel>
{this.props.components.get(componentKey).render()}
</TabPanel>
虽然这会渲染组件,但它不会调用componentDidMount 或componentWillUnmount,通常它是有缺陷的,无法使用。
我认为我不应该使用new DetailPanel() 手动创建这些组件。
但我不知道还能怎么办?如何在不创建组件的情况下创建一般的“组件集合”?如果我必须手动创建它们,那么如何正确渲染它们?
更新:正如@Berouminum 指出的那样,没有好的解决方案。只能使用 JSX 或 React.createElement 渲染元素。这些都不会真正立即创建对象,只有在需要它们时。这意味着要么我使用 React.createElement 创建它们,然后我无法访问 key 和 displaylabel 属性,要么使用 new 关键字(普通对象构造)创建它们,但我将无法渲染正确的。
到目前为止,我发现的最佳解决方法是将 IComponentProvider 更改为具有“createElement”方法而不是渲染方法:
export interface IComponentProvider {
key : string;
displaylabel : string;
description ?: string;
createElement(): React.ReactElement;
}
任何想要实现这个接口的组件,createElement 方法都可以简单实现:
class ExampleComponent extends React.Component<IProps,IState> impements IComponentProvider {
public get key() { return this.calculateKey(); }
public get displaylabel() { return this.calculateDisplaylabel(); }
public get description() { return this.calculateDescription(); }
public createElement() {
return React.createElement(ExampleComponent,this.props, null);
}
}
有了这个,就可以使用key/displaylabel属性,调用对象上的方法,并正确渲染:
const exampleComponent = new ExampleComponent(props);
/* It is possible to use its properties here. */
components.set(exampleComponent.key, exampleComponent);
/* It is also possible to render it PROPERLY, all lifecycle methods called correctly. */
class SomeContainer extends React.Component<...> {
...
public render() {
return <div>
....
{exampleComponent.createElement()}
...
</div>;
}
当然,这个解决方案也是有缺陷的,因为对象会被构造多次(一次手动使用新的关键字,并且可能在需要元素时由 React 多次创建)。所以被渲染的对象实际上是一个不同的实例。但到目前为止,这是我发现的最好的解决方法。它可重复使用且足够通用。
更新 2:如果我尝试将它与 MobX 一起使用,提供的解决方案将完全被破坏。这是因为渲染组件和原始组件是不同的对象。如果我在原始对象上设置一个 observable,那么它不会对渲染对象产生任何影响。有一个非常混乱的解决方法,但可能不值得。
【问题讨论】:
标签: reactjs