React与其他库的集成

React可以在任何web应用中使用。React可以嵌入其他的应用中,也可以将其他的应用嵌入React中,不过需要多加小心。本篇教程将介绍部分常见的使用场景,主要包括集成jQuery和Backbone,但是同样的思想可以用来集成组件到其他任何现有的代码。

与DOM操作插件的集成

React 无法感知到React之外的DOM变化。这决定了更新只能基于React内部的表示,如果相同的DOM节点被其他库所操作,React会对此产生疑惑并无法恢复。

这并不意味着很难或者无法将React于其他影响DOM的方式相结合,你需要更加注意两者各自的行为。

避免冲突最简单的方式就是阻止React的更新。你可以通过渲染React无法更新的元素来实现,例如空的<div />

如何处理这个问题

为了展示这个问题,我们来为通用的jQuery插件绘制一个包装器(wrapper)。

我们给根DOM元素添加ref。在componentDidMount中,我们将获得引用(reference),因此将其传递给jQuery插件。

为了防止React在mount之后处理DOM元素,我们将在render方法中返回空的<div />

元素没有属性或者子元素,因此React不会更新它,使得jQuery插件可以自由地管理这部分的DOM节点。

  1. class SomePlugin extends React.Component {
  2. componentDidMount() {
  3. this.$el = $(this.el);
  4. this.$el.somePlugin();
  5. }
  6. componentWillUnmount() {
  7. this.$el.somePlugin('destroy');
  8. }
  9. render() {
  10. return <div ref={el => this.el = el} />;
  11. }
  12. }

注意,我们定义了componentDidMountcomponentWillUnmount生命周期函数。很多jQuery插件为DOM元素添加了监听器(listener),因此在componentWillUnmount中退订监听器是非常重要的。如果插件本身不提供清除(cleanup)的方法,你可能需要自己提供,牢记一定要移除注册在插件中的事件监听者(event listener)以防止内存泄露。

与jQuery的Chosen插件集成

为了更具体地描述这个概念,让我们写一个最小化的Chosen插件的包装器(wrapper),其中插件Chosen接受<select>元素的输入。

注意:

仅仅不能因为有实现的可能性,就意味着这对React应用来讲是最佳实践。我们鼓励在可能的情况下使用React组件。React组件很容易在React应用中重用(reuse)并且能更好控制其行为与外观。

首先,我们来看看Chosen对DOM的行为。

如果你在<select>DOM节点上调用Chosen,其会读取原始DOM节点的属性,以内联样式方式隐藏,并在内部的虚拟表达中添加单独的DOM节点。随后触发jQuery事件通知事件改变。

让我们了解一下我们所设计的React组件包装器(wrapper)<Chosen>的API:

  1. function Example() {
  2. return (
  3. <Chosen onChange={value => console.log(value)}>
  4. <option>vanilla</option>
  5. <option>chocolate</option>
  6. <option>strawberry</option>
  7. </Chosen>
  8. );
  9. }

为了简单起见,我们将其实现为一个不受控组件

首先我们先创建一个空的组件,其中包含render()方法,其返回一个由<div>包裹的<select>:

  1. class Chosen extends React.Component {
  2. render() {
  3. return (
  4. <div>
  5. <select className="Chosen-select" ref={el => this.el = el}>
  6. {this.props.children}
  7. </select>
  8. </div>
  9. );
  10. }
  11. }

需要注意为什么我们为<select>包裹一个额外的<div>。这是必须的,因为Chosen插件会为我们传入<select>节点后添加另一个DOM节点。。然而,就React而言,<div>总是只有一个子节点。这就是我们如何来确保React更新不会与Chosen插件所添加的额外DOM节点相冲突。值得注意的是,如果在React流之外修改了DOM节点,必须确保React无论如何都不会再接触DOM节点。

接下来,我们实现生命周周期函数。我们在componentDidMount中对引用的<select>节点初始化Chosen插件,并在componentWillUnmount中清除:

  1. componentDidMount() {
  2. this.$el = $(this.el);
  3. this.$el.chosen();
  4. }
  5. componentWillUnmount() {
  6. this.$el.chosen('destroy');
  7. }

在CodePen中尝试

注意,React对this.el变量并没有特殊的含义,生效的原因仅仅是我们先前在render()方法中ref对其进行了赋值:

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

上述对于组件的渲染已经足够,但是我们也想要获得值改变的通知(notifies about the value changes),为了实现这个目的,我们在<select>节点上订阅(subscribe)jQuery Chosen插件的change事件。

我们不直接给Chosen传递this.props.onChange,因为包括事件处理程序在内的组件的属性可能会发生改变。相反,我们声明handleChange方法,它会调用this.props.onChange方法,并订阅jQuery的change事件:

  1. componentDidMount() {
  2. this.$el = $(this.el);
  3. this.$el.chosen();
  4. this.handleChange = this.handleChange.bind(this);
  5. this.$el.on('change', this.handleChange);
  6. }
  7. componentWillUnmount() {
  8. this.$el.off('change', this.handleChange);
  9. this.$el.chosen('destroy');
  10. }
  11. handleChange(e) {
  12. this.props.onChange(e.target.value);
  13. }

在CodePen中尝试

最后,还剩一件事做。在React里props会随时间而改变。例如,如果父组件state改变,<Chosen>组件可能会得到不同的children。这意味着集成的要点是我们必须手动地更新DOM节点来响应props的更新,因为我们已经不能让React再管理DOM节点。

Chosen的文档建议我们使用jQuery的trigger()API来通知原始DOM节点的更新。我们使用React去关注<select>标签内的子节点this.props.children的更新,我们会在生命周期函数componentDidUpdate中向Chosen通知子元素的改变。

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

这样一来,当React导致<select>子元素的改变时,Chosen将会所感知到DOM节点的更新。

Chosen组件的完整实现如下所示:

  1. class Chosen extends React.Component {
  2. componentDidMount() {
  3. this.$el = $(this.el);
  4. this.$el.chosen();
  5. this.handleChange = this.handleChange.bind(this);
  6. this.$el.on('change', this.handleChange);
  7. }
  8. componentDidUpdate(prevProps) {
  9. if (prevProps.children !== this.props.children) {
  10. this.$el.trigger("chosen:updated");
  11. }
  12. }
  13. componentWillUnmount() {
  14. this.$el.off('change', this.handleChange);
  15. this.$el.chosen('destroy');
  16. }
  17. handleChange(e) {
  18. this.props.onChange(e.target.value);
  19. }
  20. render() {
  21. return (
  22. <div>
  23. <select className="Chosen-select" ref={el => this.el = el}>
  24. {this.props.children}
  25. </select>
  26. </div>
  27. );
  28. }
  29. }

在CodePen中尝试

与其他的视图库集成

感谢极具灵活性的方法ReactDOM.render(),使得React可以嵌入其他的应用中。

虽然React通常在启动时将单个根节点的React组件加载进DOM节点,但ReactDOM.render()也可以被多次调用来生成独立的部分UI,小到一个按钮,大到一个应用。

事实上,在Facebook中React就是这么用的。这使得我们可以一步一步地使用React编写程序,并与我们现存的服务器生成的模板与其他客户端代码相结合。

用React替换字符串渲染

在之前的web应用中,一种常见的模式是将DOM块作为字符串描述,并将其插入DOM节点,例如: $el.html(htmlString)。这种代码是非常适合引入React的,仅仅需要将渲染的字符串重写为React组件。

因此下面的jQuery实现:

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

可以用React组件重写成:

  1. function Button() {
  2. return <button id="btn">Say Hello</button>;
  3. }
  4. ReactDOM.render(
  5. <Button />,
  6. document.getElementById('container'),
  7. function() {
  8. $('#btn').click(function() {
  9. alert('Hello!');
  10. });
  11. }
  12. );

从这里开始,你就可以将更多的逻辑移动进组件中,并采用更常见的React实践。例如,在组件,最好不要依赖id值,因为相同的组件可能被多次渲染。相反,我们可以使用React事件系统,直接在React的<button>元素上注册点击事件处理函数。

  1. function Button(props) {
  2. return <button onClick={props.onClick}>Say Hello</button>;
  3. }
  4. function HelloButton() {
  5. function handleClick() {
  6. alert('Hello!');
  7. }
  8. return <Button onClick={handleClick} />;
  9. }
  10. ReactDOM.render(
  11. <HelloButton />,
  12. document.getElementById('container')
  13. );

在CodePen中尝试

你可以按照你的想法创建多个独立的组件,并使用ReactDOM.render()将它们渲染进不同的DOM容器中。在随着你逐渐地将你的应用转成React应用的过程中,你会将组件合并成更大的组件,将将部分的ReactDOM.render()调用改为React的层次结构。

在Backbone视图中集成React

Backbone视图(View)是典型的使用字符串或者字符串产生函数来生成DOM元素的内容。这个过程可以通过渲染React组件来替代。

下面我们将创建一个名为ParagraphView的Backbone视图,它将用来覆盖Backbone的render函数,渲染React的<Paragraph>组件到Backbone提供的DOM节点。这里我们也使用ReactDOM.render():

  1. function Paragraph(props) {
  2. return <p>{props.text}</p>;
  3. }
  4. const ParagraphView = Backbone.View.extend({
  5. render() {
  6. const text = this.model.get('text');
  7. ReactDOM.render(<Paragraph text={text} />, this.el);
  8. return this;
  9. },
  10. remove() {
  11. ReactDOM.unmountComponentAtNode(this.el);
  12. Backbone.View.prototype.remove.call(this);
  13. }
  14. });

在CodePen中尝试

remove方法中我们必须调用ReactDOM.unmountComponentAtNode(),使得React注销与组件树销毁时相关的事件处理函数和其他相关资源。

当组件从组件树中删除时,清除将自动执行,但是因为我们手动地移除整个组件树,我们必须调用这个方法。

React与Model层集成

尽管我们推荐使用单向数据流例如:React stateFlux或者Redux,但React组件仍然可以使用其他框架和库的model层。

在React组件中使用Backbone Models

对React组件而言,使用Backbone models最简单的方式就是监听不同的change事件并手动强制刷新。

负责渲染model的React组件必须监听'change'事件,负责渲染collections的React组件必须监听'add''remove'事件。在这些场景下,调用this.forceUpdate()用新的数据重新渲染组件。

在下面的例子中,List组件渲染Backbone的collection,而Item组件负责渲染单个items。

  1. class Item extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.handleChange = this.handleChange.bind(this);
  5. }
  6. handleChange() {
  7. this.forceUpdate();
  8. }
  9. componentDidMount() {
  10. this.props.model.on('change', this.handleChange);
  11. }
  12. componentWillUnmount() {
  13. this.props.model.off('change', this.handleChange);
  14. }
  15. render() {
  16. return <li>{this.props.model.get('text')}</li>;
  17. }
  18. }
  19. class List extends React.Component {
  20. constructor(props) {
  21. super(props);
  22. this.handleChange = this.handleChange.bind(this);
  23. }
  24. handleChange() {
  25. this.forceUpdate();
  26. }
  27. componentDidMount() {
  28. this.props.collection.on('add', 'remove', this.handleChange);
  29. }
  30. componentWillUnmount() {
  31. this.props.collection.off('add', 'remove', this.handleChange);
  32. }
  33. render() {
  34. return (
  35. <ul>
  36. {this.props.collection.map(model => (
  37. <Item key={model.cid} model={model} />
  38. ))}
  39. </ul>
  40. );
  41. }
  42. }

在CodePen中尝试

从Backbone Models中提取数据

上述方法需要你的React组件了解Backbone的模型和集合。如果你随后计划迁移到另一个数据管理方案,你可能希望将Backbone的概念集中在尽可能少的代码中。

一个解决这个问题的方案是,每当模型的数据改变时将模型的属性提取成一个纯数据,并将逻辑保存在一个单一的位置。接下来的高阶组件提取Backbone中的属性作为state,并将数据传递给被包裹的组件。

这样,仅有高阶组件需要了解Backbone内部的model,应用中大多数的组件可以与Backbone保持独立。

在下面的例子中,我们将复制model的属性来形成最初的状态。我们订阅change事件(以及在卸载时的unsubscribe事件),当change事件发生时,我们使用model的当前属性来更新state。最终,我们确定,如果model属性本身改变时,我们不要忘记退订之前的model,订阅新的model。

请注意,下面的例子并不意味着涵盖与Backbone集成使用的方方面面,但是它提供一种通用的思路来解决上述的问题:

  1. function connectToBackboneModel(WrappedComponent) {
  2. return class BackboneComponent extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = Object.assign({}, props.model.attributes);
  6. this.handleChange = this.handleChange.bind(this);
  7. }
  8. componentDidMount() {
  9. this.props.model.on('change', this.handleChange);
  10. }
  11. componentWillReceiveProps(nextProps) {
  12. this.setState(Object.assign({}, nextProps.model.attributes));
  13. if (nextProps.model !== this.props.model) {
  14. this.props.model.off('change', this.handleChange);
  15. nextProps.model.on('change', this.handleChange);
  16. }
  17. }
  18. componentWillUnmount() {
  19. this.props.model.off('change', this.handleChange);
  20. }
  21. handleChange(model) {
  22. this.setState(model.changedAttributes());
  23. }
  24. render() {
  25. const propsExceptModel = Object.assign({}, this.props);
  26. delete propsExceptModel.model;
  27. return <WrappedComponent {...propsExceptModel} {...this.state} />;
  28. }
  29. }
  30. }

为了演示如何使用这个例子,我们将NameInputReact组件连接到Backbone的model,并在每次输入改变时更新其firstName属性。

  1. function NameInput(props) {
  2. return (
  3. <p>
  4. <input value={props.firstName} onChange={props.handleChange} />
  5. <br />
  6. My name is {props.firstName}.
  7. </p>
  8. );
  9. }
  10. const BackboneNameInput = connectToBackboneModel(NameInput);
  11. function Example(props) {
  12. function handleChange(e) {
  13. model.set('firstName', e.target.value);
  14. }
  15. return (
  16. <BackboneNameInput
  17. model={props.model}
  18. handleChange={handleChange}
  19. />
  20. );
  21. }
  22. const model = new Backbone.Model({ firstName: 'Frodo' });
  23. ReactDOM.render(
  24. <Example model={model} />,
  25. document.getElementById('root')
  26. );

在CodePen中尝试

这个技术并不局限于Backbone。你可以通过在生命周期函数中订阅model改变并且可选地将model中的数据复制进React内部的state的方式,实现React与其他model库集成使用。