Backbone =>React -高阶组件、继承和专门化

bnlyeluc  于 2022-11-10  发布在  React
关注(0)|答案(3)|浏览(150)

我有一个遗留的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子类的子类化被认为是一种反模式,我的重点自然是复合。特别是高阶分量。(我觉得这很漂亮,但不直观,而且经常是彻头彻尾的混乱)似乎涉及到添加一般特性,而不是专门化/细化一些更通用的东西。我还考虑过通过道具传递更专门化版本的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} />
    )
  }
}

显然,上面的例子是一个非常简单的例子,但本质上抓住了我需要做的事情(虽然我当然会操纵状态,但为了简单起见,这个例子并没有这样做)。我的意思是,我 * 可以 * 做这样的事情。但我想避免到处重复这个样板。那么,有没有更简单、更优雅的方法来做这件事呢?

htrmnn0y

htrmnn0y1#

一般来说,如果一个组件是无状态的,并且不使用生命周期钩子,那么它就没有理由成为Component类。一个作为命名空间的类,并且不保存状态,可以被认为是JavaScript中的反模式。
与其他一些框架相比,React没有需要Map变量才能在视图中使用的模板,所以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赋值,而不用在每个组件调用上创建新的()=>"foo"函数:

function GeneralComponent({ foo }) {
  return (
    <div>
      <span> {foo()} </span>
    </div>
  )
}

GeneralComponent.defaultProps = { foo: () => 'foo' };
z3yyvxxp

z3yyvxxp2#

IMO让您感到困惑的不是继承与组合,而是数据流:
例如,我的许多派生视图需要在主呈现之后进行自定义呈现。我使用的是第三方SVG库,呈现到'result'子视图中的数据是通过分析其上方的主数据视图中呈现的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更典型的做法是使用一个泛型类来显示数据,并将其作为道具传递给map函数,以便重新排列/转换呈现的数据。

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模式与跨兄弟组件共享数据存储的主要问题无关。

nhjlsmyf

nhjlsmyf3#

回答我自己的问题,我从来没有想过...
所以我的问题实际上是因为我需要重构一个大型的、命令式的和有状态的代码库,以便与React的基于组合的模型(也与Redux)集成。但在阅读了对我的问题的(非常有见地和有帮助的)回答后,我突然想到我的应用有两个并行的部分:UI和一个运行算法的引擎(实际上是一个音乐分析引擎)。我可以很容易地剥离与引擎相连的Backbone View层。因此,使用React的context API,我构建了一个“AnalysisEngineProvider”,它使引擎可用于子组件。该引擎都是非常命令式的,经典的面向对象,仍然使用Backbone模型。但这对UI没有影响,因为后者不了解它的内部结构--这是它应该的样子(模型也可能在某个时候被重构)。
该引擎还负责渲染SVG(不支持BB视图)。但是React对此一无所知。它只看到一个空div。我从div中获取一个ref并将其传递给引擎,这样引擎就知道在哪里进行渲染。除此之外,引擎和UI几乎没有联系--div根本不会根据React状态的变化而更新(UI的其他组件显然是这样的)引擎中的模型只会触发SVG的更新,而React对此一无所知。
我对这种方法很满意,至少现在是这样--即使它只是一个完整React解决方案的增量重构的一部分。无论我碰巧使用什么框架,它都是适合应用的设计。

相关问题