代码之家  ›  专栏  ›  技术社区  ›  ChrisM

主干=>反应-高阶组件、继承和专业化

  •  1
  • ChrisM  · 技术社区  · 7 年前

    我有一个传统的主干应用程序,我已经开始在React中重写它。该应用程序有一个主视图,包含两个子视图,按审核方式排列。上面的面板显示一些数据,下面的面板显示一些将这些数据作为输入的算法的结果。由于我有许多不同的数据源,每一个都应用了不同的算法,所以我有一个抽象的基础视图类,然后我为每个数据源子类,根据需要添加、修饰和重写方法。有点像这样:

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

    这导致了许多类似视图类的浅层次结构。这是非常必要的,但这是一个简单、直观和易于理解的模式,而且 作品 . 然而,我正在努力寻找如何在反应中实现同样的事情。考虑到 React.Component 被认为是反模式,我的重点自然是组成,特别是高阶组件。hocs(我觉得它很漂亮,但不具有说服力,而且常常完全令人困惑)似乎涉及到添加一般功能,而不是专门化/改进更一般的功能。我还考虑通过props传递更专业的组件方法版本。但这仅仅意味着我必须反复使用相同的样板组件定义:

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

    显然,上面是一个非常简单的例子,但基本上捕捉到了我需要做的事情(尽管为了简单起见,我当然是在操纵状态,这个例子不做)。我是说,我 能够 就这么做吧。但我想避免在这里重复那个样板文件。那么,有没有一种更简单、更优雅的方法呢?

    3 回复  |  直到 7 年前
        1
  •  1
  •   Daniel Lizik    7 年前

    在我看来,抛弃你的不是继承和组合,而是你的数据流:

    例如,我的许多派生视图需要在主渲染之后进行自定义渲染。我使用的是第三方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函数以重新排列/转换呈现的数据的通用类在react中更为典型。

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

    在我看来,这种使用hoc来抽象显式使用的劳动的模式 .map 在渲染调用(以及其他用途)中,是您要查找的模式。但是,正如我上面所述,hoc模式与跨兄弟组件共享数据存储的主要问题无关。

        2
  •  2
  •   Estus Flask    7 年前

    通常,如果一个组件是无状态的,并且不使用生命周期挂钩,那么它没有理由 Component 类。在JavaScript中,充当命名空间且不保持状态的类可以被视为反模式。

    在constrast到其他一些框架中,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 道具而不是创造 getBarBaz 但由于不必要的开销,这通常是不可取的。

    此外,可以使用 defaultProps 不创建新的 ()=>"foo" 每个组件调用的函数:

    function GeneralComponent({ foo }) {
      return (
        <div>
          <span> {foo()} </span>
        </div>
      )
    }
    
    GeneralComponent.defaultProps = { foo: () => 'foo' };
    
        3
  •  0
  •   ChrisM    7 年前

    回答我自己的问题,这是我从来没有做过的…

    因此,我的问题实际上源于这样一个问题:我需要重构一个大型的、必需的、有状态的代码库,以便与基于reacts组合的模型(也与redux)集成。但在阅读了(非常有洞察力和帮助性)对我的问题的回答后,我突然想到我的应用程序有两个并行部分:用户界面和运行算法的引擎(实际上它是一个音乐分析引擎)。我可以很容易地去掉引擎连接到的主干视图层。所以,使用反应 context API我已经构建了一个AnalysisEngineeProvider”,它使引擎可用于子组件。引擎都是非常必要的,经典的面向对象的,并且仍然使用主干模型,但是这对UI没有影响,因为后者不知道它的内部结构——这就是它应该如何(模型也可能在某个时刻被重构出来)。

    引擎还负责呈现SVG(不带BB视图)。但React对此一无所知。它只是看到一个空的分区。我拿一个 ref 然后把它传给引擎,让引擎知道在哪里渲染。除此之外,引擎和用户界面几乎没有联系——Div根本就不会根据反应状态的变化进行更新(显然,用户界面的其他组件也是如此)。引擎中的模型只会触发SVG的更新,而SVG对此一无所知。

    我对这种方法很满意,至少现在是这样——即使它只是针对完全反应解决方案的增量重构的一部分。无论我使用的是什么框架,它都是应用程序的正确设计。

    推荐文章