原文地址html
本文從屬於筆者的React入門與最佳實踐系列,推薦閱讀GUI應用程序架構的十年變遷:MVC,MVP,MVVM,Unidirectional,Clean前端
React組件一個很大的特性在於其擁有本身完整的生命週期,所以咱們能夠將React組件視做可自運行的小型系統,它擁有本身的內部狀態、輸入與輸出。react
對於React組件而言,其輸入的來源就是Props,咱們會用以下方式向某個React組件傳入數據:git
// Title.jsx class Title extends React.Component { render() { return <h1>{ this.props.text }</h1>; } }; Title.propTypes = { text: React.PropTypes.string }; Title.defaultProps = { text: 'Hello world' }; // App.jsx class App extends React.Component { render() { return <Title text='Hello React' />; } };
text
是Text
組件本身的輸入域,父組件App
在使用子組件Title
時候應該提供text
屬性值。除了標準的屬性名以外,咱們還會用到以下兩個設置:github
propTypes:用於定義Props的類型,這有助於追蹤運行時誤設置的Prop值。web
defaultProps:定義Props的默認值,這個在開發時頗有幫助segmentfault
Props中還有一個特殊的屬性props.children
能夠容許咱們使用子組件:後端
class Title extends React.Component { render() { return ( <h1> { this.props.text } { this.props.children } </h1> ); } }; class App extends React.Component { render() { return ( <Title text='Hello React'> <span>community</span> </Title> ); } };
注意,若是咱們不主動在Title
組件的render
函數中設置{this.props.children}
,那麼span
標籤是不會被渲染出來的。除了Props以外,另外一個隱性的組件的輸入便是context
,整個React組件樹會擁有一個context
對象,它能夠被樹中掛載的每一個組件所訪問到,關於此部分更多的內容請參考依賴注入這一章節。架構
組件最明顯的輸出就是渲染後的HTML文本,便是React組件渲染結果的可視化展現。固然,部分包含了邏輯的組件也可能發送或者觸發某些Action或者Event。app
class Title extends React.Component { render() { return ( <h1> <a onClick={ this.props.logoClicked }> <img src='path/to/logo.png' /> </a> </h1> ); } }; class App extends React.Component { render() { return <Title logoClicked={ this.logoClicked } />; } logoClicked() { console.log('logo clicked'); } };
在App
組件中咱們向Title
組件傳入了能夠從Title
調用的回調函數,在logoClicked
函數中咱們能夠設置或者修改須要傳回父組件的數據。須要注意的是,React並無提供能夠訪問子組件狀態的API,換言之,咱們不能使用this.props.children[0].state
或者相似的方法。正確的從子組件中獲取數據的方法應該是在Props中傳入回調函數,而這種隔離也有助於咱們定義更加清晰的API而且促進了所謂單向數據流。
React最大的特性之一便是其強大的組件的可組合性,實際上除了React以外,筆者並不知道還有哪一個框架可以提供如此簡單易用的方式來建立與組合各式各樣的組件。本章咱們會一塊兒討論些經常使用的組合技巧,咱們以一個簡單的例子來進行講解。假設在咱們的應用中有一個頁首欄目,而且其中放置了導航欄。咱們建立了三個獨立的React組件:App
,Header
以及Navigation
。將這三個組件依次嵌套組合,能夠獲得如下的代碼:
<App> <Header> <Navigation> ... </Navigation> </Header> </App>
而在JSX中組合這些組件的方式就是在須要的時候引用它們:
// app.jsx import Header from './Header.jsx'; export default class App extends React.Component { render() { return <Header />; } } // Header.jsx import Navigation from './Navigation.jsx'; export default class Header extends React.Component { render() { return <header><Navigation /></header>; } } // Navigation.jsx export default class Navigation extends React.Component { render() { return (<nav> ... </nav>); } }
不過這種方式卻可能存在如下的問題:
咱們將App
當作各個組件間的鏈接線,也是整個應用的入口,所以在App
中進行各個獨立組件的組合是個不錯的方法。不過Header
元素中可能包含像圖標、搜索欄或者Slogan這樣的元素。而若是咱們須要另外一個不包含Navigation
功能的Header
組件時,像上面這種直接將Navigation
組件硬編碼進入Header
的方式就會難於修改。
這種硬編碼的方式會難以測試,若是咱們在Header
中加入一些自定義的業務邏輯代碼,那麼在測試的時候當咱們要建立Header
實例時,由於其依賴於其餘組件而致使了這種依賴層次過深(這裏不包含Shallow Rendering這種僅渲染父組件而不渲染嵌套的子組件方式)。
children
APIReact爲咱們提供了this.props.children
來容許父組件訪問其子組件,這種方式有助於保證咱們的Header
獨立而且不須要與其餘組件解耦合。
// App.jsx export default class App extends React.Component { render() { return ( <Header> <Navigation /> </Header> ); } } // Header.jsx export default class Header extends React.Component { render() { return <header>{ this.props.children }</header>; } };
這種方式也有助於測試,咱們能夠選擇輸入空白的div
元素,從而將要測試的目標元素隔離開來而專一於咱們須要測試的部分。
React組件能夠接受Props做爲輸入,咱們也能夠選擇將須要封裝的組件以Props方式傳入:
// App.jsx class App extends React.Component { render() { var title = <h1>Hello there!</h1>; return ( <Header title={ title }> <Navigation /> </Header> ); } }; // Header.jsx export default class Header extends React.Component { render() { return ( <header> { this.props.title } <hr /> { this.props.children } </header> ); } };
這種方式在咱們須要對傳入的待組合組件進行一些修正時很是適用。
Higher-Order Components模式看上去很是相似於裝飾器模式,它會用於包裹某個組件而後爲其添加一些新的功能。這裏展現一個簡單的用於構造Higher-Order Component的函數:
var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} /> ) } }; export default enhanceComponent;
一般狀況下咱們會構建一個工廠函數,接收原始的組件而後返回一個所謂的加強或者包裹後的版本,譬如:
var OriginalComponent = () => <p>Hello world.</p>; class App extends React.Component { render() { return React.createElement(enhanceComponent(OriginalComponent)); } };
通常來講,高階組件的首要工做就是渲染原始的組件,咱們常常也會將Props與State傳遞進去,將這兩個屬性傳遞進去會有助於咱們創建一個數據代理。HOC模式容許咱們控制組件的輸入,即將須要傳入的數據以Props傳遞進去。譬如咱們須要爲原始組件添加一些配置:
var config = require('path/to/configuration'); var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} title={ config.appTitle } /> ) } };
這裏對於configuration
的細節實現會被隱藏到高階組件中,原始組件只須要了解從Props中獲取到title
變量而後渲染到界面上。原始組件並不會關心變量存於何地,從何而來,這種模式最大的優點在於咱們可以以獨立的模式對該組件進行測試,而且能夠很是方便地對該組件進行Mocking。在HOC模式下咱們的原始組件會變成這樣子:
var OriginalComponent = (props) => <p>{ props.title }</p>;
咱們寫的大部分組件與模塊都會包含一些依賴,合適的依賴管理有助於建立良好可維護的項目結構。而所謂的依賴注入技術正是解決這個問題的經常使用技巧,不管是在Java仍是其餘應用程序中,依賴注入都受到了普遍的使用。而React中對於依賴注入的須要也是顯而易見的,讓咱們假設有以下的應用樹結構:
// Title.jsx export default function Title(props) { return <h1>{ props.title }</h1>; } // Header.jsx import Title from './Title.jsx'; export default function Header() { return ( <header> <Title /> </header> ); } // App.jsx import Header from './Header.jsx'; class App extends React.Component { constructor(props) { super(props); this.state = { title: 'React in patterns' }; } render() { return <Header />; } };
title
這個變量的值是在App
組件中被定義好的,咱們須要將其傳入到Title
組件中。最直接的方法就是將其從App
組件傳入到Header
組件,而後再由Header
組件傳入到Title
組件中。這種方法在這裏描述的簡單的僅有三個組件的應用中仍是很是清晰可維護的,不過隨着項目功能與複雜度的增長,這種層次化的傳值方式會致使不少的組件要去考慮它們並不須要的屬性。在上文所講的HOC模式中咱們已經使用了數據注入的方式,這裏咱們使用一樣的技術來注入title
變量:
// enhance.jsx var title = 'React in patterns'; var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} title={ title } /> ) } }; export default enhanceComponent; // Header.jsx import enhance from './enhance.jsx'; import Title from './Title.jsx'; var EnhancedTitle = enhance(Title); export default function Header() { return ( <header> <EnhancedTitle /> </header> ); }
在上文這種HOC模式中,title
變量被包含在了一個隱藏的中間層中,咱們將其做爲Props值傳入到原始的Title
變量中而且獲得一個新的組件。這種方式思想是不錯,不過仍是隻解決了部分問題。如今咱們能夠不去顯式地將title
變量傳遞到Title
組件中便可以達到一樣的enhance.jsx
效果。
React爲咱們提供了context
的概念,context
是貫穿於整個React組件樹容許每一個組件訪問的對象。有點像所謂的Event Bus,一個簡單的例子以下所示:
// a place where we'll define the context var context = { title: 'React in patterns' }; class App extends React.Component { getChildContext() { return context; } ... }; App.childContextTypes = { title: React.PropTypes.string }; // a place where we need data class Inject extends React.Component { render() { var title = this.context.title; ... } } Inject.contextTypes = { title: React.PropTypes.string };
注意,咱們要使用context對象必需要經過childContextTypes
與contextTypes
指明其構成。若是在context
對象中未指明這些那麼context
會被設置爲空,這可能會添加些額外的代碼。所以咱們最好不要將context
當作一個簡單的object對象而爲其設置一些封裝方法:
// dependencies.js export default { data: {}, get(key) { return this.data[key]; }, register(key, value) { this.data[key] = value; } }
這樣,咱們的App
組件會被改形成這樣子:
import dependencies from './dependencies'; dependencies.register('title', 'React in patterns'); class App extends React.Component { getChildContext() { return dependencies; } render() { return <Header />; } }; App.childContextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func };
而在Title
組件中,咱們須要進行以下設置:
// Title.jsx export default class Title extends React.Component { render() { return <h1>{ this.context.get('title') }</h1> } } Title.contextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func };
固然咱們不但願在每次要使用contextTypes
的時候都須要顯式地聲明一下,咱們能夠將這些聲明細節包含在一個高階組件中。
// Title.jsx import wire from './wire'; function Title(props) { return <h1>{ props.title }</h1>; } export default wire(Title, ['title'], function resolve(title) { return { title }; });
這裏的wire
函數的第一個參數是React組件對象,第二個參數是一系列須要注入的依賴值,注意,這些依賴值務必已經調用過register
函數。最後一個參數則是所謂的映射函數,它接收存儲在context
中的某個原始值而後返回React Props中須要的值。由於在這個例子裏context
中存儲的值與Title
組件中須要的值都是title
變量,所以咱們直接返回便可。不過在真實的應用中多是一個數據集合、配置等等。
export default function wire(Component, dependencies, mapper) { class Inject extends React.Component { render() { var resolved = dependencies.map(this.context.get.bind(this.context)); var props = mapper(...resolved); return React.createElement(Component, props); } } Inject.contextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func }; return Inject; };
這裏的Inject就是某個能夠訪問context
的高階組件,而mapper
就是用於接收context
中的數據並將其轉化爲組件所須要的Props的函數。實際上如今大部分的依賴注入的解決方案都是基於context
,我以爲了解這種方式的底層原理仍是頗有意義的。譬如如今流行的Redux
,其核心的connect
函數與Provider
組件都是基於context
。
單向數據流是React中主要的數據驅動模式,其核心概念在於組件並不會修改它們接收到的數據,它們只是負責接收新的數據然後從新渲染到界面上或者發出某些Action以觸發某些專門的業務代碼來修改數據存儲中的數據。咱們先設置一個包含一個按鈕的Switcher
組件,當咱們點擊該按鈕時會觸發某個flag
變量的改變:
class Switcher extends React.Component { constructor(props) { super(props); this.state = { flag: false }; this._onButtonClick = e => this.setState({ flag: !this.state.flag }); } render() { return ( <button onClick={ this._onButtonClick }> { this.state.flag ? 'lights on' : 'lights off' } </button> ); } }; // ... and we render it class App extends React.Component { render() { return <Switcher />; } };
此時咱們將全部的數據放置到組件內,換言之,Switcher
是惟一的包含咱們flag
變量的地方,咱們來嘗試下將這些數據託管於專門的Store中:
var Store = { _flag: false, set: function(value) { this._flag = value; }, get: function() { return this._flag; } }; class Switcher extends React.Component { constructor(props) { super(props); this.state = { flag: false }; this._onButtonClick = e => { this.setState({ flag: !this.state.flag }, () => { this.props.onChange(this.state.flag); }); } } render() { return ( <button onClick={ this._onButtonClick }> { this.state.flag ? 'lights on' : 'lights off' } </button> ); } }; class App extends React.Component { render() { return <Switcher onChange={ Store.set.bind(Store) } />; } };
這裏的Store
對象是一個簡單的單例對象,能夠幫助咱們設置與獲取_flag
屬性值。而經過將getter
函數傳遞到組件內,能夠容許咱們在Store
外部修改這些變量,此時咱們的應用工做流大概是這樣的:
User's input | Switcher -------> Store
假設咱們已經將flag
值保存到某個後端服務中,咱們須要爲該組件設置一個合適的初始狀態。此時就會存在一個問題在於同一份數據保存在了兩個地方,對於UI與Store
分別保存了各自獨立的關於flag
的數據狀態,咱們等於在Store
與Switcher
之間創建了雙向的數據流:Store ---> Switcher
與Switcher ---> Store
// ... in App component <Switcher value={ Store.get() } onChange={ Store.set.bind(Store) } /> // ... in Switcher component constructor(props) { super(props); this.state = { flag: this.props.value }; ...
此時咱們的數據流向變成了:
User's input | Switcher <-------> Store ^ | | | | | | v Service communicating with our backend
在這種雙向數據流下,若是咱們在外部改變了Store
中的狀態以後,咱們須要將改變以後的最新值更新到Switcher
中,這樣也在無形之間增長了應用的複雜度。而單向數據流則是解決了這個問題,它強制在全局只保留一個狀態存儲,一般是存放在Store中。在單向數據流下,咱們須要添加一些訂閱Store中狀態改變的響應函數:
var Store = { _handlers: [], _flag: '', onChange: function(handler) { this._handlers.push(handler); }, set: function(value) { this._flag = value; this._handlers.forEach(handler => handler()) }, get: function() { return this._flag; } };
而後咱們在App
組件中設置了鉤子函數,這樣每次Store
改變其值的時候咱們都會強制從新渲染:
class App extends React.Component { constructor(props) { super(props); Store.onChange(this.forceUpdate.bind(this)); } render() { return ( <div> <Switcher value={ Store.get() } onChange={ Store.set.bind(Store) } /> </div> ); } };
注意,這裏使用的forceUpdate
並非一個推薦的用法,咱們一般會使用HOC模式來進行重渲染,這裏使用forceUpdate
只是用於演示說明。在基於上述的改造,咱們就不須要在組件中繼續保留內部狀態:
class Switcher extends React.Component { constructor(props) { super(props); this._onButtonClick = e => { this.props.onChange(!this.props.value); } } render() { return ( <button onClick={ this._onButtonClick }> { this.props.value ? 'lights on' : 'lights off' } </button> ); } };
這種模式的優點在於會將咱們的組件改造爲簡單的Store
中數據的呈現,此時纔是真正無狀態的View。咱們能夠以徹底聲明式的方式來編寫組件,而將應用中複雜的業務邏輯放置到單獨的地方。此時咱們應用程序的流圖變成了:
Service communicating with our backend ^ | v Store <----- | | v | Switcher ----> ^ | | User input
在這種單向數據流中咱們再也不須要同步系統中的多個部分,這種單向數據流的概念並不只僅適用於基於React的應用。
關於Flux的簡單瞭解能夠參考筆者的GUI應用程序架構的十年變遷:MVC,MVP,MVVM,Unidirectional,Clean
Flux是用於構建用戶交互界面的架構模式,最先由Facebook在f8大會上提出,自此以後,不少的公司開始嘗試這種概念而且貌似這是個很不錯的構建前端應用的模式。Flux常常和React一塊兒搭配使用,筆者自己在平常的工做中也是使用React+Flux的搭配,給本身帶來了很大的遍歷。
Flux中最主要的角色爲Dispatcher,它是整個系統中全部的Events的中轉站。Dispatcher負責接收咱們稱之爲Actions的消息通知而且將其轉發給全部的Stores。每一個Store實例自己來決定是否對該Action感興趣而且是否相應地改變其內部的狀態。當咱們將Flux與熟知的MVC相比較,你就會發現Store在某些意義上很相似於Model,兩者都是用於存放狀態與狀態中的改變。而在系統中,除了View層的用戶交互可能觸發Actions以外,其餘的相似於Service層也可能觸發Actions,譬如在某個HTTP請求完成以後,請求模塊也會發出相應類型的Action來觸發Store中對於狀態的變動。
而在Flux中有個最大的陷阱就是對於數據流的破壞,咱們能夠在Views中訪問Store中的數據,可是咱們不該該在Views中修改任何Store的內部狀態,全部對於狀態的修改都應該經過Actions進行。做者在這裏介紹了其維護的某個Flux變種的項目fluxiny。
大部分狀況下咱們在系統中只須要單個的Dispatcher,它是相似於粘合劑的角色將系統的其餘部分有機結合在一塊兒。Dispatcher通常而言有兩個輸入:Actions與Stores。其中Actions須要被直接轉發給Stores,所以咱們並不須要記錄Actions的對象,而Stores的引用則須要保存在Dispatcher中。基於這個考慮,咱們能夠編寫一個簡單的Dispatcher:
var Dispatcher = function () { return { _stores: [], register: function (store) { this._stores.push({ store: store }); }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action); }); } } } };
在上述實現中咱們會發現,每一個傳入的Store
對象都應該擁有一個update
方法,所以咱們在進行Store的註冊時也要來檢測該方法是否存在:
register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { this._stores.push({ store: store }); } }
在完成了對於Store的註冊以後,下一步咱們就是須要將View與Store關聯起來,從而在Store發生改變的時候可以觸發View的重渲染:
不少flux的實現中都會使用以下的輔助函數:
Framework.attachToStore(view, store);
不過做者並非很喜歡這種方式,這樣這樣會要求View中須要調用某個具體的API,換言之,在View中就須要瞭解到Store的實現細節,而使得View與Store又陷入了緊耦合的境地。當開發者打算切換到其餘的Flux框架時就不得不修改每一個View中的相對應的API,那又會增長項目的複雜度。另外一種可選的方式就是使用React mixins
:
var View = React.createClass({ mixins: [Framework.attachToStore(store)] ... });
使用mixin
是個不錯的修改現有的React 組件而不影響其原有代碼的方式,不過這種方式的缺陷在於它不可以以一種Predictable的方式去修改組件,用戶的可控性較低。還有一種方式就是使用React context
,這種方式容許咱們將值跨層次地傳遞給React組件樹中的組件而不須要了解它們處於組件樹中的哪一個層級。這種方式和mixins可能有相同的問題,開發者並不知道該數據從何而來。
做者最終選用的方式便是上面說起到的Higher-Order Components模式,它創建了一個包裹函數來對現有組件進行從新打包處理:
function attachToStore(Component, store, consumer) { const Wrapper = React.createClass({ getInitialState() { return consumer(this.props, store); }, componentDidMount() { store.onChangeEvent(this._handleStoreChange); }, componentWillUnmount() { store.offChangeEvent(this._handleStoreChange); }, _handleStoreChange() { if (this.isMounted()) { this.setState(consumer(this.props, store)); } }, render() { return <Component {...this.props} {...this.state} />; } }); return Wrapper; };
其中Component
代指咱們須要附着到Store
中的View,而consumer
則是應該被傳遞給View的Store中的部分的狀態,簡單的用法爲:
class MyView extends React.Component { ... } ProfilePage = connectToStores(MyView, store, (props, store) => ({ data: store.get('key') }));
這種模式的優點在於其有效地分割了各個模塊間的職責,在該模式中Store並不須要主動地推送消息給View,而主須要簡單地修改數據而後廣播說個人狀態已經更新了,而後由HOC去主動地抓取數據。那麼在做者具體的實現中,就是選用了HOC模式:
register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer) { consumers.push(consumer); }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } }
另外一個常見的用戶場景就是咱們須要爲界面提供一些默認的狀態,換言之當每一個consumer
註冊的時候須要提供一些初始化的默認數據:
var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; };
綜上所述,最終的Dispatcher函數以下所示:
var Dispatcher = function () { return { _stores: [], register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } } } };
Actions就是在系統中各個模塊之間傳遞的消息載體,做者以爲應該使用標準的Flux Action模式:
{ type: 'USER_LOGIN_REQUEST', payload: { username: '...', password: '...' } }
其中的type
屬性代表該Action所表明的操做而payload
中包含了相關的數據。另外,在某些狀況下Action中沒有帶有Payload,所以可使用Partial Application方式來建立標準的Action請求:
var createAction = function (type) { if (!type) { throw new Error('Please, provide action\'s type.'); } else { return function (payload) { return dispatcher.dispatch({ type: type, payload: payload }); } } }
上文咱們已經瞭解了核心的Dispatcher與Action的構造過程,那麼在這裏咱們將這兩者組合起來:
var createSubscriber = function (store) { return dispatcher.register(store); }
而且爲了避免直接暴露dispatcher對象,咱們能夠容許用戶使用createAction
與createSubscriber
這兩個函數:
var Dispatcher = function () { return { _stores: [], register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } } } }; module.exports = { create: function () { var dispatcher = Dispatcher(); return { createAction: function (type) { if (!type) { throw new Error('Please, provide action\'s type.'); } else { return function (payload) { return dispatcher.dispatch({ type: type, payload: payload }); } } }, createSubscriber: function (store) { return dispatcher.register(store); } } } };