上一节:高阶组件
下一节:深入JSX
React高级指引:与第三方库协同
引言
React可以在任何web应用中使用。它可以与其他应用相互嵌套。本章将会介绍一些常用的嵌套的例子,主要侧重于jQuery和Backbone。但是使用这些的思想同样可以应用到其他整合案例中。
集成带有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} />;
}
}
应该注意到了我们在这里声明了componentDidMount
和componentWillUnmount
两个生命周期函数。许多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 state,Flux或Redux这类的单向数据流,React组件也可以使用其他框架和库的model层。
在React组件中使用Backbone的Model
在React组件中使用Backbone的model和集合最简单的方法是监听不同的数据变化并且手动强制更新。
负责渲染model的组件将会监听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);
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中。
上一节:高阶组件
下一节:深入JSX