此文章是翻译Integrating with Other Libraries这篇React(版本v16.2.0)官方文档。

Integrating with Other Libraries

React 可用于任何Web 应用程序。 它可以嵌入到其他应用程序中,并且稍微小心,其他应用程序可以嵌入到React 中。 本指南将研究一些更常见的用例,重点是与jQueryBackbone的集成,但是可以将相同的想法应用于将组件与任何现有代码集成。

Integrating with DOM Manipulation Plugins

React 不知道对React 之外的DOM 所做的更改。 它根据自己的内部表示来确定更新,如果相同的DOM 节点被另一个库操作,则React 会变得困惑,无法恢复。

这并不意味着将React 与影响DOM 的其他方式结合起来是不可能的,甚至一定是难以理解的,你只需要注意每个库在做什么。

避免冲突的最简单方法是防止React 组件更新。 你可以通过渲染React 无法更新的元素来执行此操作,如空的<div />

How to Approach the Problem

为了演示这个,我们来绘制一个通用的jQuery 插件的包装器。

我们将附加一个ref 到根DOM元素。 在componentDidMount 里面,我们将得到一个引用,所以我们可以把它传递给jQuery 插件。

为了防止在加载后React 触摸DOM,我们将从render()方法返回一个空的<div /><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,因此在componentWillUnmount 中分离它们很重要。 如果插件没有提供清理方法,你可能必须提供自己的方法,记住要删除任何事件侦听器,以防止内存泄漏。

Integrating with jQuery Chosen Plugin

有关这些概念的更具体的例子,让我们为插件Chosen编写一个最小的包装器,它增强了<select>的输入。

注意:

只是因为这是可能的,并不意味着它是React 应用程序的最佳方法。 我们鼓励你可以使用React component。 React component在React应用程序中更容易重用,并且通常可以更好地控制其行为和外观。

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

如果你在<select> DOM 节点上调用它,它将从原始DOM 节点读取特性,使用行内样式隐藏它,然后在<select>之后附加具有自己的视觉表示的单独的DOM 节点。 之后它会触发jQuery 事件以通知我们有关更改。

假设这是我们正在寻求的API,使用我们的<Chosen> 包装器React 组件。

  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. </Chose>
  8. );
  9. }

为了简单起见,我们将其作为不可控的组件来实现。

首先,我们将使用render()方法创建一个空的组件,我们返回<select>包装在一个<div>中:

  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. }

注意我们如何在额外的<div> 中包装<select>。 这是必要的,因为Chosen 将在我们传递给它的<select> 节点之后附加另一个DOM 元素。 然而,就React 而言,<div> 总是只有一个孩子。 这是我们如何确保React 更新不会与Chosen 附加的额外DOM 节点冲突。 重要的是,如果你在React 流之外修改DOM,则必须确保React没有理由触摸这些DOM 节点。

接下来,我们将实现生命周期钩子。 我们需要在componentDidMount 中通过ref 指向的<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}>

这足以让我们的组件去渲染 ,但是我们也希望得到关于值变化的通知。 为此,我们将订阅由Chosen管理的<select> 上的jQuery 更改事件。

我们不会将this.props.onChange 直接传递给Chosen,因为组件的props 可能随着时间的推移而改变,这包括事件处理程序。 相反,我们将声明一个调用this.props.onChangehandleChange() 方法,并将其订阅到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> 组件可以获得不同的子级。 这意味着在集成点上,重要的是我们手动更新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 上尝试

Integrating with Other View Libraries

由于ReactDOM.render()的灵活性,React 可以嵌入到其他应用程序中。

虽然React在启动时通常用于将单个根React 组件加载到DOM中,但ReactDOM.render() 也可以多次调用为UI 的不同部分,可以像按钮一样小,或者与应用程序一样大。

事实上,这正是在Facebook 中如何使用React。 这使我们可以一步一步地在React 中编写应用程序,并将其与现有的服务器生成的模板(server-generated)和其他客户端代码(client-side)相结合。

Replacing String-Based Rendering with React

旧版Web 应用程序中的常见模式是将DOM的块(chunks)作为字符串进行描述,并将其插入到DOM 中,如:$el.html(htmlString)。 代码库(codebase)中的这些要点适用于引入的React。 只需将基于字符串的渲染重写为React 组件即可。

所以下面的jQuery实现…

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

…可以使用React component 重写:

  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 event system,并直接在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() 到层次之上调用。

Embeddding React in a Backbone View

Backbone 视图通常使用HTML 字符串或字符串生成模板函数(string-producing template functions)为其DOM元素创建内容。 此过程也可以通过渲染一个React 组件来替代。

下面我们将创建一个名为ParagraphView 的Backbone视图。 它将覆盖Backbone 的render() 函数,去渲染React <Paragraph>组件到由Backbone(this.el)提供的DOM 元素中。 这里也是使用ReactDOM.render()

  1. function Paragraph(props) {
  2. return <p>{props.text}</p>
  3. }
  4. const ParagraphView = Backbone.View.extends({
  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 树中删除时,清理将自动执行,但是由于我们正在手动删除整个树,所以我们必须调用之为此方法。

Integrating with Model Layers

虽然通常建议使用单向数据流(unidirectional data flow),如React stateFluxRedux,但React 组件可以使用其他框架和库中的模型层。

Using Backbone Models in React Components

从React 组件中消费Backbone模型和集合的最简单方法是监听各种更改事件并手动强制更新。

负责渲染模型的组件将监听'change'事件,而负责呈现集合的组件将监听'add''remove'事件。 在这两种情况下,请调用this.forceUpdate() 以使用新数据重新渲染组件。

在下面的示例中,List 组件渲染Backbone集合,使用Item 组件渲染单个条目。

  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. tihs.handleChange = this.handleChange();
  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 上尝试

Extracting Data from Backbone Models

上述方法需要你的React 组件了解Backbone模型和集合。如果你以后计划迁移到另一个数据管理解决方案,你可能希望聚焦关于Backbone 的知识尽可能少的部分代码。

一个解决方案是将模型的特性作为纯数据每当它改变时提取,并将该逻辑保留在一个单一的位置。以下是将Backbone 模型的所有特性提取到state 的高阶组件,将数据传递到包装组件。

这样,只有高阶组件需要了解Backbone 模型内部部件,并且应用程序中的大多数组件可以与Backbone保持不变。

在下面的例子中,我们将复制模型的特性以形成初始state。我们订阅更改事件(并在卸载时取消订阅),当它发生时,我们使用模型的当前特性更新state。最后,我们确定,如果model props本身发生变化,我们不要忘记取消订阅旧模型,并订阅新模型。

请注意,此示例并不意味着与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.changeAttributes());
  23. }
  24. render() {
  25. const propsExceptModel = Object.assign({}, this.props);
  26. delete propsExceptModel.model;
  27. return <WrappedComponent {...propsExceptModel} {...this.state} />;
  28. }
  29. }
  30. }

为了演示如何使用它,我们将NameInput React 组件连接到Backbone 模型,并在每次输入更改时更新其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: 'Frode' });
  23. ReactDOM.render(
  24. <Example model={model} />,
  25. document.getElementById('root')
  26. );

在CodePen 上尝试

这个技术不局限于Backbone。对于任何处理数据模型的库或框架,你都可以使用任何模型库使用React,通过在生命周期钩子中订阅它的变化事件,并且,可选的,复制这些数据到React 组件的state 中。

这种技术不限于Backbone。 你可以通过在lifecycle hooks中订阅其更改,并可选地将数据复制到本地React state,将React与任何模型库配合使用。