注:React.js 是 Facebook 推出的一個用來構建使用者介面的 JavaScript 庫。
ReactJS從根本上改變了我對待Web介面的態度。在短期接觸ReactJS的時間裡,它帶給了我從未體會過的驚喜。
讓人興奮的根源,我想是React對待UI的方式和我們之前接觸過的不太一樣。你不需要直接操作DOM,Readct的元件會渲染一個視覺化的DOM。這個視覺化的DOM會將它的改變展現出來,並會計算出更新所需要的最少步驟(然後真正地更新了DOM),這一點僅僅是作用在DOM需要改變的部分上(想要了解React reconciliation的機制,請看這裡和這裡)。
到目前為止我使用過的庫或者框架都是直接操作DOM的,如果我不想放棄已經在使用UI部件,又想要使用React,我也能這麼做嗎?
你當然可以咯
當React遇上KendoUI
為了說明這一點,讓我們用KendoUI構建一個計量器(Radial Gauge,它還包含了KendoUI slider)來演示,並將它轉換成React元件。你可以點選上面的連結,看一看使用KendoUI和jQuery是如何寫出這個效果的(這是非React的方式)。(這裡我要提醒大家一下,Radial Gauge是KendoUI專業版的功能,這個版本是需要支付費用的,比起開源版的KendoUI core專業版提供了很多複雜的部件或視覺化效果。你可以看看這篇文章來對比這兩個版本。)
Radial Gauge
首先,建立一個RadiaGauge元件,它可以在DOM中渲染出一個div(這個步驟執行完畢之後,KendoUI部件將會鎖定這個元素):
1 2 3 4 5 6 7 8 |
// react 0.11 supports namespaced components :) var Kendo = {}; Kendo.RadialGauge = React.createClass({ render: function() { return <div className="gauge" />; } }); |
不要擔心,它不會停在那兒的:)。
React元件有一些生命週期方法(lifecycle methods),我們將會用到的是componentDidMount。當一個元件第一次被渲染到頁面上時,這個方法就會被呼叫,這時我們就可以操作DOM了。我們可以使用getDOMNode來獲取剛剛那個div的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Kendo.RadialGauge = React.createClass({ componentDidMount: function() { $(this.getDOMNode()).kendoRadialGauge({ theme: "silver", pointer: { value: 88 }, scale: { minorUnit: 5, startAngle: -30, endAngle: 210, max: 180 } }); }, render: function() { return <div className="gauge" />; } }); |
好吧,上面的程式碼看起來糟糕透了,元件的配置完全是硬編碼。我們其實可以將這個配置的值用props來傳遞,預設值可以用getDefaultProps來使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// just showing the relevant methods Kendo.RadialGauge = React.createClass({ getDefaultProps: function() { return { theme: "silver", minorUnit: 5, startAngle: -30, endAngle: 210, max: 180 } }, componentDidMount: function() { var props = this.props; $(this.getDOMNode()).kendoRadialGauge({ theme: props.theme, pointer: { value: props.value }, scale: { minorUnit: props.minorUnit, startAngle: props.startAngle, endAngle: props.endAngle, max: props.max } }); }, // other methods, etc. }); |
現在看起來好多了,不過還有最後一道坎要跨。
“有一點很重要:因為我們是在元件渲染之後才開始操作DOM的,KendoUI部件是沒法使用ReactJS分析DOM前後不同這個功能。所以我們需要監聽部件的改變好及時作出反應。”
我們需要監聽props.value的值是否被改變,如果改變了就要重新渲染部件。componentWillReceiveProps和componentDidUpdate這兩個方法都能對props.value的改變做出反應。如果我們只是簡單的重新渲染KendoUi radial gauge,那就沒有辦法將那個改變的過程動畫化。所以目前我們是這麼做的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
Kendo.RadialGauge = React.createClass({ getDefaultProps: function() { return { theme: "silver", minorUnit: 5, startAngle: -30, endAngle: 210, max: 180 }; }, componentDidMount: function() { var props = this.props; $(this.getDOMNode()).kendoRadialGauge({ theme: props.theme, pointer: { value: props.value }, scale: { minorUnit: props.minorUnit, startAngle: props.startAngle, endAngle: props.endAngle, max: props.max } }); }, componentWillReceiveProps: function(nextProps) { if(nextProps.value !== this.props.value) { $(this.getDOMNode()).data("kendoRadialGauge").value(nextProps.value); } }, render: function() { return <div className="gauge" />; } }); |
在componentWillReceiveProps中,如果發現部件發生改變,那麼部件中的某個值會變化,KendoUI會動畫化這個改變的過程。
從React角度上來看,RadialGauge元件就是渲染出一個div,所以監聽它的props改變只是微不足道的效能消耗。(對於componentWillReceiveProps來說,DOM的改變不一定會觸發KendoUI部件的更新。)但是如果你的元件渲染方法比較複雜,那最好還是要有shouldComponentUpdate。
Slider
現在讓我們給輪播建立一個元件。第一步:渲染一個div,在componentDidMount呼叫這個元件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Kendo.Slider = React.createClass({ getDefaultProps: function() { return { min: 0, max: 180, showButtons: false }; }, componentDidMount: function() { var props = this.props; $(this.getDOMNode()).kendoSlider({ min: props.min, max: props.max, showButtons: props.showButtons, value: this.props.value }); }, render: function() { return <div />; } }); |
這裡有一點不同。
事件(Events)
我們需要處理由於使用者的輸入導致輪播的值出現變化的情況。因為現在處理的是一個由React合成事件系統(synthetic events system)產生的事件,我們需要提供一個事件處理程式給這個KendoUI部件。還有一點要注意:如果這個元件從DOM中被移除了,那相應的事件處理程式也要移除掉,這個可以讓componentWillUnmount來做。
混合(Mixins)
在Slider元件中,我還用到了React的混合(mixins)。當一個子控制元件要通知其他控制元件它的值發生改變的時候,可選的方法並不多。我使用的是messaging mixin,這個方法可以使得我的元件值發生改變的時候告訴給其他元件(其他元件要監聽這個值):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
Kendo.Slider = React.createClass({ mixins: [React.postal], getDefaultProps: function() { return { min: 0, max: 180, showButtons: false }; }, componentDidMount: function() { var props = this.props; $(this.getDOMNode()).kendoSlider({ min: props.min, max: props.max, showButtons: props.showButtons, value: this.props.value, change: this.handleChange }); }, componentWillUnmount: function() { $(this.getDOMNode()).data("kendoSlider").destroy(); }, handleChange: function(e) { this.publish("change.speed", { speed: e.value }); }, render: function() { return <div />; } }); |
請注意,我們在componentWillUnmount方法中呼叫了destroy(),這一步很重要。
再多說一點
難道就停在gauge和slider這裡了嗎?我想要用“傳統的”React元件來展示這個,所以現在你會在頁面上看到一個下拉選單,選單中的兩個選項是兩種不同的測量方式。
UnitOfMeasure Component
UnitOfMeasure元件使用了messaging mixin來展現所選擇的值的改變。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var UnitOfMeasure = React.createClass({ mixins: [React.postal], handleChange: function(e) { this.publish("change.uom", { uom: $(this.getDOMNode()).val() }); }, render: function() { return <select className="uom-select" onChange={this.handleChange}> <option>MPH</option> <option>KPH</option> </select>; } }); |
SpeedDisplay Component
這個元件沒有什麼好解釋的,它唯一的功能就是將文字展現在div中。
1 2 3 4 5 6 7 |
var SpeedDisplay = React.createClass({ render: function() { return <div className="speed-display"> { this.props.speed + " " + this.props.uom } </div>; } }); |
所有程式碼整合起來
KendoExample元件包含了兩個變數:speed和uom。這個元件會被UnitOfMeasure和Kendo.Slider影響,當上述的兩個元件中的任意一個產生了新的資訊,KendoExample都會呼叫setState。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
var KendoExample = React.createClass({ mixins: [React.postal], getInitialState: function() { return { speed: 88, uom:"MPH" } }, componentWillMount: function() { this.subscribe("change.#", function(data) { this.setState(data); }); }, componentWillUnmount: function() { this.disposeSubscriptions(); }, render: function() { return <div> <div className="container"> <Kendo.RadialGauge value={this.state.speed} /> <Kendo.Slider value={this.state.speed} id="gauge-slider" channel={this.props.channel} /> <UnitOfMeasure channel={this.props.channel} /> </div> <SpeedDisplay speed={this.state.speed} uom={this.state.uom} /> </div>; } }); |
你可以在這裡看到演示結果。
包裝
我希望你覺得這篇文章很有用。如果你想要了解React和別的庫是如何結合使用的,可以看React Bootstrap和Wingspan Forms。
更新
Christopher Chedeau(React的天才開發人員之一)評議了這篇文章並指出關於RadialGauge元件這點,他傾向使用componentWillReceiveProps來比較新舊props,然後看情況更新DOM。原先更新radial gauge是在componentDidUpdate中完成的,現在我把componentWillReceiveProps的方法更新到例子中了。React的開發團隊很友善,所以不要害怕向他們提問。