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

Backbone => React - Higher Order Components, inheritance and specialisation

我有一个遗留的 Backbone 应用程序,我已经开始用 React 重写它。该应用程序有一个主视图,其中包含两个按垂直排列的子视图。顶部面板显示一些数据,底部面板显示将这些数据作为输入的某种算法的结果。由于我有许多不同的数据源,每个数据源都应用了不同的算法,我有一个抽象的基础视图 class,然后我为每个数据源子 class,添加、装饰和覆盖方法如下必要的。有点像这样:

// 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);
    }
  },

})

这导致许多相似视图 class 的层次结构较浅。这一切都是非常必要的,但它是一个简单、直观且易于推理的模式,而且 有效 。然而,我正在努力了解如何在 React 中实现同样的事情。鉴于 subclassing of subclasses of React.Component 被认为是一种反模式,我的重点自然是放在组合上,尤其是高阶组件。 HOC(我觉得它很漂亮,但不直观,而且常常完全令人困惑)似乎涉及添加通用功能,而不是 specialising/refining 更通用的东西。我还考虑过通过 props 传递更专业版本的 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} />
    )
  }
}

显然,以上是一个非常简单的案例,但基本上捕获了我需要做的事情(尽管我当然会操纵状态,为了简单起见,该示例没有这样做)。我的意思是,我 可以 就那样做。但我想避免到处重复那个样板文件。那么有没有更简单优雅的方法呢?

通常,如果组件是无状态的并且不使用生命周期挂钩,则没有理由成为 Component class。在 JavaScript.

中,充当命名空间且不保存状态的 class 可以被视为反模式

与其他一些框架相比,React 没有需要映射变量以便它们在视图中可用的模板,因此唯一需要提及 bar 函数的地方是它被调用的地方。 JSX 是 JavaScript 的扩展,JSX 表达式可以使用当前范围内可用的任何名称。这允许在没有任何 classes:

的情况下组合函数
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' };

IMO 让你失望的不是继承还是组合,而是你的数据流:

For example, many of my derived views need to do custom rendering after the main render. I'm using a third-party SVG library, and the data rendered into the 'result' subview is derived from analysis of rendered SVG elements in the main data view above it

那么您在这里尝试做的是 child 在渲染后更新远距离相关组件的道具,对吗?像这样?

// 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>

有很多状态管理库可以帮助您解决这个问题,即在一个组件中生成数据并将其广播到远距离相关的组件。如果你想使用工具 built-in 来解决这个问题,你可能想使用 context,它为你提供了一个全局存储,你可以将其提供给任何想要使用它的组件。

在您的示例中,您的 child class 有 data-specific 方法(add,等等)。 IMO 更典型的反应是有一个通用的 class 来显示数据,并简单地将它作为道具传递给地图函数,以便 rearrange/transform 渲染数据。

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

回答我自己的问题,这是我以前从未想过的...

所以我的问题真的是担心我需要重构一个大型的、命令式的和有状态的代码库,以便与 React 的基于组合的模型(也与 Redux)集成。但是在阅读了对我的问题的(非常有洞察力和帮助的)回复后,我突然想到我的应用程序有两个并行部分:UI 和一个运行算法的引擎(实际上它是一个音乐分析引擎)。而且我可以很容易地去掉引擎连接到的 Backbone 视图层。因此,使用 React 的 context API 我构建了一个“AnalysisEngineProvider”,它使引擎可用于子组件。引擎都是非常命令式的,并且是经典的面向对象的,并且仍然使用 Backbone 模型,但这对 UI 没有影响,因为后者不了解其内部结构 - 这应该是这样的(模型也可能在某个时候被重构)...

该引擎还负责渲染 SVG(不是 BB 视图)。但是 React 对此一无所知。它只看到一个空 div。我从 div 中取出一个 ref 并将其传递给引擎,以便引擎知道在哪里渲染。除此之外,引擎和 UI 几乎没有联系——div 根本不会根据 React 状态更改进行更新(不过,很明显,UI 的其他组件是)。引擎中的模型只会触发对 SVG 的更新,而 React 对此一无所知。

我对这种方法很满意,至少目前是这样——即使它只是对完全 React 解决方案的增量重构的一部分。无论我碰巧使用什么框架,感觉都是适合应用程序的正确设计。