這裏有一份簡潔的前端知識體系等待你查收,看看吧,會有驚喜哦~若是以爲不錯,麻煩star哈~css
react 是數據驅動的框架,也是目前前端最火的框架之一,學習react,咱們照舊從應用維度跟設計維度進行學習。html
從技術的應用維度看,首先考慮的是要解決什麼問題,這是技術產生的緣由。問題這層,用來回答「幹什麼用」。前端
react 的誕生實際上是要解決兩個問題。UI細節問題問題 和 數據模型的問題。node
UI細節問題問題react
傳統UI操做關注太多細節,jQuery雖然能夠給咱們提供了便捷的API,以及良好的瀏覽器兼容,但開發人員仍是要手動去操做DOM,關注太多細節,不只下降了開發效率,還容易引入BUG。webpack
react以數據爲中心,數據驅動視圖,而不直接操做dom,也就是隻負責描述界面應該顯示成什麼樣子,而不關心實現細節。git
數據模型的問題github
在react以前,前端管理數據的模型是MVC架構。傳統的MVC架構難以擴展和維護,當應用程序出現問題,很難知道是model仍是view出現問題。web
react採用的是單向數據流,能夠很好的避免相似的問題。數據庫
技術被研發出來,人們怎麼用它才能解決問題呢?這就要看技術規範,能夠理解爲技術使用說明書。技術規範,回答「怎麼用」的問題,反映你對該技術使用方法的理解深度。
要真正理解 React,開發者必需要明白這幾點:
下面逐一分析。
筆者認爲:學習框架,關鍵是要關注框架自己解決了什麼問題,以及如何解決,這些纔是框架最核心的部分,若是鬍子眉毛一把抓,很容易就迷失在知識的海洋中。
細心的你確定會發現,react的三個基本原則,正是解決上文提到的兩大傳統的良方。
針對UI細節問題,react的解決方案就是:數據驅動與組件化;
針對數據模型,react的解決方案是單向數據流,而props爲單向數據流提供了支持。
學習react的過程當中,牢記兩大傳統問題以及react開出的解決方案,有利於你造成系統思惟,強化你的react的理解。
React 的哲學,簡單說來能夠用下面這條公式來表示:
UI = f(data)
複製代碼
等號左邊的 UI 表明最終畫出來的界面;等號右邊的 f 是一個函數,也就是咱們寫的 React 相關代碼;data 就是數據,在 React 中,data 能夠是 state 或者 props。
UI 就是把 data 做爲參數傳遞給 f 運算出來的結果。這個公式的含義就是,若是要渲染界面,不要直接去操縱 DOM 元素,而是修改數據,由數據去驅動 React 來修改界面。
咱們開發者要作的,就是設計出合理的數據模型,讓咱們的代碼徹底根據數據來描述界面應該畫成什麼樣子,而沒必要糾結如何去操做瀏覽器中的 DOM 樹結構。
這樣一種程序結構,是聲明式編程(Declarative Programming)的方式,代碼結構會更加容易理解和維護。
在 React 中一切皆爲組件。這是由於:
第一點用戶界面就是組件,很好理解,咱們須要一個按鈕,就能夠實現一個Button組件,在 React 中,一個組件能夠是一個類,也能夠是一個函數,這取決於這個組件是否有本身的狀態。
第二點,組件能夠嵌套包裝組成複雜功能。現實中的應用是很複雜的,React 中的組件能夠重複嵌套,就是爲了支持現實中的用戶界面須要。
第三點,組件能夠用來實現反作用。並非說組件必需要在界面畫一些東西,一個組件能夠什麼都不畫,或者把畫界面的事情交給其餘組件去作,本身作一些和界面無關的事情,好比獲取數據。
父組件想要傳遞數據給子組件,能夠經過 props。
一樣,子組件想要傳遞數據給父組件,可讓父組件傳遞一個函數類型的 props 進來,當子組件要傳遞數據給父組件時,調用這個函數類型 props,就把信息傳遞給了父組件。
若是兩個徹底沒有關係的組件之間有話說,狀況就複雜了一點,好比下圖中,兩個橙色組件之間若是有話說,就無法直接經過 props 來傳遞信息。
一個比較土的方法,就是經過 props 之間的逐步傳遞,來把這兩個組件關聯起來。若是之間跨越兩三層的關係,這種方法還湊合,可是,若是這兩個組件隔了十幾層,或者說所處位置多變,那讓 props 跨越千山萬水來相會,實在是得不償失。
另外一個簡單的方式,就是創建一個全局的對象,兩個組件把想要說的話都掛在這個全局對象上。這種方法固然簡單可行,可是,咱們都知道全局變量的危害罄竹難書,若是不想未來被難以維護的代碼折磨,咱們最好對這種方法敬而遠之。
通常,業界對於這種場景,每每會採用第三方數據管理工具來解決,好比 Redux 和 Mobx 。
其實,不依賴於第三方工具,React 也提供了本身的跨組件通信方式,這種方式叫 Context,後面會介紹。
小結:
咱們平常建立組件的通常步驟,是這樣的:
若是要設計出更優雅的組件,咱們還要了解組件設計原則。
React 組件設計原則,簡單說來,就是高內聚、低耦合。
就是要減小組件之間的耦合性,讓系統易於理解、易於維護。
因此,建立組件的原則,概括起來有兩點:
更具體一點,在設計 React 組件時,要注意如下事項:
傳統的網頁應用分爲三層,分別是用 HTML 實現的「內容」,用 CSS 實現的「樣式」,還有用 JS 實現的「動態行爲」。
HTML、CSS 和 JS 被分開管理,致使的問題是要修改一個功能,須要至少修改三個文件,這就違背了高內聚的原則。
在 React 中,當你要修改一個功能的內容和行爲時,在一個文件中就能完成,這樣就知足了高內聚的要求。
在react中處理CSS,官方沒有一個統一的標準,請看後面樣式處理的章節。
組件設計的內容遠不止這些。社區有很是多的最佳實踐,請看組件設計模式
在前面的章節中,咱們反覆聲明過 React 其實就是這樣一個公式:
UI = f(data)
複製代碼
f 的參數 data,除了 props,就是 state。props 是組件外傳遞進來的數據,state 表明的就是 React 組件的內部狀態。
雖然有 Redux 和 Mobx 這樣的狀態管理工具,不過,咱們首先不要管這些第三方工具,先從瞭解 React 組件自身的管理開始。
爲何呢?
第一個緣由,由於 React 組件自身的狀態管理是基礎,其餘第三方工具都是在這個基礎上構築的,連基礎都不瞭解,沒法真正理解第三方工具。
另外一個重要緣由,對於不少應用場景,React 組件自身的狀態管理就足夠解決問題,犯不上動用 Redux 和 MobX 這樣的大殺器,簡單問題簡單處理,可讓代碼更容易維護。
判斷一個數據應該放在哪裏,用下面的原則:
不能直接修改 state 對象,必須使用 this.setState。
由於使用 setState 函數,那不光修改 state,還能引起組件的從新渲染。
React 爲了性能考慮,不會每次 setState 都引起從新渲染。
this.setState({count: 1});
this.setState({caption: 'foo'});
this.setState({count: 2});
複製代碼
連續的同步調用 setState,第三次還覆蓋了第一次調用的效果,可是效果只至關於調用了下面這樣一次:
this.setState({count: 2, caption: 'foo'});
複製代碼
每一個 setState 都引起一次從新渲染,實在太浪費了。
React 很是巧妙地用任務隊列解決了這個問題,能夠理解爲每次 setState 函數調用都會往 React 的任務隊列裏放一個任務,屢次 setState 調用天然會往隊列裏放多個任務。React 會選擇時機去批量處理隊列裏執行任務,當批量處理開始時,React 會合並多個 setState 的操做,好比上面的三個 setState 就被合併爲只更新 state 一次,也只引起一次從新渲染。
由於這個任務隊列的存在,React 並不會同步更新 state,因此,在 React 中,setState 也不保證同步更新 state 中的數據。
簡單說來,調用 setState 以後的下一行代碼,讀取 this.state 並非修改以後的結果。
console.log(this.state.count);// 修改以前this.state.count爲0
this.setState({count: 1})
console.log(this.state.count);// 在這裏this.state.count依然爲0
複製代碼
這是由於React 的任務隊列機制。setState 只是給任務隊列裏增長了一個修改 this.state 的任務,這個任務並無當即執行,因此 this.state 並不會馬上改變。
但也有例外。由 React 的生命週期函數或者事件處理函數以外引發的 setState ,就能夠同步更新 state。
看下面的代碼,結果可能會出乎你的所料:
setTimeout(() => {
this.setState({count: 2}); //這會馬上引起從新渲染
console.log(this.state.count); //這裏讀取的count就是2
}, 0);
複製代碼
爲何 setTimeout 可以強迫 setState 同步更新 state 呢?
能夠這麼理解,當 React 調用某個組件的生命週期函數或者事件處理函數時,React 會想:「嗯,這一次函數可能調用屢次 setState,我會先打開一個標記,只要這個標記是打開的,全部的 setState 調用都是往任務隊列裏聽任務,當這一次函數調用結束的時候,我再去批量處理任務隊列,而後把這個標記關閉。」
由於 setTimeout 是一個 JS 函數,和 React 無關,對於 setTimeout 的第一個函數參數,這個函數參數的執行時機,已經不是 React 可以控制的了,換句話說,React 不知道何時這個函數參數會被執行,因此那個「標記」也沒有打開。
當那個「標記」沒有打開時,setState 就不會給任務列表裏增長任務,而是強行馬上更新 state 和引起從新渲染。這種狀況下,React 認爲:「這個 setState 發生在本身控制能力以外,也許開發者就是想要強行同步更新呢,寧濫勿缺,那就同步更新了吧。」
雖然有辦法同步更新state,但要謹慎使用。
React 選擇不一樣步更新 state,是一種性能優化。
並且,每當你以爲須要同步更新 state 的時候,每每說明你的代碼設計存在問題,絕大部分狀況下,你所須要的,並非「state 馬上更新」,而是,「肯定 state 更新以後我要作什麼」,這就引出了 setState 另外一個功能。
setState 的第二個參數能夠是一個回調函數,當 state 真的被修改時,這個回調函數會被調用。
console.log(this.state.count); // 0
this.setState({count: 1}, () => {
console.log(this.state.count); // 這裏就是1了
})
console.log(this.state.count); // 依然爲0
複製代碼
當 setState 的第二個參數被調用時,React 已經處理完了任務列表,因此 this.state 就是更新後的數據。
若是須要在 state 更新以後作點什麼,請利用第二個參數。
setState 的第一個參數除了能夠是對象,其實也能夠傳入一個函數。
當 setState 的第一個參數爲函數時,任務列表上增長的就是一個可執行的任務函數了,React 每處理完一個任務,都會更新 this.state,而後把新的 state 傳遞給這個任務函數。
setState 第一個參數的形式以下:
function increment(state, props) {
return {count: state.count + 1};
}
複製代碼
能夠看到,這是一個純函數,不光接受當前的 state,還接受組件的 props,在這個函數中能夠根據 state 和 props 任意計算,返回的結果會用於修改 this.state。
如此一來,咱們就能夠這樣連續調用 setState:
this.setState(increment);
this.setState(increment);
this.setState(increment);
複製代碼
用這種函數式方式連續調用 setState,就真的可以讓 this.state.count 增長 3,而不僅是增長 1。
最佳實踐回答「怎麼能用好」的問題,反映你實踐經驗的豐富程度。
聰明組件和傻瓜組件:讓咱們更好的組織代碼
在 React 應用中,最簡單也是最經常使用的一種組件模式,就是「聰明組件和傻瓜組件」。
軟件設計中有一個原則,叫作「責任分離」,簡單說就是讓一個模塊的責任儘可能少,若是發現一個模塊功能過多,就應該拆分爲多個模塊,讓一個模塊都專一於一個功能,這樣更利於代碼的維護。
使用 React 來作界面,無外乎就是得到驅動界面的數據,而後利用這些數據來渲染界面。
把獲取和管理數據的邏輯放在父組件,也就是聰明組件;把渲染界面的邏輯放在子組件,也就是傻瓜組件。
這麼作的好處,是能夠靈活地修改數據狀態管理方式,好比,最初你可能用 Redux 來管理數據,而後你想要修改成用 Mobx,若是按照這種模式分割組件,那麼,你須要改的只有聰明組件,傻瓜組件能夠保持原狀。
由於傻瓜組件通常沒有本身的狀態,咱們能夠利用 PureComponent 來提升傻瓜組件的性能。
PureComponent 幫咱們處理了shouldComponentUpdate。
值得一提的是,PureComponent 中 shouldComponentUpdate 對 props 作得只是淺層比較,不是深層比較,若是 props 是一個深層對象,就容易產生問題。
好比,兩次渲染傳入的某個 props 都是同一個對象,可是對象中某個屬性的值不一樣,這在 PureComponent 眼裏,props 沒有變化,不會從新渲染,可是這明顯不是咱們想要的結果。
雖然 PureComponent 能夠提升組件渲染性能,可是它也不是沒有代價的,它逼迫咱們必須把組件實現爲 class,不能用純函數來實現組件。
若是你使用 React v16.6.0 以後的版本,能夠使用一個新功能 React.memo 來完美實現 React 組件,好比:
const Joke = React.memo(() => (
<div> <img src={SmileFace} /> {this.props.value || 'loading...' } </div> )); 複製代碼
高階組件:讓咱們更好的抽象公共邏輯
在開發 React 組件過程當中,很容易發現這樣一種現象,某些功能是多個組件通用的,若是每一個組件都重複實現這樣的邏輯,確定十分浪費,並且違反了「不要重複本身」(DRY,Don't Repeat Yourself)的編碼原則,咱們確定想要把這部分共用邏輯提取出來重用。
咱們說過,在 React 的世界裏,組件是第一公民,首先想到的是固然是把共用邏輯提取爲一個 React 組件。不過,有些狀況下,這些共用邏輯還無法成爲一個獨立組件,換句話說,這些共用邏輯單獨沒法使用,它們只是對其餘組件的功能增強。
舉個例子,對於不少網站應用,有些模塊都須要在用戶已經登陸的狀況下才顯示。好比,對於一個電商類網站,「退出登陸」按鈕、「購物車」這些模塊,就只有用戶登陸以後才顯示,對應這些模塊的 React 組件若是連「只有在登陸時才顯示」的功能都重複實現,那就浪費了。
這時候,咱們就能夠利用「高階組件(HoC)」這種模式來解決問題。
高階組件,本質是一個函數,它接受至少一個 React 組件爲參數,而且可以返回一個全新的 React 組件做爲結果,固然,這個新產生的 React 組件是對做爲參數的組件的包裝,因此,有機會賦予新組件一些加強的「神力」。
一個最簡單的高階組件是這樣的形式:
const withDoNothing = (Component) => {
const NewComponent = (props) => {
return <Component {...props} />; }; return NewComponent; }; 複製代碼
有了高階組件,咱們就能夠用它來抽取共同邏輯。
高階組件只須要返回一個 React 組件便可,沒人規定高階組件只能接受一個 React 組件做爲參數,徹底能夠傳入多個 React 組件給高階組件。
咱們能夠用高階組件封裝登陸與登出的邏輯,以下:
const withLoginAndLogout = (ComponentForLogin, ComponentForLogout) => {
const NewComponent = (props) => {
if (getUserId()) {
return <ComponentForLogin {...props} />;
} else {
return <ComponentForLogout{...props} />;
}
}
return NewComponent;
};
複製代碼
高階組件最巧妙的一點,是能夠鏈式調用。
假設,你有三個高階組件分別是 withOne、withTwo 和 withThree,那麼,若是要賦予一個組件 X 三個高階組件的超能力,能夠連續調用高階組件,以下:
const SuperX = withThree(withTwo(withOne(X)));
複製代碼
高階組件自己就是一個純函數,純函數是能夠組合使用的,因此,咱們其實能夠把多個高階組件組合爲一個高階組件,而後用這一個高階組件去包裝X,代碼以下:
const hoc = compose(withThree, withTwo, withOne);
const SuperX = hoc(X);
複製代碼
在上面代碼中使用的 compose,是函數式編程中很基礎的一種方法,做用就是把多個函數組合爲一個函數。
React 組件能夠當作積木同樣組合使用,如今有了 compose,咱們就能夠把高階組件也當作積木同樣組合,進一步重用代碼。
假如一個應用中多個組件都須要一樣的多個高階組件包裝,那就能夠用 compose 組合這些高階組件爲一個高階組件,這樣在使用多個高階組件的地方實際上就只須要使用一個高階組件了。
高階組件雖然能夠用一種可重用的方式擴充現有 React 組件的功能,但高階組件並非絕對完美的。
首先,高階組件不得不處理 displayName,否則 debug 會很痛苦。當 React 渲染出錯的時候,靠組件的 displayName 靜態屬性來判斷出錯的組件類,而高階組件老是創造一個新的 React 組件類,因此,每一個高階組件都須要處理一下 displayName。
若是要作一個最簡單的什麼加強功能都沒有的高階組件,也必需要寫下面這樣的代碼:
const withExample = (Component) => {
const NewComponent = (props) => {
return <Component {...props} />; } NewComponent.displayName = `withExample(${Component.displayName || Component.name || 'Component'})`; return NewCompoennt; }; 複製代碼
對於 React 生命週期函數,高階組件不用怎麼特殊處理,可是,若是內層組件包含定製的靜態函數,這些靜態函數的調用在 React 生命週期以外,那麼高階組件就必需要在新產生的組件中增長這些靜態函數的支持,這更加麻煩。關於這點,請參考這裏
其次,高階組件支持嵌套調用,這是它的優點。可是若是真的一大長串高階組件被應用的話,當組件出錯,你看到的會是一個超深的 stack trace,十分痛苦。
關於高階組件的進階內容,請參考高階組件實現方法
render props 模式:讓咱們更好的抽象公共邏輯,同時又避免了高階組件的一些問題
高階組件並非 React 中惟一的重用組件邏輯的方式,且高階組件存在一些弊端,因此又誕生了render props 模式,也稱爲「以函數爲子組件」的模式。
所謂 render props,指的是讓 React 組件的 props 支持函數這種模式。由於做爲 props 傳入的函數每每被用來渲染一部分界面,因此這種模式被稱爲 render props。
一個最簡單的 render props 組件 RenderAll,代碼以下:
const RenderAll = (props) => {
return(
<React.Fragment> {props.children(props)} </React.Fragment> ); }; 複製代碼
這個 RenderAll 預期子組件是一個函數,它所作的事情就是把子組件當作函數調用,調用參數就是傳入的 props,而後把返回結果渲染出來,除此以外什麼事情都沒有作。
使用 RenderAll 的代碼以下:
<RenderAll>
{() => <h1>hello world</h1>}
</RenderAll>
複製代碼
能夠看到,RenderAll 的子組件,也就是夾在 RenderAll 標籤之間的部分,實際上是一個函數。這個函數渲染出 <h1>hello world</h1>
,這就是上面使用 RenderAll 渲染出來的結果。
固然,這個 RenderAll 沒作任何實際工做,接下來咱們看 render props 真正強悍的使用方法。
下面是實現 render props 的 Login 組件,能夠看到,render props 和高階組件的第一個區別,就是 render props 是真正的 React 組件,而不是一個返回 React 組件的函數。
const Login = (props) => {
const userName = getUserName();
if (userName) {
const allProps = {userName, ...props};
return (
<React.Fragment> {props.children(allProps)} </React.Fragment> ); } else { return null; } }; 複製代碼
當用戶處於登陸狀態,getUserName 返回當前用戶名,不然返回空,而後咱們根據這個結果決定是否渲染 props.children 返回的結果。
固然,render props 徹底能夠決定哪些 props 能夠傳遞給 props.children,在 Login 中,咱們把 userName 做爲增長的 props 傳遞給下去,這樣就是 Login 的加強功能。
一個使用上面 Login 的 JSX 代碼示例以下:
<Login>
{({userName}) => <h1>Hello {userName}</h1>}
</Login>
複製代碼
render props 這個模式沒必要侷限於 children 這一個 props,任何一個 props 均可以做爲函數,也能夠利用多個 props 來做爲函數。
咱們來擴展 Login,不光在用戶登陸時顯示一些東西,也能夠定製用戶沒有登陸時顯示的東西,咱們把這個組件叫作 Auth,對應代碼以下:
const Auth= (props) => {
const userName = getUserName();
if (userName) {
const allProps = {userName, ...props};
return (
<React.Fragment>
{props.login(allProps)}
</React.Fragment>
);
} else {
<React.Fragment>
{props.nologin(props)}
</React.Fragment>
}
};
複製代碼
用法以下:
<Auth
login={({userName}) => <h1>Hello {userName}</h1>}
nologin={() => <h1>Please login</h1>}
/>
複製代碼
render props 其實就是 React 世界中的「依賴注入」。
所謂依賴注入,指的是解決這樣一個問題:邏輯 A 依賴於邏輯 B,若是讓 A 直接依賴於 B,固然可行,可是 A 就無法作得通用了。依賴注入就是把 B 的邏輯以函數形式傳遞給 A,A 和 B 之間只須要對這個函數接口達成一致就行,如此一來,再來一個邏輯 C,也能夠用同樣的方法重用邏輯 A。
在上面的代碼示例中,Login 和 Auth 組件就是上面所說的邏輯 A,而傳遞給組件的函數類型 props,就是邏輯 B 和 C。
首先,render props 模式的應用,就是作一個 React 組件,而高階組件,雖然名爲「組件」,其實只是一個產生 React 組件的函數。
render props 不像高階組件有那麼多毛病,若是說 render props 有什麼缺點,那就是 render props 不能像高階組件那樣鏈式調用,固然,這並非一個致命缺點。
render props 相對於高階組件還有一個顯著優點,就是 props 傳遞更加靈活。
總結:當須要重用 React 組件的邏輯時,建議首先看這個功能是否能夠抽象爲一個簡單的組件;若是行不通的話,考慮是否能夠應用 render props 模式;再不行的話,才考慮應用高階組件模式。
mixin:同樣能夠抽象公共邏輯,但如今已經不提倡使用。這裏只作簡單介紹。
React 在使用 createClass 構建組件時提供了 mixin 屬性。mixin有兩個做用:
ES6 Classes 不支持 mixin。
mixin 的問題:
高階組件與 mixin 的不一樣之處
高階組件符合函數式編程思想。對於原組件來講,並不會感知到高階組件的存在,只須要把功能套在它之上就能夠了,從而避免了使用 mixin 時產生的反作用。
提供者模式:讓咱們更好的跨層級傳遞數據
在 React 中,props 是組件之間通信的主要手段,可是,有一種場景單純靠 props 來通信是不恰當的,那就是兩個組件之間間隔着多層其餘組件,下面是一個簡單的組件樹示例圖圖:
在上圖中,組件 A 須要傳遞信息給組件 X,若是經過 props 的話,那麼從頂部的組件 A 開始,要把 props 傳遞給組件 B,而後組件 B 傳遞給組件 D,最後組件 D 再傳遞給組件 X。
其實組件 B 和組件 D 徹底用不上這些 props,可是又被迫傳遞這些 props,這明顯不合理,要知道組件樹的結構會變化的,未來若是組件 B 和組件 D 之間再插入一層新的組件,這個組件也須要傳遞這個 props,這就麻煩無比。
可見,對於跨級的信息傳遞,咱們須要一個更好的方法。
在 React 中,解決這個問題應用的就是「提供者模式」。
提供者模式有兩個角色,一個叫「提供者」(Provider),另外一個叫「消費者」(Consumer)。在上面的組件樹中,組件 A 能夠做爲提供者,組件 X 就是消費者。
既然名爲「提供者」,它能夠提供一些信息,並且這些信息在它之下的全部組件,不管隔了多少層,均可以直接訪問到,而不須要經過 props 層層傳遞。
避免 props 逐級傳遞,便是提供者的用途。
實現提供者模式,須要 React 的 Context 功能,能夠說,提供者模式只不過是讓 Context 功能更好用一些而已。
所謂 Context 功能,就是可以創造一個「上下文」,在這個上下文籠罩之下的全部組件均可以訪問一樣的數據。
提供者模式的一個典型用例就是實現「樣式主題」(Theme),由頂層的提供者肯定一個主題,下面的樣式就能夠直接使用對應主題裏的樣式。這樣,當須要切換樣式時,只須要修改提供者就行,其餘組件不用修改。
在 React v16.3.0 以前,要實現提供者,就要實現一個 React 組件,不過這個組件要作兩個特殊處理。
下面就是一個實現「提供者」的例子,組件名爲 ThemeProvider:
class ThemeProvider extends React.Component {
getChildContext() {
return {
theme: this.props.value
};
}
render() {
return (
<React.Fragment> {this.props.children} </React.Fragment> ); } } ThemeProvider.childContextTypes = { theme: PropTypes.object }; 複製代碼
對於 ThemeProvider,咱們創造了一個上下文,這個上下文就是一個對象,結構是這樣:
{
theme: {
//一個對象
}
}
複製代碼
接下來,就是使用這個「上下文」的組件。這裏有兩種方式:
使用類的方式:
class Subject extends React.Component {
render() {
const {mainColor} = this.context.theme;
return (
<h1 style={{color: mainColor}}> {this.props.children} </h1>
);
}
}
Subject.contextTypes = {
theme: PropTypes.object
}
複製代碼
使用純函數組件:
const Paragraph = (props, context) => {
const {textColor} = context.theme;
return (
<p style={{color: textColor}}> {props.children} </p>
);
};
Paragraph.contextTypes = {
theme: PropTypes.object
};
複製代碼
這兩種方式訪問「上下文」的方式有些不一樣,都必須增長 contextTypes 屬性,必須和 ThemeProvider 的 childContextTypes 屬性一致,否則,this.context 就不會獲得任何值。
最後,咱們看如何結合」提供者「和」消費者「。
咱們作一個組件來使用 Subject 和 Paragraph,這個組件不須要幫助傳遞任何 props,代碼以下:
const Page = () => (
<div> <Subject>這是標題</Subject> <Paragraph> 這是正文 </Paragraph> </div>
);
複製代碼
上面的組件 Page 使用了 Subject 和 Paragraph,如今咱們想要定製樣式主題,只須要在 Page 或者任何須要應用這個主題的組件外面包上 ThemeProvider,對應的 JSX 代碼以下:
<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
<Page /> </ThemeProvider>
複製代碼
當咱們須要改變一個樣式主題的時候,改變傳給 ThemeProvider的 value 值就搞定了。
首先,要用新提供的 createContext
函數創造一個「上下文」對象。
const ThemeContext = React.createContext();
複製代碼
這個「上下文」對象 ThemeContext
有兩個屬性,分別就是——對,你沒猜錯——Provider 和 Consumer。
const ThemeProvider = ThemeContext.Provider;
const ThemeConsumer = ThemeContext.Consumer;
複製代碼
使用「消費者」以下:
const Paragraph = (props, context) => {
return (
<ThemeConsumer> { (theme) => ( <p style={{color: theme.textColor}}> {props.children} </p> ) } </ThemeConsumer>
);
};
複製代碼
實現 Page 的方式並無變化:
<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
<Page /> </ThemeProvider>
複製代碼
在老版 Context API 中,「上下文」只是一個概念,並不對應一個代碼,兩個組件之間達成一個協議,就誕生了「上下文」。
在新版 Context API 中,須要一個「上下文」對象(上面的例子中就是 ThemeContext),使用「提供者」的代碼和「消費者」的代碼每每分佈在不一樣的代碼文件中,那麼,這個 ThemeContext 對象放在哪一個代碼文件中呢?
最好是放在一個獨立的文件中,這麼一來,就多出一個代碼文件,並且全部和這個「上下文」相關的代碼,都要依賴於這個「上下文」代碼文件,雖然這沒什麼大不了的,可是的確多了一層依賴關係。
爲了不依賴關係複雜,每一個應用都不要濫用「上下文」,應該限制「上下文」的使用個數。
組合組件:簡化父組件向子組件傳遞props的方式
組合組件模式要解決的是這樣一類問題:父組件想要傳遞一些信息給子組件,可是,若是用 props 傳遞又顯得十分麻煩。
利用 Context 能夠解決問題,但很是繁瑣,組合組件讓咱們能夠用更簡潔的方式去實現。
不少界面都有 Tab 這樣的元件,咱們須要一個 Tabs 組件和 TabItem 組件,Tabs 是容器,TabItem 是一個一個單獨的 Tab,由於一個時刻只有一個 TabItem 被選中,很天然但願被選中的 TabItem 樣式會和其餘 TabItem 不一樣。
這並非一個很難的功能,首先咱們想到的就是,用 Tabs 中一個 state 記錄當前被選中的 Tabitem 序號,而後根據這個 state 傳遞 props 給 TabItem,固然,還要傳遞一個 onClick 事件進去,捕獲點擊選擇事件。
按照這樣的設計,Tabs 中若是要顯示 One、Two、Three 三個 TabItem,JSX 代碼大體這麼寫:
<TabItem active={true} onClick={this.onClick}>One</TabItem>
<TabItem active={false} onClick={this.onClick}>Two</TabItem>
<TabItem active={false} onClick={this.onClick}>Three</TabItem>
複製代碼
這樣寫能夠實現功能,但未免過於繁瑣,且不利於維護。咱們但願能夠簡單點,最好代碼就這樣:
<Tabs>
<TabItem>One</TabItem>
<TabItem>Two</TabItem>
<TabItem>Three</TabItem>
</Tabs>
複製代碼
相似這種場景,父子組件不經過 props 傳遞,兩者之間有某種神祕的「組合」,就是咱們所說的「組合組件」。
咱們先寫出 TabItem 的代碼,以下:
const TabItem = (props) => {
const {active, onClick} = props;
const tabStyle = {
'max-width': '150px',
color: active ? 'red' : 'green',
border: active ? '1px red solid' : '0px',
};
return (
<h1 style={tabStyle} onClick={onClick}> {props.children} </h1>
);
};
複製代碼
有了 TabItem ,咱們再看下 TabItem 的調用方式。
<Tabs>
<TabItem>One</TabItem>
<TabItem>Two</TabItem>
<TabItem>Three</TabItem>
</Tabs>
複製代碼
沒有 props 傳遞,怎麼悄無聲息地把 active 和 onClick 傳遞給 TabItem ?
咱們能夠把 props.children 拷貝一份,這樣就有機會去篡改這份拷貝,最後渲染這份拷貝就行了。
咱們來看 Tabs 的實現代碼:
class Tabs extends React.Component {
state = {
activeIndex: 0
}
render() {
const newChildren = React.Children.map(this.props.children, (child, index) => {
if (child.type) {
return React.cloneElement(child, {
active: this.state.activeIndex === index,
onClick: () => this.setState({activeIndex: index})
});
} else {
return child;
}
});
return (
<Fragment> {newChildren} </Fragment>
);
}
}
複製代碼
在 render 函數中,咱們用了 React 中不經常使用的兩個 API:
使用 React.Children.map,能夠遍歷 children 中全部的元素,由於 children 多是一個數組嘛。
使用 React.cloneElement 能夠複製某個元素。這個函數第一個參數就是被複制的元素,第二個參數能夠增長新產生元素的 props,咱們就是利用這個機會,把 active 和 onClick 添加了進去。
這兩個 API 雙劍合璧,就能實現不經過表面的 props 傳遞,完成兩個組件的「組合」。
應用組合組件的每每是共享組件庫,把一些經常使用的功能封裝在組件裏,讓應用層直接用就行。在 antd 和 bootstrap 這樣的共享庫中,都使用了組合組件這種模式。
所謂模式,就是特定於一種問題場景的解決辦法。
模式(Pattern) = 問題場景(Context) + 解決辦法(Solution)
若是不搞清楚場景,單純知道有這麼一個辦法,就比如拿到了一杆槍殊不知道這杆槍用於打什麼目標,是沒有任何意義的。並非全部的槍都是同樣的,有的槍擅長狙擊,有的槍適合近戰,有的槍只是發個信號。
模式就是咱們的武器,咱們必定要搞清楚一件武器應用的場合,才能真正發揮這件武器的威力。
實現高階組件的方法有:
定義:高階組件經過被包裹的 React 組件來操做 props。
import React from 'react';
const MyContainer = WrapComponent =>
class extends React.Component {
render () {
return <WrapComponent {...this.props} />; } }; 複製代碼
高階組件的做用有:控制 props、經過 refs 使用引用、抽象 state 和使用其餘元素包裹。
咱們能夠讀取、增長、編輯或是移除從 WrappedComponent 傳進來的 props,但須要當心刪除與編輯重要的 props。咱們應該儘量對高階組件的 props 做新的命名以防止混淆。
在高階組件中,咱們能夠接受 refs 使用WrappedComponent 的引用。
import React, {Component} from 'React';
const MyContainer = WrappedComponent =>
class extends Component {
proc (wrappedComponentInstance) {
wrappedComponentInstance.method ();
}
render () {
const props = Object.assign ({}, this.props, {
ref: this.proc.bind (this),
});
return <WrappedComponent {...props} />; } }; 複製代碼
當 WrappedComponent 被渲染時,refs 回調函數就會被執行,這樣就會拿到一份Wrapped-Component 實例的引用。這就能夠方便地用於讀取或增長實例的 props,並調用實例的方法。
咱們能夠經過 WrappedComponent 提供的 props 和回調函數抽象 state,高階組件能夠將原組件抽象爲展現型組件,分離內部狀態。
import React, {Component} from 'React';
const MyContainer = WrappedComponent =>
class extends Component {
constructor (props) {
super (props);
this.state = {
name: '',
};
this.onNameChange = this.onNameChange.bind (this);
}
onNameChange (event) {
this.setState ({
name: event.target.value,
});
}
render () {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange,
},
};
return <WrappedComponent {...this.props} {...newProps} />; } }; 複製代碼
咱們把 input 組件中對 name prop 的 onChange 方法提取到高階組件中,這樣就有效地抽象了一樣的 state 操做。能夠這麼來使用它
import React, {Component} from 'React';
@MyContainer
class MyComponent extends Component {
render () {
return <input name="name" {...this.props.name} />; } } 複製代碼
經過這樣的封裝,咱們就獲得了一個被控制的 input 組件。
咱們還能夠使用其餘元素來包裹 WrappedComponent,這既能夠是爲了加樣式,也可 以是爲了佈局。
import React, {Component} from 'React';
const MyContainer = WrappedComponent =>
class extends Component {
render () {
return (
<div style={{display: 'block'}}> {' '}<WrappedComponent {...this.props} /> </div> ); } }; 複製代碼
定義:高階組件繼承於被包裹的 React 組件(從字面意思上看,它必定與繼承特性相關)
簡單例子:高階組件返回的組件繼承於 WrappedComponent。由於被動地繼承了 WrappedComponent,全部的調用都會反向,這也是這種方法的由來。
const MyContainer = WrappedComponent =>
class extends WrappedComponent {
render () {
return super.render ();
}
};
複製代碼
在反向繼承方法中,高階組件能夠使用 WrappedComponent 引用,這意味着它能夠使用WrappedComponent 的 state、props 、生命週期和 render 方法。但它不能保證完整的子組件樹被解析。
反向繼承兩大特色:
渲染劫持指的就是高階組件能夠控制 WrappedComponent 的渲染過程,並渲染各類各樣的結 果。咱們能夠在這個過程當中在任何 React 元素輸出的結果中讀取、增長、修改、刪除 props,或 讀取或修改 React 元素樹,或條件顯示元素樹,又或是用樣式控制包裹元素樹。
正如以前說到的,反向繼承不能保證完整的子組件樹被解析,這意味着將限制渲染劫持功能。 渲染劫持的經驗法則是咱們能夠操控 WrappedComponent 的元素樹,並輸出正確的結果。但若是 元素樹中包括了函數類型的 React 組件,就不能操做組件的子組件。
const MyContainer = WrappedComponent =>
class extends WrappedComponent {
render () {
if (this.props.loggedIn) {
return super.render ();
} else {
return null;
}
}
};
複製代碼
高階組件能夠讀取、修改或刪除WrappedComponent 實例中的 state,若是須要的話,也能夠 增長 state。但這樣作,可能會讓WrappedComponent 組件內部狀態變得一團糟。大部分的高階組 件都應該限制讀取或增長 state,尤爲是後者,能夠經過從新命名 state,以防止混淆。
const MyContainer = WrappedComponent =>
class extends WrappedComponent {
render () {
return (
<div> <h2>HOC Debugger Component</h2> <p>Props</p> {' '} <pre>{JSON.stringify (this.props, null, 2)}</pre> {' '} <p>State</p> <pre>{JSON.stringify (this.state, null, 2)}</pre> {' '} {super.render ()} </div>
);
}
};
複製代碼
上文提到了狀態管理·組件狀態,接下來咱們來看看社區主流的狀態管理工具。
要理解 Redux,首先要明白咱們爲何須要 Redux,或者說,Redux 適用於什麼樣的場景。
在真實應用中,React 組件樹會很龐大很複雜,兩個沒有父子關係的 React 組件之間要共享信息,怎麼辦呢?
最直觀的作法,就是將狀態保存在一個全局對象中,這個對象,叫 store。
若是 store 是一個普通對象,誰均可以修改,那狀態就亂套了。因此咱們要作一些限制,讓 store 只接受特定事件,若是要修改 store 的數據,就往 store 發送這些事件, store 對事件進行響應,從而修改狀態。
這裏說的事件,就是 action ,而對應修改狀態的函數,就是reducer。
Redux 的主要貢獻,就是限制了對狀態的修改方式,讓全部改變均可以被追蹤。
對於某個狀態,究竟是放在 Redux 的 Store 中呢,仍是放在 React 組件自身的狀態中呢?
針對這個問題,有如下原則:
第一步,看這個狀態是否會被多個 React 組件共享。
第二步,看這個組件被 unmount 以後從新被 mount,以前的狀態是否須要保留。
第三步,到這一步,基本上能夠肯定,這個狀態能夠放在 React 組件中了。
1、Store 上的數據應該範式化。
所謂範式化,就是儘可能減小冗餘信息,像設計 MySQL 這樣的關係型數據庫同樣設計數據結構。
2、使用 selector。
對於 React 組件,須要的是『反範式化』的數據,當從 Store 上讀取數據獲得的是範式化的數據時,須要經過計算來獲得反範式化的數據。你可能會所以擔憂出現問題,這種擔憂不是沒有道理,畢竟,若是每次渲染都要重複計算,這種浪費聚沙成塔可能真會產生性能影響,因此,咱們須要使用 seletor。業界應用最廣的 selector 就是 reslector 。
reselector 的好處,是把反範式化分爲兩個步驟,第一個步驟是簡單映射,第二個步驟是真正的重量級運算,若是第一個步驟發現產生的結果和上一次調用同樣,那麼第二個步驟也不用計算了,能夠直接複用緩存的上次計算結果。
絕大部分實際場景中,老是隻有少部分數據會頻繁發生變化,因此 reselector 能夠避免大量重複計算。
3、只 connect 關鍵點的 React 組件
當 Store 上狀態發生改變的時候,全部 connect 上這個 Store 的 React 組件會被通知:『狀態改變了!』
而後,這些組件會進行計算。connect 的實現方式包含 shouldComponentUpdate 的實現,能夠阻擋住大部分沒必要要的從新渲染,可是,畢竟處理通知也須要消耗 CPU,因此,儘可能讓關鍵的 React 組件 connect 到 store 就行。
一個實際的例子就是,一個列表種可能包含幾百個項,讓每個項都去 connect 到 Store 上不是一個明智的設計,最好是隻讓列表去 connect,而後把數據經過 props 傳遞給各個項。
使用 Redux 對於同步狀態更新很是順手,可是,遇到須要異步更新狀態的場景,例如調用 AJAX 從服務器得到數據,這時候單用 Redux 就不夠了,須要其餘方式來輔助。
至今爲止,還沒法推薦一個殺手級的方法,各類方法都在吹噓本身多厲害,可是任何一種方法都是易用性和複雜性的平衡。
最簡單的 redux-thunk,代碼量少,只有幾行,用起來也很直觀,可是開發者要寫不少代碼;而比較複雜的 redux-observable 至關強大,能夠只用少許代碼就實現複雜功能,可是前提是你要學會 RxJS,RxJS 自己學習曲線很陡,內容須要 一本書 的篇幅來介紹,這就是代價。
讀者在本身的項目中,不管選擇什麼方式,必定要考慮這個方式的複雜度和學習成本。
在這裏我不想過多介紹任何一種 Redux 擴展,由於任何一種都比不上 React 將要支持的 Suspense,Suspense 纔是 React 中作異步操做的將來,在第 19 小節會詳細介紹 Suspense。
咱們用 Mobx 來實現一個很簡單的計數工具,首先,須要有一個對象來記錄計數值,代碼以下:
import {observable} from 'mobx';
const counter = observable ({
count: 0,
});
複製代碼
在上面的代碼中,counter 是一個對象,其實就是用 observable 函數包住一個普通 JS 對象,可是 observable 的介入,讓 counter 對象擁有了神力。
咱們用最簡單的代碼來展現這種「神力」,代碼以下:
import {autorun} from 'mobx';
window.counter = counter;
autorun (() => {
console.log ('#count', counter.count);
});
複製代碼
把 counter 賦值給 window.counter,是爲了讓咱們在 Chrome 的開發者界面能夠訪問。用 autorun 包住了一個函數,這個函數輸出 counter.count 的值,這段代碼的做用,咱們很快就能看到。
在 Chrome 的開發者界面,咱們能夠直接訪問 window.counter.count,神奇之處是,若是咱們直接修改 window.counter.count 的值,能夠直接觸發 autorun 的函數參數!
這個現象說明,mobx 的 observable 擁有某種「神力」,任何對這個對象的修改,都會馬上引起某些函數被調用。和 observable 這個名字同樣,被包裝的對象變成了「被觀察者」,而被調用的函數就是「觀察者」,在上面的例子中,autorun 的函數參數就是「觀察者」。
Mobx 這樣的功能,等於實現了設計模式中的「觀察者模式」(Observer Pattern),經過創建 observer 和 observable 之間的關聯,達到數據聯動。不過,傳統的「觀察者模式」要求咱們寫代碼創建二者的關聯,也就是寫相似下面的代碼:
observable.register(observer);
複製代碼
Mobx 最了不得之處,在於不須要開發者寫上面的關聯代碼,Mobx本身經過解析代碼就可以自動發現 observer 和 observable 之間的關係。
咱們很天然想到,若是讓咱們的數據擁有這樣的「神力」,那咱們就不用在修改完數據以後,再費心去調用某些函數使用這些數據了,數據管理會變得十分輕鬆。
Mobx 和 React 並沒有直接關係,爲了創建兩者的關係,須要安裝 mobx-react
仍是以 Counter 爲例,看如何用 decorator 使用 Mobx,咱們先看代碼:
import {observable} from 'mobx';
import {observer} from 'mobx-react';
@observer
class Counter extends React.Component {
@observable count = 0;
onIncrement = () => {
this.count++;
};
onDecrement = () => {
this.count--;
};
componentWillUpdate () {
console.log ('#enter componentWillUpdate');
}
render () {
return (
<CounterView caption="With decorator" count={this.count} onIncrement={this.onIncrement} onDecrement={this.onDecrement} /> ); } } 複製代碼
在上面的代碼中,Counter 這個 React 組件自身是一個 observer,而 observable 是 Counter 的一個成員變量 count。
注意 observer 這 個decorator 來自於 mobx-react,它是 Mobx 世界和 React 的橋樑,被它「裝飾」的組件,就是一個「觀察者」。
本例中,成員變量 count 是被觀察者,只要被觀察者一變化,做爲觀察者的 Counter 組件就會從新渲染。
真實的業務場景是一個狀態須要多個組件共享,因此 observable 通常是在 React 組件以外。
咱們重寫一遍 Counter 組件,代碼以下:
const store = observable ({
count: 0,
});
store.increment = function () {
this.count++;
};
store.decrement = function () {
this.count--;
}; // this decorator is must
@observer
class Counter extends React.Component {
onIncrement = () => {
store.increment ();
};
onDecrement = () => {
store.decrement ();
};
render () {
return (
<CounterView caption="With external state" count={store.count} onIncrement={this.onIncrement} onDecrement={this.onDecrement} /> ); } } 複製代碼
Mobx 和 Redux 的目標都是管理好應用狀態,可是最根本的區別在於對數據的處理方式不一樣。
Redux 認爲,數據的一致性很重要,爲了保持數據的一致性,要求Store 中的數據儘可能範式化,也就是減小一切沒必要要的冗餘,爲了限制對數據的修改,要求 Store 中數據是不可改的(Immutable),只能經過 action 觸發 reducer 來更新 Store。
Mobx 也認爲數據的一致性很重要,可是它認爲解決問題的根本方法不是讓數據範式化,而是不要給機會讓數據變得不一致。因此,Mobx 鼓勵數據乾脆就「反範式化」,有冗餘沒問題,只要全部數據之間保持聯動,改了一處,對應依賴這處的數據自動更新,那就不會發生數據不一致的問題。
值得一提的是,雖然 Mobx 最初的一個賣點就是直接修改數據,可是實踐中你們仍是發現這樣無組織無紀律很差,因此後來 Mobx 仍是提供了 action 的概念。和 Redux 的 action 有點不一樣,Mobx 中的 action 其實就是一個函數,不須要作 dispatch,調用就修改對應數據
若是想強制要求使用 action,禁止直接修改 observable 數據,使用 Mobx 的 configure,以下:
import {configure} from 'mobx';
configure({enforceActions: true});
複製代碼
總結一下 Redux 和 Mobx 的區別,包括這些方面:
咱們能夠使用 classnames 庫來操做類。
// 若是不使用 classnames 庫,就須要這樣處理動態類名:
import React, {Component} from 'react';
class Button extends Component {
render () {
let btnClass = 'btn';
if (this.state.isPressed) {
btnClass += ' btn-pressed';
} else if (this.state.isHovered) {
btnClass += ' btn-over';
}
return <button className={btnClass}>{this.props.label}</button>;
}
}
// 使用了 classnames 庫代碼後,就能夠變得很簡單:
import React, { Component } from 'react';
import classNames from 'classnames';
class Button1 extends Component {
// ...
render () {
const btnClass = classNames ({
btn: true,
'btn-pressed': this.state.isPressed,
'btn-over': !this.state.isPressed && this.state.isHovered,
});
return <button className={btnClass}>{this.props.label}</button>;
}
}
複製代碼
CSS 模塊化要解決兩個問題:CSS 樣式的導入與導出。靈活按需導入以便複用代碼,導出時要可以隱藏內部做用域,以避免形成全局污染。
CSS 模塊化解決方案有兩種:Inline Style、CSS Modules。
Inline Style
這種方案完全拋棄 CSS,使用 JS 或 JSON 來寫樣式,能給 CSS 提供 JS 一樣強大的模塊化能力。但缺點一樣明顯,Inline Style 幾乎不能利用 CSS 自己 的特性,好比級聯、媒體查詢(media query)等,:hover 和 :active 等僞類處理起來比較 複雜。另外,這種方案須要依賴框架實現,其中與 React 相關的有 Radium、jsxstyle 和 react-style。
CSS Modules
依舊使用 CSS,但使用 JS 來管理樣式依賴。CSS Modules 能最大化地結合現有 CSS 生態和 JS 模塊化能力,其 API 很是簡潔,學習成本幾乎爲零。 發佈時依舊編譯出單獨的 JS 和 CSS 文件。webpack css-loader 內置 CSS Modules 功能。
使用了 CSS Modules 後,就至關於給每一個 class 名外加了 :local,以此來實現樣式的局部化。若是咱們想切換到全局模式,能夠使用 :global 包裹
/* components/Button.css */
.base { /* 全部通用的樣式 */ }
.normal {
composes: base;
/* normal其餘樣式 */
}
.disabled {
composes:base;
/* disabled 其餘樣式 */
}
複製代碼
生成的 HTML 變爲:
<button class="button--base-abc53 button--normal-abc53"> Processing... </button>
複製代碼
因爲在 .normal 中組合了 .base,因此編譯後的 normal 會變成兩個 class。 此外,使用 composes 還能夠組合外部文件中的樣式:
/* settings.css */
.primary-color {
color: #f40;
}
/* components/Button.css */
.base { /* 全部通用的樣式 */ }
.primary {
composes: base;
composes: $primary-color from './settings.css'; /* primary 其餘樣式 */
}
複製代碼
:export 關鍵字能夠把 CSS 中的變量輸出到 JS 中
/* config.scss */
$primary-color: '#f40';
:export {
primaryColor: $primary-color;
}
複製代碼
/* app.js */
import style from 'config.scss';
// 會輸出 #F40 console.log(style.primaryColor);
複製代碼
隨着 AJAX 技術的成熟,如今單頁應用(Single Page Application)已是前端網頁界的標配,名爲「單頁」,其實在設計概念上依然是多頁的界面,只不過從技術層面上頁之間的切換是沒有總體網頁刷新的,只須要作局部更新。
要實現「單頁應用」,一個最要緊的問題就是作好「路由」(Routing),也就是處理好下面兩件事:
react-router 的 v3 和 v4 版完徹底全是不一樣的兩個工具,最大的區別是 v3 爲靜態路由, v4 作到了動態路由。
所謂「靜態路由」,就是說路由規則是固定的。
所謂動態路由,指的是路由規則不是預先肯定的,而是在渲染過程當中肯定的。
react-router 的工做方式,是在組件樹頂層放一個 Router 組件,而後在組件樹中散落着不少 Route 組件(注意比 Router 少一個「r」),頂層的 Router 組件負責分析監聽 URL 的變化,在它保護傘之下的 Route 組件能夠直接讀取這些信息。
很明顯,Router 和 Route 的配合,就是以前咱們介紹過的「提供者模式」,Router 是「提供者」,Route是「消費者」。
更進一步,Router 其實也是一層抽象,讓下面的 Route 無需各類不一樣 URL 設計的細節,不要覺得 URL 就一種設計方法,至少能夠分爲兩種。
第一種很天然,好比 / 對應 Home 頁,/about 對應 About 頁,可是這樣的設計須要服務器端渲染,由於用戶可能直接訪問任何一個 URL,服務器端必須能對 /的訪問返回 HTML,也要對 /about 的訪問返回 HTML。
第二種看起來不天然,可是實現更簡單。只有一個路徑 /,經過 URL 後面的 # 部分來決定路由,/#/ 對應 Home 頁,/#/about 對應 About 頁。由於 URL 中#以後的部分是不會發送給服務器的,因此,不管哪一個 URL,最後都是訪問服務器的 / 路徑,服務器也只須要返回一樣一份 HTML 就能夠,而後由瀏覽器端解析 # 後的部分,完成瀏覽器端渲染。
在 react-router,有 BrowserRouter 支持第一種 URL,有 HashRouter 支持第二種 URL。
假設,咱們增長一個新的頁面叫 Product,對應路徑爲 /product,可是隻有用戶登陸了以後才顯示。若是用靜態路由,咱們在渲染以前就肯定這條路由規則,這樣即便用戶沒有登陸,也能夠訪問 product,咱們還不得不在 Product 組件中作用戶是否登陸的檢查。
若是用動態路由,則只須要在代碼中的一處涉及這個邏輯:
<Switch>
<Route exact path='/' component={Home}/>
{
isUserLogin() &&
<Route exact path='/product' component={Product}/>,
}
<Route path='/about' component={About}/>
</Switch>
複製代碼
<Link>
:普通連接,不會觸發瀏覽器刷新<NavLink>
:相似<Link>
可是會添加當前選中狀態<Prompt>
:知足條件時提示用戶是否離開當前頁面<Redirect>
:重定向當前頁面,例如登陸判斷<Route>
:路由配置的核心標記,路徑匹配時顯示對應組件<Switch>
:只現實第一個匹配路由最近幾年瀏覽器端框架很繁榮,以致於不少新入行的開發者只知道瀏覽器端渲染框架,都不知道存在服務器端渲染這回事,其實,網站應用最初全都是服務器端渲染,由服務器端用 PHP、Java 或者 Python 等其餘語言產生 HTML 來給瀏覽器端解析。
相比於瀏覽器端渲染,服務器端渲染的好處是:
React v16 以前的版本,代碼是這樣:
import React from 'react';
import ReactDOMServer from 'react-dom/server';
// 把產生html返回給瀏覽器端
const html = ReactDOMServer.renderToString(<Hello />); 複製代碼
從 React v16 開始,上面的服務器端代碼依然能夠使用,可是也能夠把 renderToString 替換爲 renderToNodeStream,代碼以下:
import React from 'react';
import ReactDOMServer from 'react-dom/server';
// 把渲染內容以流的形式塞給response
ReactDOMServer.renderToNodeStream(<Hello />).pipe(response); 複製代碼
此外,瀏覽器端代碼也有一點變化,ReactDOM.render 依然能夠使用,可是官方建議替換爲 ReactDOM.hydrate,原來的 ReactDOM.render 未來會被廢棄掉。
renderToString 的功能是一口氣同步產生最終 HTML,若是 React 組件樹很龐大,這樣一個同步過程可能比較耗時。
renderToNodeStream 把渲染結果以「流」的形式塞給 response 對象,這意味着不用等到全部 HTML 都渲染出來了纔給瀏覽器端返回結果,「流」的做用就是有多少內容給多少內容,這樣能夠改進首屏渲染時間。
React 有一個特色,就是把內容展現和動態功能集中在一個組件中。好比,一個 Counter 組件既負責怎麼畫出內容,也要負責怎麼響應按鍵點擊,這固然符合軟件高內聚性的原則,可是也給服務器端渲染帶來更多的工做。
設想一下,若是隻使用服務器端渲染,那麼產生的只有 HTML,雖然可以讓瀏覽器端畫出內容,可是,沒有 JS 的輔助是沒法響應用戶交互事件的。
如何讓頁面響應用戶事件?其實咱們已經作過這件事了,Counter 組件裏面已經有對按鈕事件的處理,咱們所要作的只是讓 Counter 組件在瀏覽器端從新執行一遍,也就是 mount 一遍就能夠了。
也就是說,若是想要動態交互效果,使用 React 服務器端渲染,必須也配合使用瀏覽器端渲染。
如今問題變得更加有趣了,在服務器端咱們給 Counter 一個初始值(這個值能夠不是缺省的 0),讓 Counter 渲染產生 HTML,這些 HTML 要傳遞給瀏覽器端,爲了讓 Counter 的 HTML「活」起來點擊相應事件,必需要在瀏覽器端從新渲染一遍 Counter 組件。在瀏覽器端渲染 Counter 以前,用戶就能夠看見 Counter 組件的內容,可是沒法點擊交互,要想點擊交互,就必需要等到瀏覽器端也渲染一次 Counter 以後。
接下來的一個問題,若是服務器端塞給 Counter 的數據和瀏覽器端塞給 Counter 的數據不同呢?
在 React v16 以前,React 在瀏覽器端渲染以後,會把內容和服務器端給的 HTML 作一個比對。若是徹底同樣,那最好,接着用服務器端 HTML 就行了;若是有一丁點不同,就會馬上丟掉服務器端的 HTML,從新渲染瀏覽器端產生的內容,結果就是用戶能夠看到界面閃爍。由於 React 拋棄的是整個服務器端渲染內容,組件樹越大,這個閃爍效果越明顯。
React 在 v16 以後,作了一些改進,再也不要求整個組件樹兩端渲染結果分絕不差,可是若是發生不一致,依然會拋棄局部服務器端渲染結果。
總之,若是用服務器端渲染,必定要讓服務器端塞給 React 組件的數據和瀏覽器端一致。
爲了達到這一目的,必須把傳給 React 組件的數據給保留住,隨着 HTML 一塊兒傳遞給瀏覽器網頁,這個過程,叫作「脫水」(Dehydrate);在瀏覽器端,就直接拿這個「脫水」數據來初始化 React 組件,這個過程叫「注水」(Hydrate)。
前面提到過 React v16 以後用 React.hydrate 替換 React.render,這個 hydrate 就是「注水」。
總之,爲了實現React的服務器端渲染,必需要處理好這兩個問題:
有不少文章都提到同構應用,其實就是首屏使用服務端渲染,後面使用瀏覽器端渲染的應用。
上文提到服務器端渲染,不過,服務器端渲染的問題並不這麼簡單,一個最直接的問題,就是怎麼處理多個頁面的『單頁應用』?
所謂單頁應用,就是雖然用戶感受有多個頁面,可是實現上只有一個頁面,用戶感受到頁面能夠來回切換,但其實只是一個頁面並無徹底刷新,只是局部界面更新而已。
假設一個單頁應用有三個頁面 Home、Prodcut 和 About,分別對應的的路徑是 /home、/product 和 /about,並且三個頁面都依賴於 API 調用來獲取外部數據。
如今咱們要作服務器端渲染,若是隻考慮用戶直接在地址欄輸入 /home、/product 和 /about 的場景,很容易知足,按照上面說的套路作就是了。可是,這是一個單頁應用,用戶能夠在 Home 頁面點擊連接無縫切換到 Product,這時候 Product 要作徹底的瀏覽器端渲染。換句話說,每一個頁面都須要既支持服務器端渲染,又支持徹底的瀏覽器端渲染,更重要的是,對於開發者來講,確定不但願爲了這個頁面實現兩套程序,因此必須有同時知足服務器端渲染和瀏覽器端渲染的代碼表示方式。
咱們經過一個簡單的例子來說解Next.js中最重要的概念getInitialProps。
import React from 'react';
const timeout = (ms, result) => {
return new Promise (resolve => setTimeout (() => resolve (result), ms));
};
const Home = props => (
<h1> Hello {props.userName} </h1>
);
Home.getInitialProps = async () => {
return await timeout (200, {userName: 'Morgan'});
};
export default Home;
複製代碼
這裏模擬了一個延時操做用以獲取userName,並將其展現在頁面上。
這段代碼的關鍵在於getInitialProps。
這個 getiInitialProps 是 Next.js 最偉大的發明,它肯定了一個規範,一個頁面組件只要把訪問 API 外部資源的代碼放在 getInitialProps 中就足夠,其他的不用管,Next.js 天然會在服務器端或者瀏覽器端調用 getInitialProps 來獲取外部資源,並把外部資源以 props 的方式傳遞給頁面組件。
注意 getInitialProps 是頁面組件的靜態成員函數,也能夠在組件類中加上 static 關鍵字定義。
class Home extends React.Component {
static async getInitialProps () {}
}
複製代碼
咱們能夠這樣來看待 getInitialProps,它就是 Next.js 對錶明頁面的 React 組件生命週期的擴充。React 組件的生命週期函數缺少對異步操做的支持,因此 Next.js 乾脆定義出一個新的生命週期函數 getInitialProps,在調用 React 原生的全部生命週期函數以前,Next.js 會調用 getInitialProps 來獲取數據,而後把得到數據做爲 props 來啓動 React 組件的原生生命週期過程。
這個生命週期函數的擴充十分巧妙,由於:
咱們打開Next應用的網頁源代碼,能夠看到相似下面的內容:
<script> __NEXT_DATA__ = { "props":{ "pageProps": {"userName":"Morgan"}}, "page":"/","pathname":"/","query":{},"buildId":"-","assetPrefix":"","nextExport":false,"err":null,"chunks":[]} </script>
複製代碼
Next.js 在作服務器端渲染的時候,頁面對應的 React 組件的 getInitialProps 函數被調用,異步結果就是「脫水」數據的重要部分,除了傳給頁面 React 組件完成渲染,還放在內嵌 script 的 NEXT_DATA 中,這樣,在瀏覽器端渲染的時候,是不會去調用 getInitialProps 的,直接經過 NEXT_DATA 中的「脫水」數據來啓動頁面 React 組件的渲染。
這樣一來,若是 getInitialProps 中有調用 API 的異步操做,只在服務器端作一次,瀏覽器端就不用作了。
那麼,getInitialProps 何時會在瀏覽器端調用呢?
當在單頁應用中作頁面切換的時候,好比從 Home 頁切換到 Product 頁,這時候徹底和服務器端不要緊,只能靠瀏覽器端本身了,Product頁面的 getInitialProps 函數就會在瀏覽器端被調用,獲得的數據用來開啓頁面的 React 原生生命週期過程。
關鍵點是,瀏覽器可能會直接訪問 /home 或者 /product,也可能經過網頁切換訪問這兩個頁面,也就是說 Home 或者 Product 均可能被服務器端渲染,也可能徹底只有瀏覽器端渲染,不過,這對應用開發者來講無所謂,應用開發者只要寫好 getInitialProps,至於調用 getInitialProps 的時機,交給 Next.js 處理就行了。
react 提供了renderToString、renderToNodeStream、hydrate等API支持服務端渲染,但Facebook官方並無使用react的服務端渲染,致使react服務端渲染沒有一個官方標準。
服務端渲染的思路並不難,就是在服務端渲染出HTML傳給瀏覽器去解析。
難就難在,服務端渲染的數據從何而來。若是服務端渲染的數據和瀏覽器端渲染的數據不一致,瀏覽器端會從新執行渲染,頁面會出現閃爍。
爲何有了服務端渲染,還須要關注瀏覽器端渲染呢?
這是由於服務端渲染只是返回了HTML,頁面能繪製出來,卻沒辦法響應用戶操做,因此必須從新進行瀏覽器端渲染,讓頁面能夠正常響應用戶操做。當瀏覽器端渲染出的HTML跟服務端返回的HTML不一致時,就會出現上面說的閃爍。
要解決這個問題,就引入了「注水」跟「脫水」的概念。
服務端渲染時,獲取的數據,一方面用於生成最終的HTML,另外一方面,也會包含在HTML中返回給瀏覽器端,這個過程稱爲「脫水」。
瀏覽器端拿到脫水的數據進行渲染,就保證了渲染出的HTML跟服務端返回的一致,這個過程就是「注水」,涉及的API就是hydrate。
原理搞明白了,如何實施呢?若是咱們作的是單頁應用,那問題更加麻煩。由於用戶能夠經過不一樣的URL返回頁面,這意味着咱們每一個頁面都要既支持服務端渲染,也支持瀏覽器端渲染,但咱們確定不但願爲每一個頁面寫兩份代碼!
這就輪到 Next.js 登場了。
Next.js 是目前解決服務端渲染最好的框架。他經過增長 getInitialProps 這個API優雅的解決了上述的問題。
Next.js 將服務端渲染脫水後的數據,經過 NEXT_DATA 返回給瀏覽器端,首屏加載能夠使用該數據進行渲染,就保證先後數據一致,頁面不會閃爍。
瀏覽器可能會直接訪問 /home 或者 /product,也可能經過網頁切換訪問這兩個頁面,也就是說 Home 或者 Product 均可能被服務器端渲染,也可能徹底只有瀏覽器端渲染,不過,這對應用開發者來講無所謂,應用開發者只要寫好 getInitialProps,至於調用 getInitialProps 的時機,交給 Next.js 處理就行了。
開發react應用最經常使用的調試工具備:ESLint,Prettier,React DevTool,Redux DevTool
ESLint
Prettier
小技巧:在Chrome中監測react性能:在URL後加?react_perf,好比:localhost:3000/?react_perf
React 讓前端單元測試變得容易:
React 組件的單元測試,只有三個要點:
「測試驅動」難以開展,主要有幾個緣由:
Jest 較好地解決了上面說的問題,由於 Jest 最重要的一個特性,就是支持並行執行。
Jest 爲每個單元測試文件創造一個獨立的運行環境,換句話說,Jest 會啓動一個進程執行一個單元測試文件,運行結束以後,就把這個執行進程廢棄了,這個單元測試文件即便寫得比較差,把全局變量污染得一團糟,也不會影響其餘單元測試文件,由於其餘單元測試文件是用另外一個進程來執行。
更妙的是,由於每一個單元測試文件之間再無糾葛,Jest 能夠啓動多個進程同時運行不一樣的文件,這樣就充分利用了電腦的多 CPU 多核,單進程 100 秒才完成的測試執行過程,8 核只須要 12.5 秒,速度快了不少。
使用 create-react-app 產生的項目自帶 Jest 做爲測試框架,不奇怪,由於 Jest 和 React 同樣都是出自 Facebook。
jest中文教程
Enzyme 是最受歡迎的 React 測試工具庫。
Enzyme 的官網地址
一個應用不光全部的單元測試都要經過,並且全部單元測試都必須覆蓋到代碼 100% 的角落。
若是對覆蓋率的要求低於 100%,時間一長,質量一定會愈來愈下滑
在 create-react-app 創造的應用中,已經自帶了代碼覆蓋率的支持,運行下面的命令,不光會運行全部單元測試,也會獲得覆蓋率彙報。
npm test -- --coverage 代碼覆蓋率包含四個方面:
只有四個方面都是 100%,纔算真的 100%。
隨着技術生態的發展,和應用問題的變遷,技術的應用場景和流行趨勢會受到影響。這層回答「誰用,用在哪」的問題,反映你對技術應用領域的認識寬度。