[译]React高级指引8:与第三方库协同

1,160 阅读10分钟

原文链接:reactjs.org/docs/integr…

引言

React可以在任何web应用中使用。它可以与其他应用相互嵌套。本章将会介绍一些常用的嵌套的例子,主要侧重于jQueryBackbone。但是使用这些的思想同样可以应用到其他整合案例中。

集成带有DOM操作的插件

React无法感知到在React之外DOM改变。它根据内部虚拟DOM的改变来更新,但是如果DOM节点被第三方库更改了,那么React就会感到困惑并且没有办法恢复。

但这并不意味着将React和其他可以改变DOM节点的方式结合很难或者不可能,你只需要注意他们各自做了什么就可以了。

避免冲突最简单的方法就是阻止React组件更新。你可以渲染一个无需渲染的元素,比如空的<div>

如何解决这个问题

为了展示这个,我们来编写一个用于常用jQuery插件的wrapper。

我们将会将一个ref绑定在根DOM元素。在componentDidMount方法中,我们将会获取到它的引用,这样我们就可以把它传递给jQuery插件了。

为了防止React在DOM挂载之后触碰它,我们将会在render方法中返回一个空<div>。这个<div>元素没有任何属性或者子元素,所以React无法更新它,这样jQuery插件就可以自由地管理这个DOM元素了:

class SomePlugin extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.somePlugin();
  }

  componentWillUnmount() {
    this.$el.somePlugin('destroy');
  }

  render() {
    return <div ref={el => this.el = el} />;
  }
}

应该注意到了我们在这里声明了componentDidMountcomponentWillUnmount两个生命周期函数。许多jQuery插件都在DOM元素上绑定了事件监听器,所以在componentWillUnmount函数中为其解绑是很重要的。如果插件没有提供解绑的方法,那么你据需要自行编写一个方法来清除所有的监听器了。请记得移除所有插件注册的事件监听器以免内存泄漏。

集成jQuery Chosen插件

为了给这些概念一个更具体的例子,我们来为Chosen插件编写一个简略的wrapper,它能需要输入一个<select>参数。

注意:

这只是一种方式,但不是最佳地使用React的方式。我们推荐尽可能地使用React组件。React组件在React应用中能够被更容易地复用,并且提供了更多的对自身行为和样式的控制。

首先,我们来看看Chosen对DOM节点做了什么。

如果你在<select>DOM节点上调用了它,那么它会获取原始DOM节点上的属性并且用一个内联样式隐藏它。之后在<select>后面添加上它自身提供样式的DOM节点。在这之后它就激活了jQuery时间来通知我们DOM节点的更改。

以下是我们最终要实现的代码结果:

function Example() {
  return (
    <Chosen onChange={value => console.log(value)}>
      <option>vanilla</option>
      <option>chocolate</option>
      <option>strawberry</option>
    </Chosen>
  );
}

为了简单起见,我们将以非受控组件来实现它。

首先,我们创建一个带有render方法的空组件,在render方法中我们返回一个包裹了<select>元素的<div>

class Chosen extends React.Component {
  render() {
    return (
      <div>
        <select className="Chosen-select" ref={el => this.el = el}>
          {this.props.children}
        </select>
      </div>
    );
  }
}

注意在这里我们将<select>包裹在一个额外的<div>元素中。这是很重要的,因为Chosen会在我们传递的<select>元素后面添加其他DOM元素。然而,就React而言,<div>只有一个子元素。这也是我们为什么能确定React的更新机制不会与Chosen添加的额外DOM节点冲突的原因。如果你在React数据流之外修改DOM节点,那么你必须确保React不会去更新这些DOM节点。

接下来,我们将会实现生命周期方法。我们需要在componentDidMount方法中初始化赋予<select>节点的ref,并且在componentWillUmount中将其销毁:

componentDidMount() {
  this.$el = $(this.el);
  this.$el.chosen();
}

componentWillUnmount() {
  this.$el.chosen('destroy');
}

注意React不会给this.el字段赋予特殊的含义。只有我们在render方法中将一个ref的值赋予给它时它才会有具体的作用:

<select className="Chosen-select" ref={el => this.el = el}>

现在我们所作的已经能够让我们的组件被渲染了,但是我们还需要实时感知到值的修改。为了实现这个,我们需要订阅由Chosen管理的在<select>元素上的jQuery change事件。

由于组件的props会随时间更改,所以我们不会将this.props.onChange直接传递给Chosen,这其中也包含了事件处理权柄。对应的,我们将会声明一个handleChange()来调用this.props.onChange,并且在jQuery的change事件上注册它:

componentDidMount() {
  this.$el = $(this.el);
  this.$el.chosen();

  this.handleChange = this.handleChange.bind(this);
  this.$el.on('change', this.handleChange);
}

componentWillUnmount() {
  this.$el.off('change', this.handleChange);
  this.$el.chosen('destroy');
}

handleChange(e) {
  this.props.onChange(e.target.value);
}

现在,只剩下一件事了。在React中,props是会随时间变化的。举个例子,如果父组件的状态改变的话,<Chosen>组件可能会得到不同的子元素。这意味着从集成的角度来说,手动更新DOM以应对prop的更新是非常重要的,因为我们没有让React来替我们管理DOM。

Chosen的官方文档推荐我们使用jQuery trigger() API来通知对原始DOM元素的改变。我们将会让React来管理<select>中的this.props.children的更新,同时我们也会在componentDidUpdate() 生命周期方法中通知Chosen关于子元素列表的改变:

componentDidUpdate(prevProps) {
  if (prevProps.children !== this.props.children) {
    this.$el.trigger("chosen:updated");
  }
}

这样,当React管理的<select>的子元素更改时,Chosen就会去更新它的DOM元素。

完整的Chosen组件实现如下:

class Chosen extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.chosen();

    this.handleChange = this.handleChange.bind(this);
    this.$el.on('change', this.handleChange);
  }
  
  componentDidUpdate(prevProps) {
    if (prevProps.children !== this.props.children) {
      this.$el.trigger("chosen:updated");
    }
  }

  componentWillUnmount() {
    this.$el.off('change', this.handleChange);
    this.$el.chosen('destroy');
  }
  
  handleChange(e) {
    this.props.onChange(e.target.value);
  }

  render() {
    return (
      <div>
        <select className="Chosen-select" ref={el => this.el = el}>
          {this.props.children}
        </select>
      </div>
    );
  }
}

与其他视图库整合

由于React.render()的自由性,React可以在其他应用中嵌套使用。

尽管React通常被用作在启动时在DOM中加载单一的根组件,但是React.render()也可以在各自独立的UI中多次调用,比如一个按钮或者一整个应用。

事实上,这也是Facebook中使用React的方式。这让我们在应用中一小块一小块地使用React,并和已有的服务端生成的模板和其他客户端代码结合。

使用React替换基于字符串的渲染

在老的web应用中一个常用的模式是将DOM语块作为字符串处理并将它插入到DOM节点中,例如$el.html(htmlString)。代码库中的这些例子非常适合引入React。只需要将基于字符串的渲染重写成React组件即可。

让我们来看下面通过jQuery实现的代码:

$('#container').html('<button id="btn">Say Hello</button>');
$('#btn').click(function() {
  alert('Hello!');
});

它可以被转化成React组件:

function Button() {
  return <button id="btn">Say Hello</button>;
}

ReactDOM.render(
  <Button />,
  document.getElementById('container'),
  function() {
    $('#btn').click(function() {
      alert('Hello!');
    });
  }
);

从这里开始,你可以把更多的逻辑代码编写到组件中并且开始应用更多的React实践。比如,在组件中最好不要使用ID,因为同一个组件可能会被渲染多次。对应地,我们将会使用React事件处理系统并且直接在React元素<button>上注册事件处理器:

function Button(props) {
  return <button onClick={props.onClick}>Say Hello</button>;
}

function HelloButton() {
  function handleClick() {
    alert('Hello!');
  }
  return <Button onClick={handleClick} />;
}

ReactDOM.render(
  <HelloButton />,
  document.getElementById('container')
);

只要你喜欢,你可以创建多个独立的这种组件。并且通过React.render()把它们渲染到不同的DOM容器中。当你逐渐地将你的应用向React转变,你就可以将它们组合进更大的组件中,并且以此调整某些React.render()所处的层级。

把React嵌入到Backbone视图

Backbone通常使用HTML字符串或者返回字符串的模板函数来创建DOM元素的内容。这个过程也可以用渲染React组件来代替。

在下面的例子中,我们将会创建一个Backbone视图:ParagraphView。它会重载Backbone的render()函数来讲一个React组件<Paragraph>渲染到Backbone(this.el)提供的DOM元素中。在这里,我们使用React.render():

function Paragraph(props) {
  return <p>{props.text}</p>;
}

const ParagraphView = Backbone.View.extend({
  render() {
    const text = this.model.get('text');
    ReactDOM.render(<Paragraph text={text} />, this.el);
    return this;
  },
  remove() {
    ReactDOM.unmountComponentAtNode(this.el);
    Backbone.View.prototype.remove.call(this);
  }
});

在remove方法中调用ReactDOM.unmountComponentAtNode()是非常重要的,因为这样React就可以在解除的时候移除事件处理器以及组件树上其他资源的关联。

当一个组件从React组件树中移除时,清理工作是自动完成的,但是由于我们是手动地移除整个树,所以我们需要在手动地调用这个方法。

和Model层集成

虽然通常推荐使用类似React stateFluxRedux这类的单向数据流,React组件也可以使用其他框架和库的model层。

在React组件中使用Backbone的Model

在React组件中使用Backbone的model和集合最简单的方法是监听不同的数据变化并且手动强制更新。

负责渲染model的组件将会监听change事件,负责渲染集合的组件将会监听addremove事件。在这两种情况中,都需要调用this.forceUpdate()来使用新数据重新渲染组件。

在下面的例子中,List组件渲染了Backbone的集合,使用Item组件来渲染集合中的每一项。

class Item extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange() {
    this.forceUpdate();
  }

  componentDidMount() {
    this.props.model.on('change', this.handleChange);
  }

  componentWillUnmount() {
    this.props.model.off('change', this.handleChange);
  }

  render() {
    return <li>{this.props.model.get('text')}</li>;
  }
}

class List extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange() {
    this.forceUpdate();
  }

  componentDidMount() {
    this.props.collection.on('add', 'remove', this.handleChange);
  }

  componentWillUnmount() {
    this.props.collection.off('add', 'remove', this.handleChange);
  }

  render() {
    return (
      <ul>
        {this.props.collection.map(model => (
          <Item key={model.cid} model={model} />
        ))}
      </ul>
    );
  }
}

从Backbone的Model中提取数据

上述方法要求你的React组件能够感知到Backbone的model和集合。如果你计划之后使用另外的数据管理方案,你可能会在代码中尽可能少的使用Backbone。

解决这个问题的一个方案是每当model的属性变化时将它提取成简单的数据,并且将它的逻辑保存在同一个地方。下面是一个高阶组件,它把所有的Backbone属性提取出来放入了state中,并将数据传递给被包裹组件。

通过这种方法,只有高阶组件需要知道Backbone的model内部实现,其他的大部分组件不需要知道这些。

在下面的例子中,我们将会从初始的state中复制一份model的属性。我们监听了change事件(在卸载时将监听器移除),当change事件发生时,我们就用当前的model属性更新state。最终,我们确定了当model的prop改变时我们会为老的model解除监听并在新的model上设置监听器。

请注意这个例子不是集成Backbone的详尽的例子,但是它提供了一般集成时的思路:

function connectToBackboneModel(WrappedComponent) {
  return class BackboneComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = Object.assign({}, props.model.attributes);
      this.handleChange = this.handleChange.bind(this);
    }

    componentDidMount() {
      this.props.model.on('change', this.handleChange);
    }

    componentWillReceiveProps(nextProps) {
      this.setState(Object.assign({}, nextProps.model.attributes));
      if (nextProps.model !== this.props.model) {
        this.props.model.off('change', this.handleChange);
        nextProps.model.on('change', this.handleChange);
      }
    }

    componentWillUnmount() {
      this.props.model.off('change', this.handleChange);
    }

    handleChange(model) {
      this.setState(model.changedAttributes());
    }

    render() {
      const propsExceptModel = Object.assign({}, this.props);
      delete propsExceptModel.model;
      return <WrappedComponent {...propsExceptModel} {...this.state} />;
    }
  }
}

为了解释如何使用这个,我们将名为NameInput的React组件与Backbone的model组合,并且在每次输入时更新它的firstName属性:

function NameInput(props) {
  return (
    <p>
      <input value={props.firstName} onChange={props.handleChange} />
      <br />
      My name is {props.firstName}.
    </p>
  );
}

const BackboneNameInput = connectToBackboneModel(NameInput);

function Example(props) {
  function handleChange(e) {
    props.model.set('firstName', e.target.value);
  }

  return (
    <BackboneNameInput
      model={props.model}
      handleChange={handleChange}
    />
  );
}

const model = new Backbone.Model({ firstName: 'Frodo' });
ReactDOM.render(
  <Example model={model} />,
  document.getElementById('root')
);

这项技术不仅限于Backbone。你可以在其他任何model库中使用,只要在生命周期方法中注册监听器就可以,当然,作为可选项,你也可以把数据复制到本地React的state中。