(译)和其他库(jQuery插件,Backbone)一起使用React

注:原文链接

react 可以被用在任何的web 应用中。它可以被嵌入到其他的应用中,当然,如果你足够小心的话,其他的应用也能被嵌入到react中。这篇文章将会从一些常用的使用场景入手,重点会关注与jQuery 和backbone 的交互。但是里面的思想在我们和其他库交互时都是可以被参考的。

和操纵DOM的插件的交互

react 感知不到它管理之外的dom 的变化。react的更新是取决于其内部的表现,如果同样的DOM节点被其他可操作dom节点的插件更改了,react内部状态就会变的很混乱并且无法恢复了。

这并不意味着react 无法和那些操纵DOM 的插件一起共用,你只需要更加清楚每个插件做了什么。

最简单的避免这种冲突发生的方式是阻止react 组件的更新。你可以通过渲染一个react 没有必要去更新的元素,比如一个空的<div/>

如何处理这种问题

为了更好的阐述这个问题,让我们来对一个一般的jquery 插件添加一个wrapper。

首先,我们在这个节点上添加一个ref属性。在componentDidMount 方法里,我们通过获取这个节点的引用,将它传给jquery 插件。

为了避免react 在渲染期间对这个节点进行改变, 我们在render() 方法里面返回了一个空的<div/>.这个空的节点没有任何的属性或子节点,所以React 不会对该节点进行更新,这个节点的控制权完全在jQuery插件上。这样就不会出现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 插件写了一个小型的wrapper。Chosen 插件的参数是一个<select>

注意,虽然可以这样用,但这并不是最好的方式。我们推荐尽可能的使用react组件。这样在react应用中可以更好的复用,而且会有更好的使用效果

首先,让我们来看看Chosen 插件对DOM元素做了什么。
如果你在对一个<select> 节点应用了该组件。它会读取原始DOM节点的属性,使用内联样式隐藏它。并且使用自己的展示方式在<select>节点后面插入新的DOM节点。然后它触发jQuery的事件来通知我们这些改变。

这就是我们想要我们的Chosen 插件包装完成的功能

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

为了简单起见,我们使用一个非受控组件来实现它
首先,我们创建一个只有render方法的组件。在render方法中我们返回一个<div><select></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>标签。这很有必要,因为我们后续会在<select>标签后面添加一个传入的节点。然而,就React而言,<div>标签通常只有一个孩子节点。这就是我们如何确保React 的更新不会和通过Chosen 插入的额外的DOM节点冲突的原因。很重要的一点是,如果你在React 流之外修改了DOM节点,你必须确保React 不会因为任何原因再对这些DOM节点进行操作。

接下来,我们继续实现生命周期的钩子函数。我们需要在componentDidMount里使用<select>节点的引用来初始化Chosen.并且在componentDidUnmount 里面销毁它。

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

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

记住,react 不会对this.el 字段赋予任何特殊的含义。除非你之前在render方法里面对它进行赋值。

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

以上对于在render 里面获取你的组件就足够了,但是我们还希望值变化时能给实现通知。因为,我们通过Chosen 在<select>上 订阅了jQuery 的change事件。

我们不会直接的将this.props.onChange传给Chosen. 因为组件的属性可能会一直改变,而且这里还包含着事件的处理。因为,我们声明了一个handleChange方法来调用this.props.onChange.并且为它订阅了jQuery的change事件中。也就是说,只要一发生change 事件。就会自动执行handleChange 方法。

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 中,由于属性是可以一直改变的。例如,<Chosen>组件能够获取不同的children 如果父组件状态改变的话。这意味着在交互过程中,很重要的一点是,当属性改变时,我们需要手动的控制DOM的更新,不再需要react 来为我们管理DOM节点了。

Chosen 的文档建议我们使用jQuery 的trigger() 方法来通知原始DOM元素的变化。我们将使React重点关注在<select>中的属性this.props.children 的更新。但是我们同时也在componentDidUpdate 的生命周期函数里添加通知Chosen 他的children 列表变化的函数。

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

通过这种方式,当通过React 管理的<select> 节点改变的时候,Chosen 就会知道需要更新DOM元素了。

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('destory');
    }
    
    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>
        );
    }
}

和其他的View 库共用

由于ReactDOM.render()方法的灵活性使得React可以被嵌入到其他的应用中。

由于React 通常被用来将一个React 节点渲染到某个DOM元素中,而且ReactDOM.render()可以被UI的各个独立的部分多次调用,小到一个按钮,大到一个app。

事实上,这就是React 在Facebook 被使用的方式。这使得我们可以在React 中一块一块的开发一个应用,并且可以把它整合在现有的服务器渲染的模版中或者其他的客户端代码中。

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

在一些老的web 应用,一种常见的方式是写一大段DOM结构作为字符串,然后使用$el.html(htmlString) 的方式插入到DOM节点中。如果你的代码库中有类似的场景,那么推荐你使用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组件中去并且采用更多react 实践方式。例如,组件最好不要依赖id,因为同样的组件可能会被渲染多次。而且,我们推荐使用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节点中。慢慢的,你在app 中使用越来越多的react 技术,你就可以将这些独立的组件整合成更大的组件。同时,将一些ReactDOM.render() 的调用移动到不同的层级中。

将React 嵌入到Backbone 的视图中

Backbone 的视图就是典型的使用HTML 字符串,或者使用一些字符串模版函数来生成这样的字符串,然后将之作为DOM元素的内容。这种处理方式,也能被替换为使用React 组件渲染的方式。

下面,我们将会创建一个Backbone 的视图ParagraphView. 我们会通过渲染一个React <Paragraph> 组件,然后使用Backbone 提供的(this.el)方式将它插入到DOM元素中的方式来重写Backbone 的render() 方法. 当然,我们也会使用ReactDOM.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 应用。其实,react 也能使用一些其他框架或者库的Model 层来进行管理。

在react 应用中使用Backbone 的model层

在React 组件中消费Backbone中model和collections 最简单的方法是监听各种change 事件并手动进行强制更新。

渲染models 的组件会监听 'change'事件,渲染collections 的组件会监听‘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 的Models 中提取数据

上述的处理方式要求你的React 组件能够感知到Backbone 的Models 和 Collections .如果你后续要整合其他的数据管理方案,你可能需要更多关注Backbone 的实现细节了。

解决这个问题的一个方法是,当model 的属性改变时,将它提取为普通的数据,并将这段逻辑保存在一个单独的地方。下面演示的是一个高阶组件,这个组件将Backbone 的model层的属性转换为state,然后把数据传递给被包裹的组件。

通过这种方式,只有这个高阶组件需要知道Backbone Models的内部细节信息,大部分的组件对Backbone 都是透明的。

下面的例子中,我们会对Model 的属性进行一份拷贝来作为初始state。我们注册了change 事件(在unmounting 中取消注册),当监听到change事件的时候,我们用model 当前的属性来更新state。最后,我们要确保,如果model 的属性自己改变的话,我们不要忘记从老的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} />;
    }
  }
}

为了阐述如何来使用它,我们会将一个react组件NameInput 和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) {
    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. 你也可以使用React 和其他的model 库进行整合,通过在生命周期中订阅它的变化,并且,选择性的,将数据复制到react 的state中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,833评论 1 18
  • 参考文章:深度剖析:如何实现一个Virtual DOM 算法 作者:戴嘉华React中一个没人能解释清楚的问题——...
    waka阅读 5,965评论 0 21
  • 人总后知后觉 脚印里总有深有浅 岁月里走过了蜿蜒 才明白爱在取舍之间 人要走多远 错与对才学会分辨 就算我忘记全世...
    暧进aini阅读 486评论 0 0
  • 原文戳我这个教程要求Xcode7和Swift2,在这里还是测试版,大家可以去下载最新的.在wwdc2015,发布了...
    iDeveloper阅读 217评论 0 0
  • 题记:偶然的一次,在中央六的佳片有约上看到的印度电影-《月亮河》,不同于其它的印度电影,《月亮河》没有歌舞喧嚣的热...
    兔爰爱阅读 3,308评论 0 5