此文章是翻译Integrating with Other Libraries这篇React(版本v16.2.0)官方文档。
Integrating with Other Libraries
React 可用于任何Web 应用程序。 它可以嵌入到其他应用程序中,并且稍微小心,其他应用程序可以嵌入到React 中。 本指南将研究一些更常见的用例,重点是与jQuery和Backbone的集成,但是可以将相同的想法应用于将组件与任何现有代码集成。
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 的一部分:
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} />;
}
}
请注意,我们定义了componentDidMount
和componentWillUnmount
声明周期钩子。 许多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 组件。
function Example() {
return (
<Chosen onChange={value => console.log(value)}>
<option>vanilla</option>
<option>chocolate</option>
<option>strawberry</option>
</Chose>
);
}
为了简单起见,我们将其作为不可控的组件来实现。
首先,我们将使用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>
)
}
}
注意我们如何在额外的<div>
中包装<select>
。 这是必要的,因为Chosen 将在我们传递给它的<select>
节点之后附加另一个DOM 元素。 然而,就React 而言,<div>
总是只有一个孩子。 这是我们如何确保React 更新不会与Chosen 附加的额外DOM 节点冲突。 重要的是,如果你在React 流之外修改DOM,则必须确保React没有理由触摸这些DOM 节点。
接下来,我们将实现生命周期钩子。 我们需要在componentDidMount
中通过ref 指向的<select>
节点来初始化Chosen,在componentWillUnmount
进行销毁:
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 更改事件。
我们不会将this.props.onChange
直接传递给Chosen,因为组件的props 可能随着时间的推移而改变,这包括事件处理程序。 相反,我们将声明一个调用this.props.onChange
的handleChange()
方法,并将其订阅到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 可以随着时间的推移而改变。 例如,如果父组件的state 改变,<Chosen>
组件可以获得不同的子级。 这意味着在集成点上,重要的是我们手动更新DOM 以响应props 更新,因为我们不再让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>
)
}
}
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实现...
$('#container').html(`<button id="btn">Say Hello</button>`);
$('#btn').click(function() {
alert('Hello!');
});
...可以使用React component 重写:
function Button() {
return <button id="btn">Say Hello</button>;
}
ReactDOM.render(
<Button />,
document.getElementById('container'),
function() {
$('#btn').click(function() {
alert('Hello!');
});
}
);
从这里你可以开始将更多的逻辑转移到组件中,并开始采用更为常见的React实践。 例如,在组件中,最好不要依赖于ID,因为相同的组件可以被多次渲染。 相反,我们将使用React event system,并直接在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')
);
你可以拥有你喜欢的许多这样的隔离组件,并使用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()
:
function Paragraph(props) {
return <p>{props.text}</p>
}
const ParagraphView = Backbone.View.extends({
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 树中删除时,清理将自动执行,但是由于我们正在手动删除整个树,所以我们必须调用之为此方法。
Integrating with Model Layers
虽然通常建议使用单向数据流(unidirectional data flow),如React state,Flux或Redux,但React 组件可以使用其他框架和库中的模型层。
Using Backbone Models in React Components
从React 组件中消费Backbone模型和集合的最简单方法是监听各种更改事件并手动强制更新。
负责渲染模型的组件将监听'change'
事件,而负责呈现集合的组件将监听'add'
和'remove'
事件。 在这两种情况下,请调用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);
tihs.handleChange = this.handleChange();
}
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>
);
}
}
Extracting Data from Backbone Models
上述方法需要你的React 组件了解Backbone模型和集合。如果你以后计划迁移到另一个数据管理解决方案,你可能希望聚焦关于Backbone 的知识尽可能少的部分代码。
一个解决方案是将模型的特性作为纯数据每当它改变时提取,并将该逻辑保留在一个单一的位置。以下是将Backbone 模型的所有特性提取到state 的高阶组件,将数据传递到包装组件。
这样,只有高阶组件需要了解Backbone 模型内部部件,并且应用程序中的大多数组件可以与Backbone保持不变。
在下面的例子中,我们将复制模型的特性以形成初始state。我们订阅更改事件(并在卸载时取消订阅),当它发生时,我们使用模型的当前特性更新state。最后,我们确定,如果model
props本身发生变化,我们不要忘记取消订阅旧模型,并订阅新模型。
请注意,此示例并不意味着与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.changeAttributes());
}
render() {
const propsExceptModel = Object.assign({}, this.props);
delete propsExceptModel.model;
return <WrappedComponent {...propsExceptModel} {...this.state} />;
}
}
}
为了演示如何使用它,我们将NameInput
React 组件连接到Backbone 模型,并在每次输入更改时更新其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) {
model.set('firstName', e.target.value);
}
return (
<BackboneNameInput
model={props.model}
handleChange={handleChange}
/>
);
}
const model = new Backbone.Model({ firstName: 'Frode' });
ReactDOM.render(
<Example model={model} />,
document.getElementById('root')
);
这个技术不局限于Backbone。对于任何处理数据模型的库或框架,你都可以使用任何模型库使用React,通过在生命周期钩子中订阅它的变化事件,并且,可选的,复制这些数据到React 组件的state 中。
这种技术不限于Backbone。 你可以通过在lifecycle hooks中订阅其更改,并可选地将数据复制到本地React state,将React与任何模型库配合使用。