React-引领未来的用户界面开发框架-读书笔记(六)
第12章 服務端渲染
想讓搜索引擎抓取到你的站點,服務端渲染這一步不可或缺,服務端渲染還可以提升站點的性能,因為在加載JavaScript腳本的同時,瀏覽器就可以進行頁面渲染。
React的虛擬DOM是其可被用于服務端渲染的關鍵。首先每個React Component 在虛擬DOM中完成渲染,然后React通過虛擬DOM來更新瀏覽器DOM中產生變化的那一部分,虛擬DOM作為內存中的DOM表現(xiàn),為React在Node.js這類非瀏覽器環(huán)境下的運行提供了可能,React可以從虛擬DOM中生成一個字符串。而不是更新真正的DOM,這使得我們可以在客戶端和服務端使用同一個React Component。
React 提供了兩個可用于服務端渲染組件的函數(shù):React.renderToString 和React.renderToStaticMarkup。
在設計用于服務端渲染的ReactComponent時需要有預見性,考慮以下方面。
- 選取最優(yōu)的渲染函數(shù)。
- 如何支持組件的異步狀態(tài)。
- 如何將應用的初始化狀態(tài)傳遞到客戶端。
- 哪些生命周期函數(shù)可以用于服務端的渲染。
- 如何為應用提供同構路由支持。
- 單例、實例以及上下文的用法。
渲染函數(shù)
在服務端渲染React Component時,無法使用標準的React.render方法,因為服務端不存在DOM。React提供了兩個渲染的函數(shù),它們支持標準的React Component生命周期的一個子集。因而能夠實現(xiàn)服務端渲染。
React.renderToString
React.renderToString是兩個服務端渲染函數(shù)中的一個,也是開發(fā)主要使用的一個函數(shù),和React.render不同,該函數(shù)去掉了用于表示渲染位置的參數(shù)。取而代之,該函數(shù)只返回一個字符串,這是一個快速的同步(阻塞式)函數(shù),非常快。
var MyComponent = React.createClass({render:fucniton(){return <div> Hello World!</div>;} }); var world= React.renderToString (<MyComponent/>);//這個示例返回一個單行并且格式化的輸出 <divdata-reactid=".fgvrzhg2yo"data-ract-checksum="-1663559667">Hello World! </div>你會注意到,React為這個<div>元素添加了兩個data前綴的屬性。在瀏覽器環(huán)境下,React使用data-reactid區(qū)分DOM節(jié)點。這也是每當組件的state及props發(fā)生變化時,React都可以精準的跟新制定DOM節(jié)點的原因。
data-react-checksum僅僅存在于服務端。顧名思義,它是已創(chuàng)建DOM和校驗和。這準許React在客戶端服用與服務端結構上相同點的DOM結構。該屬性只會添加到根元素上。
React.renderToStaticMarkup
React.renderToStaticMarkup是第二個服務端渲染函數(shù),除了不會包含React的data屬性外,它和React.renderToString沒有區(qū)別。
varMyComponent=React.createClass({render:function(){return<div>Hello World!</div>;} }); varworld= React.renderToStaticMarkup(<MyCompoent/>);//單行輸出 <div>HelloWorld!</div>用React.renderToString還是React.renderToStaticMarkup
每個渲染函數(shù)都有自己的用途,所以你必須明確自己的需求,再去決定使用哪個渲染函數(shù)。當且僅當你不打算在客戶端渲染這個React Component時,才應該選擇使用React.renderToStaticMarkup函數(shù)。
下面有一些示例:
- 生成HTML電子郵件
- 通過HTML到PDF的轉化來生成PDF。
- 組件測試。
大多數(shù)情況下,我們都會選擇使用React.renderToString。這將準許React使用data-react-checksum在客戶端更迅速的初始化同一個React Component。因為React可以重用服務端提供的 DOM,所以它可以跳過生成DOM節(jié)點以及把他們掛載到文檔中這兩個昂貴的進程。對于復雜些的站點,這樣做就會顯著的減少加載時間,用戶可以更快的與站點進行交互。
確保React Component能夠在服務端和客戶端準確的渲染出一致的結構是很重要的。如果data-react-checksum不匹配,React會舍棄服務端提供的DOM,然后生成新的DOM節(jié)點,并且將它們更新到文檔中。此時,React也不再擁有服務端渲染帶來的各種性能上的優(yōu)勢。
服務端組件生命周期
一旦渲染為字符串,組件就會只調用位于render之前的組件生命周期方法,需要指出,componentDidMount和componentWillUnmount不會在服務端渲染過程中被調用,而componentWillMount在兩種渲染方式下均有效。
當新建一個組件時,你需要考慮到它可能即在服務端又在客戶端進行渲染。這一點在創(chuàng)建事件監(jiān)聽器時尤為重要,因為并不存在一個生命周期方法會通知我們React Component是否已經(jīng)走完了整個生命周期。
在componentWillMount內注冊的所有事件監(jiān)聽器及定時器都可能潛在的導致服務端內存泄漏。
最佳做法是只在componentDidMount內部創(chuàng)建事件監(jiān)聽器及定時器,然后在componentWilUnmount內清除這兩者。
設計組件
服務端渲染時,請務必慎重考慮如何將組件的state傳遞到客戶端,以充分利用服務端渲染的優(yōu)勢。在設計服務端渲染組件時,要時刻記得這一點。
在設計React Component時,需要保證同一個props傳遞到組件中,總會輸出相同的初始渲染結果。堅持這樣做將會提升組件的可測試性,并且可以保證組件在服務端和客戶端渲染結果的一致性。充分利用服務端渲染的性能優(yōu)勢十分重要。
我們假設現(xiàn)在需要一個組件,它可以打印一個隨機數(shù)。一個棘手問題是組件每次輸出的結果總是不一致。如果組件在服務端而不是客戶端進行渲染,checksum將會失效。
var MyComponent =React.createClass({render:dunction(){return <div>{Math.random()}</div>;} }); var result = React.renderToStaticmarkup(<MyComponent/>); var result2 = REact.renderToStaticMarkup(<MyComponent/>);//result <div>0.5820949131157249</div>//result2 <div>0.420401582631672</div>如果你打算重構它,組件將會通過props來接收一個隨機數(shù)。然后,將props傳遞到客戶端用于渲染。
var MyComponent= React.createClass({render :function(){retrun<div>{this.props.number}</div>} });var num=Math.random(); //服務端 React.renderToString(<MyComponentnumber={num}/);//將num傳遞到客戶端 React.render(<MyComponentnumber ={num}/>,document.body);有多種方式可以將服務端的props傳遞到客戶端。
最簡單的方式之一是通過JavaScript對象將初始的props值傳遞到客戶端。
<!DOCTYPEhtml> <html><head><title>Example</title><!--bundle 包括MyComponent、React等--><script type =“text/javascript"src="bundle.js"></script></head><body><!--服務端渲染MyComponent的結果--><div data-reactid=".fgvrzhg2yo" data-react-checksum="-1663559667">0.5820949131157249</div><!--注入初始props,供服務端使用--><script type="text/javascript">var initialProps = {"num":0.5820949131157249};</script><!--使用服務端初始props--><script type-"text/javasript">var num = initialProps.num;React.render(<MyComponent number={num}/>, document.body);</script></body></html>異步狀態(tài)
很多應用需要從數(shù)據(jù)庫或者網(wǎng)絡服務這類遠程數(shù)據(jù)源中讀取數(shù)據(jù),在客戶端,這不是問題,在等待異步數(shù)據(jù)返回的時候,React Component可以展示一個加載圖標。在服務端,React 無法直接復制該方案,因為render函數(shù)是同步的。為了使用異步數(shù)據(jù),首先需要抓取數(shù)據(jù),然后再渲染時將數(shù)據(jù)傳遞到組件中。
示例:
1、你可能需要從異步的store中轉區(qū)用戶記錄;2、抓取到用戶記錄后,考慮到SEO以及性能等因素,需要在服務端渲染組件的狀態(tài);3、你需要讓組件監(jiān)聽在客戶端的變化,然后重新渲染
問題:React.renderToString是同步的,所以沒有辦法使用組件的任何一個生命周期方法,來抓區(qū)異步的數(shù)據(jù)
解決方案:使用statics函數(shù)來抓取異步數(shù)據(jù),然后把數(shù)據(jù)傳遞到組件中用于渲染。將initialState作為props值傳遞到客戶端。使用組件生命周期方法來監(jiān)聽變化,然后使用同一個statics函數(shù)更新狀態(tài)。
var Username= React.createClass({statics:{getAsyncState:function()(props,setState){User.findById(props.userId).then(function(user){setState({user:user});}).catch(function(error){setState({error:error});});}},//客戶端和服務器componentWillMount:function(){if(this.props.initialState){this.setState(this.props.initialState);}},//僅客戶端componentDidMount:function(){//如果props中沒有,則獲取一部stateif(!this.props.initialState){this.updateAsynState();}//監(jiān)聽change事件User.on('change',this.updateAsyncState);},//僅客戶端componentWillUnmount:funtion(){//停止監(jiān)聽change事件User.off(‘change’,this.undateAsyncState);},updateAsyncState:function(){//訪問示例中的靜態(tài)函數(shù)this.constructor.getAsyncState(this.props,this.setstate);},render:funciton(){if(this.state.error){return <div>{this.state.error.message}</div>;}if(!this.state.usr){retrun<div>Loading...</div>}return <div>{this.state.user.username}</div>;} });//在服務器端渲染 var props={userId:123//也可以通過路由傳遞 }; Username.getAsyncState(props,funciton(initialState){props[initialState]=initialState;var result =React.renderToString(Username(props));//使用initialState將結果傳遞到客戶端 });上述解決方案中,預先抓取到異步數(shù)據(jù)這一步僅在服務端是必須的。在客戶端,只有初次渲染時需要查找服務端所傳遞的initialState。后續(xù)客戶端上的路由變化(比如HTML5,pushState或者fragment change)都會忽略掉服務端所有的initialState。同時,在抓取數(shù)據(jù)時最好加載文案信息。
同構路由
對于任意一個完整的應用來說,路由都至關重要。為了在服務端渲染出擁有路由的React應用,你必須確保路由系統(tǒng)支持無DOM渲染。
抓取異步數(shù)據(jù)是路由系統(tǒng)及其控制器的職責。我們假設一個深度嵌套的組件需要一些異步的數(shù)據(jù)。如果SEO需要這些數(shù)據(jù),那么抓取數(shù)據(jù)的職責應該被提升至路由控制器,并且這些數(shù)據(jù)應該被傳遞到嵌套組件的最內層。如果不用考慮SEO,那么在客戶端的componentDidMount方法內抓取數(shù)據(jù)是沒問題的。這與傳統(tǒng)的Ajax加載數(shù)據(jù)方式類似。
考慮一個React同構路由解決方案時,需確保它具有異步狀態(tài)支持,或者可以輕易地更改以支持異步狀態(tài)。理想情況下,你也會傾向于使用路由系統(tǒng)來控制,將initialState傳遞到客戶端。
單例、實例及上下文
在瀏覽器端,你的應用如同包裹在獨立的氣泡中一樣。每個實例之間的狀態(tài)不會混在一起,因為每個實例通常存在于不同的計算機或者同一臺計算機的不同沙箱之中。這使得我們可以在應用架構中輕松地使用單例模式。
當你開始遷移代碼并在服務端運行時,你必須要小心,因為可能存在同一應用的多個實例在相同的作用域內同時運行的情況。有可能出現(xiàn)應用的兩個實例都去更改單例狀態(tài)的情況,這會導致異常的行為發(fā)生。
React渲染是同步的,所以你可以重置之前使用過的所有單例,而后在服務端渲染你的應用。如果異步狀態(tài)需要使用單例,則又會遇到問題。同樣,在渲染過程中使用抓取到的異步狀態(tài)時,也需要考慮到這一點。
盡管可以在渲染前重置之前使用過的單例,但是在隔離的環(huán)境下運行你的應用總是有好處的。Contextify之類的包準許你在服務端彼此隔離地運行代碼。這與客戶端使用webworkers類似。Contextify通過將應用代碼運行在一個隔離的Node.js V8實例中來工作。一旦加載完代碼,你就可以調用環(huán)境中的所有函數(shù)。這種方法可以讓你隨意地使用單例模式,而不用考慮性能上的花銷,因為每次請求都對應一個全新的Node.js V8實例。
React的核心開發(fā)小組不鼓勵在組件樹中傳遞上下文和實例。這種做法會降低組件的可移植性,并且應用內組件依賴的更改會對層級上的所有組件產生連動式的影響。這轉而增加了應用的復雜性,而隨著應用的增長,應用的可維護性也會降低。
當決定使用單例或者實例來控制你的上下文時,需要對兩者權衡舍去。在選擇一個方法之前,你需要估算出詳細的需求,還需要考慮你所使用的第三方類庫是如何架構的。
總結
服務端渲染是構建搜索引擎優(yōu)化的Web站點和Web應用時的重要部分。React支持在服務端和客戶端瀏覽器中渲染相同的React component。要有效地做到這一點,你需要保證整個應用都使用這一架構方式以支持服務端渲染。
第13章 周邊類庫
圍繞著Ract,facebook還開發(fā)了一系列的前端工具。在你的React項目中,這些工具不是非用不可的,不過它們確實可以和React一起完美的工作。例如:
- Jest
- Immutable.js
- Flux
Jest
Jest是Factbook開發(fā)的一個測試運行工具。它基于Jasmine測試框架提供相近的方式,使用大家熟悉的類似于expect(value).to(other)的斷言。它提供了默認的模擬行為,會自動模擬require()返回的CommonJS模塊。讓現(xiàn)有的代碼變成可測試的。它使用了模擬的DOM API ,同時通過小巧的Node.js命令行工具進行運行,縮短每次測試運行的時間。
page 108~112
Immutable.js
不可以變數(shù)據(jù)結構(Immutable Data Structures)中的數(shù)據(jù)是不允許修改的。相反,如果數(shù)據(jù)需要改變,它們會返回原始數(shù)據(jù)的一個經(jīng)過修改的拷貝。React跟Flux可以很好的結合不可變數(shù)據(jù)結構,帶來代碼的簡潔和性能的提升。
Immutable.js提供了多個數(shù)據(jù)結構,可以有原生的JavaScript數(shù)據(jù)結構構造而成,在需要的時候,也可以轉會原生的JavaSctipt數(shù)據(jù)結構。
immutable.Map
Immutable.Map可作為常規(guī)JavaScript對象的替代者來使用:
var question = Immutable.Map({desctiption:'who is your favorite superhero?'});//使用.get從Map中取值 question.get('desctiption');//通過.set更新值時返回一個新的對象 //原始對象保持不變 question2 = question.set('desctiption', 'Who is your favorite comicbook hero?');//使用.merge合并兩個對象得到第三個對象 //同樣原來的對象沒有任何變化 var title = {title : 'Question #1'}; var question3 = question.merge(question2,title); question3.toObject();//{title: 'Question #1',desctiption":'who is your favorite comicbook hero'}Immutable.Vector
可以使用Immutable.Vector代替數(shù)組:
var options = Immutable.Vector('Superman', 'Batman'); var option2 = options.push('Spiderman'); option2.toArray(); //['Superman','Batman','Spiderman']你還可以對這些數(shù)據(jù)結構進行嵌套:
var options = Immutable.Vector('Superman', 'Batman'); var question = Immutable.Map({description : 'who is your favorite superhero?',options : options });Immutable.js還有更豐富的特性,你可以到immutable-js上獲取更多的相關信息。
Flux
Flux是Facebook在發(fā)布React時發(fā)布的一種模式。它顯著的特性是嚴格的單向數(shù)據(jù)流。
Facebook在GitHub發(fā)布了一份關于實習那Flux的參考,可以通過flux訪問到。
Flux包含了三個重要的組件
- Dispatcher
- Store
- View
下圖清晰地展示了如何將這些部件組合到一起:
Flux沒有強制的依賴,你可以任意選取自己需要的模塊。
關于Flux更詳細的討論見第16章。
總結
以上是生活随笔為你收集整理的React-引领未来的用户界面开发框架-读书笔记(六)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: React-引领未来的用户界面开发框架-
- 下一篇: React-引领未来的用户界面开发框架-