在這片文章里我們將討論任何使用高階組件保持你的react應用整潔,結構合理,易於維護。純函數是如何保持代碼整潔並且如何將這些原則應用到react組件中。

純函數

如果一個函數遵循以下幾個規則那麼他就是純函數:

  • 函數處理的數據都是作為參數傳遞給函數。
  • 他不接受不確定的數據或者任何其他數據(他們通常是副作用的)。
  • 同樣的輸入,函數將得到同樣的輸出。

舉個例子,下面的add函數就是個純函數:

function add(x, y) {
return x + y;
}

下面badadd就是不是純函數:

var y = 2;
function badAdd(x) {
return x + y;
}

這個函數之所以不是純函數是因為他所引用的數據不是直接給予的(不是通過參數傳遞,而是直接引用了外部全局變數。)。所以當你輸入相同的數據時候可能得到不一樣的輸出。

var y = 2;
badAdd(3) // 5
y = 3;
badAdd(3) // 6

高階函數

高階函數的定義是,當一個函數被調用他將輸出另一個函數。有時他們把函數作為參數,但是這個不是高階函數的必要條件。

讓我們再來看看前面說過的 add 函數,我們希望他在返回結果前,console一下結果。我們不能改變 add 函數,所以我們要創建一個新函數來替代他:

function addAndLog(x, y) {
var result = add(x, y);
console.log(Result, result);
return result;
}

我們顯示函數的記錄結果是有用的,現在我們要對減法函數做同樣的事情。而不是複製上述內容,我們可以編寫一個更高階的函數,它可以執行一個函數接受函數作為參數,被調用的函數會返回結果和顯示記錄,函數則會返回新的函數。

function logAndReturn(func) {
return function() {
var args = Array.prototype.slice.call(arguments);
var result = func.apply(null, args);
console.log(Result, result);
return result;
}
}

現在我們可以使用這個函數為 addsubtract 函數添加顯示記錄:

var addAndLog = logAndReturn(add);
addAndLog(4, 4) // 8是返回值, 『Result 8』 是顯示;
var subtractAndLog = logAndReturn(subtract);
subtractAndLog(4, 3) // 1是返回值, 『Result 1』是顯示;

logAndReturn 就是一個高階函數因為他接受一個函數作為參數,並且返回一個函數被我們調用。封裝且不改變現有函數的內部實現是非常有用的。

高階組件

現在我們討論討論React,上面講到的邏輯我們同樣可以在React組件上使用,以賦予他們更多的行為。

在這節我們將使用React Router,這個是React的路由解決方案。如果你開始使用庫,我強烈建議你使用GitHub上的React Router Tutorial。

路由鏈接組件

React 路由提供 <Link> 用於鏈接React應用里的頁面。<Link> 組件其中一個屬性是activeClassName。當 <Link> 組件處於活動狀態時使用了這個屬性(用戶經過鏈接時候的意思),組件將使用這個樣類名,以便開發人員能夠對其樣式進行美化。

這是一個非常有用的功能,在我們假設的應用程序中,我們決定我們總是使用這個屬性。然而,很快我們會發現這會使 <Link> 組件變得臃腫:

<Link to="/" activeClassName="active-link"> Home </Link>
<Link to="/about" activeClassName="active-link"> About</Link>
<Link to="/contact" activeClassName="active-link"> Contact</Link>

注意我們每次都必須重複使用那個類的屬性名。這不僅使我們的組件臃腫,這也意味著如果我們決定改變類名,我們必須在很多地方進行修改。

替換,我們要寫一個組件去封裝 <Link> 組件:

var AppLink=React.createClass({
render:function(){
return(
<Link to={this.props.to} activeClassName="active-link">
{this.props.children}
</Link>;
);
}
});

現在我們可以使用這個組件來整合我們的鏈接:

<AppLink to="/">Home</AppLink>
<AppLink to="/about">About</AppLink>
<AppLink to="/contact">Contact</AppLink>

在React生態系統中,這些組件被稱為高階組件,因為他們採用現有組件並對他們進行封裝,但是並沒有更改現有組件。你也可以將他們視為封裝組件,但你會發現他們通常被稱為React-based內容中的高階組件。

函數式無狀態組件

React0.14開始提供函數式無狀態組件,函數式無狀態組件具有以下特點:

  • 他們沒有state
  • 他們無法使用React生命周期函數(比如 componentWillMount()
  • 他們只定義了 render 方法,沒有其他的了。

當一個組件遵守上述內容時,我們可以將其定義為一個函數,而不是使用React.createClass(如果你使用了 ES2015 classes 你也可以使用class 來擴展你的React 組件)。下面的兩個表達式產生相同的組件:

var App = React.createClass({
render: function() {
return <p>My name is { this.props.name }</p>;
}
});
var App = function(props) {
return <p>My name is { props.name }</p>;
}

在函數式無狀態組件中,不是引用this.props,而是將props用作參數。React 文檔有更多介紹。

因為更高階的組件通常會封裝一個現有的組件,所以您經常會發現可以將它們定義為函數組件。在本文的其餘部分,我將儘可能地這樣做。

更好的高階組件

我們不只局限於組件可以正常運行,我們希望做的更好。我們創建的 AppLink 組件就不太適合。

接受多個參數

<AppLink> 組件估計會有兩個參數:

  • this.props.to 表示鏈接讓用戶訪問的URL
  • this.props.children 表示顯示的文本

當然,<Link> 組件可以接受更多發屬性,有時我們只是想組件允許傳遞額外的屬性以及上述的兩個屬性。我們並沒有通過代碼屬性讓 <AppLink> 有很高的擴展性。

JSX 傳遞

JSX,一種類似於HTML的語法用於定義React組件,支持擴展操作符將對象作為屬性傳遞給組件。例如,下面的代碼示例實現了同樣的事情:

var props={a:1,b:2};
<Foo a={props.a }b={props.b} />
<Foo {...props} />

使用{... props}擴展對象中的每個鍵,並將其作為單個屬性傳遞給Foo。

我們可以利用 <AppLink> 這個技巧,讓我們支持 <Link> 支持的任意屬性。通這樣做,我們也將來證明自己;如果 <Link> 在將來添加任何新的屬性,我們的包裝器組件將已經支持它們。在我們這樣做的時候,我同時要將 AppLink 改成函數式組件。

var AppLink = function(props){
return <Link {...props} activeClassName="active-link" />;
}

現在將接受任何屬性並傳遞它們。請注意,我們還可以使用筆和標籤,而不是在標籤之間傳遞{props.children}。React允許 children 作為常規 prop 或作為在開始和結束標籤之間的組件的子元素進行傳遞。

React中屬性的排序
想像一下,對於頁面上的一個特定鏈接,你必須使用不同的 activeClassName。嘗試將其傳遞到 <AppLink> 中,我們通過以下方式傳遞所有屬性:

<AppLink to="/special-link" activeClassName="special-active" >
Special Secret Link
</AppLink>

但是,這不行。渲染 <Link> 組件時屬性的順序:

return < Link {...props} activeClassName="active-link" />;

當您在React組件中多次具有相同的屬性時,最後一個會被使用。這就意味著放在 {...this.props} 後面的 activeClassName=「active-link」 會被使用。我們可以把 this.props 放在最後來解決這個問題。這意味著如果用戶真的需要我們可以設置一些想要合理的默認值,但是這些默認值是可以被覆蓋:

return <Link activeClassName="active-link" {...props} />;

通過創建一個逛街組件可以使現有的組件得到額外的功能,我們盡量保持我們的代碼簡潔,並且通過不重複的屬性來應對未來的變化,同時把他們的值保存在一個地方。

高階組件創造者

通常你需要給很多組件添加同樣的方法。這就和我們前面說到的給 add 函數和 subtract 函數添加顯示輸出一樣。

讓我們想像一下,在你的應用程序中,你有一個對象,其中包含關於系統進行身份驗證的當前用戶的信息。你需要你的 React 組件可以訪問這些信息,而不是盲目地使每個組件都可以訪問,你更希望嚴格地控制哪些組件接收信息。

解決的方法就是我們可以創建一個函數去調用一個 React 組件。函數會返回一個可以訪問用戶信息並且把原來組件進行渲染的新 React 組件。

聽起來很複雜,但是通過一些代碼就可以很簡潔了:

function wrapWithUser(Component){
// 收限制的用戶信息
var secretUserInfo = {
name:Jack Franklin,
favouriteColour:blue
};
// 返回一個新的 React 組件
// 使用一個無狀態函數式組件
return function(props){
// 用戶信息作為屬性進行傳遞
// 其他屬性我們可以自定義
return <Component user={secretUserInfo} {...props} />
}
}

函數通過一個React組件(自定義的React組件必須有大寫字母開頭)並返回一個新的函數,它將使用設置在 secretUserInfo 里的用戶的屬性來渲染那個組件。

現在我們有一個 <AppHeader> 組件需要這些信息,以便把登陸的用戶信息展示出來:

var AppHeader=function(props){
if(props.user){
return <p>Logged in as {props.user.name} </p>;
} else {
return <p>You need to login</p>;
}
}

最後一步是將此組件連接起來,我們可以通過 wrapWithUser 函數創建新的組件,以便把 this.props.user 傳遞給他。

var ConnectedAppHeader = wrapWithUser(AppHeader);

現在我們有了 <ConnectedAppHeader> 組件可以訪問 user 對象,並且渲染原來的組件。

我給這個組件起名叫 ConnectedAppHeader 因為他傳遞了一些不希望所有組件都可以使用的額外數據。

這種模式在 React 庫中非常常見,特別是在 Redux 中,知道你依賴的第三方庫是如何工作的,它被使用的原因將幫助你隨著你的應用程序一起成長。

結論

這篇文章即將結束,通過應用諸如純函數和更高階函數的函數式編程的原理應用到React,您可以創建一個更容易維護和使用的代碼庫。

通過創建更高階的組件,你只能將數據定義在一個位置,使重構更容易。更高階的函數創建者使你能夠將大部分數據保留為私有,並且僅將數據片段暴露給真正需要的組件。通過這樣做,你可以清楚地知道哪些組件正在使用哪些數據,並且隨著應用程序的發展,你會發現這是有益的。


推薦閱讀:
相关文章