博客 / 詳情

返回

和其他庫(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中。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.