我有一个传统的Backbone应用程序,我已经开始在React中重写了。该应用程序的主视图包含两个子视图,以兽性方式排列。顶部面板显示一些数据,底部显示一些算法的结果,将此数据作为输入。由于我有许多不同的数据源,每个都有不同的算法应用于它,我有一个抽象的基础View类,然后我为每个数据源子类化,根据需要添加,装饰和覆盖方法。有点像这样:
// Base View.
const BaseView = Backbone.View.extend({
events: {},
initialize() {
this.subViewA = // instantiate subview...
this.subViewB = // instantiate subview...
},
generateResultData() {
// 'Abstract' method which should be specialised to generate data rendered by subViewB...
},
render() {
// render subviews...
},
});
// Derived View.
const Derived = BaseView.extend({
events: {
// event handlers...
},
add(a, b) {
return a+b;
},
// additional methods...
generateResultData() {
return {
result: this.add(2,2);
}
},
})
这导致许多类似View类的浅层次结构。这一切都非常迫切,但它是一个简单,直观且易于推理的模式,并且只是有效。然而,我正在努力想看看如何在React中实现同样的目标。鉴于React.Component
的子类的子类被认为是反模式,我的重点自然是组合,特别是高阶组件。 HOC(我发现它很漂亮,但不直观,通常只是令人困惑)似乎涉及添加一般功能,而不是专门/提炼更一般的东西。我还考虑过通过道具传递更多专业版的Componenet方法。但这只是意味着我必须一遍又一遍地使用相同的样板组件定义:
// General functional component, renders the result of prop function 'foo'.
function GeneralComponent(props) {
const foo = this.props.foo || ()=>"foo";
return (
<div>
<span> { this.props.foo() } </span>
</div>
)
}
// Specialised component 1, overrides 'foo'.
class MySpecialisedComponent extends React.Component {
foo() {
return this.bar()
}
bar() {
return "bar"
}
render() {
return (
<GeneralComponent foo={this.foo} />
)
}
}
// Specialised component 2, overrides 'foo' and adds another method.
class MyOtherSpecialisedComponent extends React.Component {
foo() {
return this.bar() + this.bar()
}
bar() {
return "bar"
}
baz() {
return "baz"
}
render() {
return (
<GeneralComponent foo={this.foo} />
)
}
}
显然,上面是一个非常简单的情况,但基本上捕获了我需要做的事情(虽然我当然会操纵状态,为简单起见,这个例子没有做到)。我的意思是,我可以做那样的事情。但我想避免在整个地方重复这个样板。那么有更简单,更优雅的方式吗?
什么让你失望的IMO不是继承与组合,它是你的数据流:
例如,我的许多派生视图需要在主渲染之后进行自定义渲染。我正在使用第三方SVG库,渲染到“结果”子视图中的数据来自对其上方主数据视图中渲染的SVG元素的分析
所以你在这里尝试做的是在渲染后有一个远程相关组件的子更新道具,对吗?像这样?
// after the svg renders, parse it to get data
<div id="svg-container">
<svg data="foo" />
<svg data="bar />
</div>
// show parsed data from svg after you put it through your algos
<div id="result-container">
// data...
</div>
有很多状态管理库可以帮助您解决这个问题,即在一个组件中生成数据并将其广播到远端相关组件。如果您想使用内置工具来解决此问题,您可能需要使用context
,它为您提供了一个全局存储,您可以为任何想要使用它的组件提供该存储。
在您的示例中,您的子类具有特定于数据的方法(add
等)。 IMO更常见的是有一个用于显示数据的通用类,只是将它作为道具传递给地图函数,以重新排列/转换渲染数据。
class AbstractDataMap extends PureComponent {
static defaultProps = {
data: [],
map: (obj, i) => (<div key={i}>{obj}</div>)
};
render() {
const { data, map, children } = this.props;
const mapped = data.map(map);
return (
<Fragment>
{mapped.map((obj, i) => (
children(obj, i)
))}
</Fragment>
);
}
}
// in some other container
class View extends Component {
render() {
return (
<div>
<AbstractDataMap data={[1, 2, 3]} map={(n) => ({ a: n, b: n + 1 })}>
{({ a, b }, i) => (<div key={i}>a: {a}, b: {b}</div>)}
</AbstractDataMap>
<AbstractDataMap data={[2, 4, 6]} map={(n) => (Math.pow(n, 2))}>
{(squared, i) => (<div key={i}>squared: {squared}</div>)}
</AbstractDataMap>
</div>
);
}
}
IMO使用HOC抽象掉你在渲染调用(以及其他用途)中明确使用.map
的工作模式是你正在寻找的模式。但是,正如我上面所说,HOC模式与兄弟组件之间的共享数据存储的主要问题无关。
通常,如果组件是无状态的并且不使用生命周期钩子,则没有理由将它作为Component
类。充当命名空间但不保持状态的类可以被视为JavaScript中的反模式。
与其他一些框架相比,React没有模板需要映射变量才能使它们在视图中可用,所以需要提到bar
函数的唯一地方就是它被调用的地方。 JSX是JavaScript的扩展,JSX表达式可以使用当前范围内可用的任何名称。这允许在没有任何类的情况下编写函数:
const getBar => "bar";
const getBaz => "baz";
const getBarBaz => getBar() + getBaz();
const MySpecialisedComponent = props => <GeneralComponent foo={getBar} />;
const MyOtherSpecialisedComponent = props => <GeneralComponent foo={getBarBaz} />;
匿名函数可以作为foo
prop传递,而不是创建getBarBaz
,但这通常是不鼓励的,因为不必要的开销。
此外,可以使用defaultProps
分配默认prop值,而无需在每个组件调用上创建新的()=>"foo"
函数:
function GeneralComponent({ foo }) {
return (
<div>
<span> {foo()} </span>
</div>
)
}
GeneralComponent.defaultProps = { foo: () => 'foo' };
回答我自己的问题,这是我以前从未做过的......
所以我的问题确实来自于我需要重构一个庞大的,命令式的和有状态的代码库,以便与React的基于组合的模型(也与Redux)集成。但是在我阅读了(非常有见地和有帮助的)对我的问题的回答后,我发现我的应用程序有两个平行的部分:用户界面和运行算法的引擎(实际上它是一个音乐分析引擎)。我可以很容易地剥离引擎所连接的Backbone View层。因此,使用React的context API我构建了一个'AnalysisEngineProvider',它使引擎可用于子组件。这个引擎都是非常强制性和经典的面向对象,并且仍然使用Backbone模型,但这对UI没有任何影响,因为后者不知道它的内部 - 它应该是什么样的(模型很可能会被重构出来)在某些时候也)...
引擎还负责渲染SVG(不使用BB视图)。但是React对此一无所知。它只看到一个空div。我从div中取出一个ref并将其传递给引擎,以便后者知道在哪里渲染。除此之外,引擎和UI几乎没有联系 - div根本不会从React状态更改中更新(显然,UI的其他组件也是如此)。引擎中的模型只会触发SVG的更新,而React对此一无所知。
我对这种方法感到满意,至少现在是这样 - 即使它只是完全React解决方案的渐进式重构的一部分。无论我碰巧使用哪种框架,感觉就像是应用程序的正确设计。