React.js 模式

前言

我想找一個好的前端前端框架,找了好久。這個框架將可以幫助我寫出具備可擴展性、可維護性 UI 的代碼。經過對 React.js 優點的理解,我認爲「我找到了它」。在我大量的使用過程當中,我發現了一些模式性的東西。這些技術被一次又一次的用於編程開發之中。此時,我將它寫下來、討論和分享這些我發現的模式。javascript

這些全部的代碼都是可用的,可以在 https://github.com/krasimir/react-in-patterns 中下載。我可能不會更新個人博客,可是我將一直在 GitHub 中發佈一些東西。我也將鼓勵你在 GitHub 中討論這些模式,經過 issue 或者直接 pull request 的方式。html

1、React 本身的交流方式(Communication)

在使用 React 構建了幾個月的狀況下,你將可以體會到每個 React Component 都是一個小系統,它可以本身運做。它有本身的 state、input、output.前端

Input

React Component 經過 props 做爲 input(以後用輸入代替)。下面咱們來寫一個例子:java

// 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' />;
        }
    };

其中的 Title 組件只有一個輸入 - text. 在父組件(App)提供了一個屬性,經過 <Title> 組件。在 Title 組件中咱們添加了兩個設置 propTypesdefaultProps,咱們來單獨看一下:react

  • propTypes - 定義 props 的類型,這將幫助咱們告訴 React 咱們將傳什麼類型的 prop,可以對這個 prop 進行驗證(或者說是測試)。
  • defaultProps - 定義 props 默認的值,設置一個默認值是一個好習慣。

還有一個 props.children 屬性,可以讓咱們訪問到當前組件的子組件。好比:git

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 標籤(孩子組件)將不會被渲染。程序員

對於一個組件的間接性輸入(就是多層組件傳遞數據的時候),咱們也能夠調用 context 進行數據的訪問。在整個 React tree 中的每個組件中可能會有一個 context 對象。更多的說明將在依賴注入章節講解。github

Output

React 的輸出就是渲染事後的 HTML 代碼。在視覺上咱們將看到一個 React 組件的樣子。固然,有些組件可能包含一些邏輯,可以幫助咱們傳遞一些數據或者觸發一個事件行爲(這類組件可能不會有具體的 UI 形態)。爲了實現邏輯類型的組件,咱們將繼續使用組件的 props:編程

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');
        }
    };

咱們經過一個 callback 的方式在子組件中進行調用,logoClicked 方法可以接受一些數據,這樣咱們就可以從子組件向父組件傳輸一些數據了(這裏就是 React 方式的子組件向父組件通訊)。redux

咱們以前有提到咱們不可以訪問 child 的 state。或者換句話說,咱們不可以使用 this.props.children[0].state 的方式或者其餘什麼方式去訪問。正確的姿式應該是經過 props callback 的方式獲取子組件的一些信息。這是一件好事。這就迫使咱們要去定義明確的 APIs,並鼓勵使用單向數據流(在後面的單向數據流中將介紹)。

2、組件構成(composition)

源碼

另一個很棒的是 React 的可組合性。對於我來講,除了 React 以外尚未發現有任何框架可以如此簡單的方式去建立組件以及合併組件。這段我將探索一些組件的構建方式,來讓開發工做更加棒。

讓咱們先來看一個簡單的例子:

(1)假設咱們有一個應用,包含 header 部分,header 內部有一個 navigation(導航)組件。

(2)因此,咱們將有三個 React 組件:App、Header 和 Navigation。

(3)他們是層級嵌套的關係。

因此最後代碼以下:

<App>
    	<Header>
        	<Navigation> ... </Navigation>
    	</Header>
    </App>

咱們爲了組合這些小組件,而且引用他們,咱們須要向下面這樣定義他們:

// 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 組件作爲程序的入口,在這個組件裏面去構建組件是一個不錯的地方。對於 Header 組件,可能會包含其餘組件,好比 logo、search 或者 slogan 之類的。它將是很是好處理,能夠經過某種方式從外部傳入,所以咱們沒有須要建立一個強依賴的組件。若是咱們在另外的地方須要使用 Header 組件,可是這個時候又不須要內層的 Navigation 子組件。這個時候咱們就不容易實現,由於 Header 和 Navigation 組件是兩個強耦合的組件。
  • 這樣編寫組件是不容易測試的,咱們可能在 Header 組件中有一些業務邏輯,爲了測試 Header 組件,咱們就必需要建立一個 Header 的實例(其實就是引用組件來渲染)。然而,又由於 Header 組件依賴了其餘組件,這就致使了咱們也可能須要建立一些其餘組件的實例,這就讓測試不是那麼容易。而且咱們在測試過程當中,若是 Navigation 組件測試失敗,也將致使 Header 組件測試失敗,這將致使一個錯誤的測試結果(由於不會知道是哪一個組件測試沒有經過)。(注:而後在測試中 shallow rendering 解決了這個問題,可以只渲染 Header 組件,不用實例化其餘組件)。

使用 React's children API

在 React 中,咱們可以經過 this.props.children 來很方便的處理這個問題。這個屬性可以讓父組件讀取和訪問子組件。這個 API 將使咱們的 Header 組件更抽象和低耦合(原文是 dependency-free 很差翻譯,可是是這個意思)。

// 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>;
        }
    }

這將容易測試,由於咱們可讓 Header 組件渲染成一個空的 div 標籤。這就讓組件脫離出來,而後只專一於應用的開發(其實就是抽象了一層父組件,而後讓這個父組件和子組件進行了解耦,而後子組件可能纔是應用的一些功能實現)。

將 child 作爲一個屬性

每個 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>
            );
        }
    };

這個技術在咱們要合併兩個組件,這個組件在 Header 內部的時候是很是有用的,以及在外部提供這個須要合併的組件。

3、高階組件(Higher-order components)

源碼

高階組件看起來很像裝飾器模式。他是包裹一個組件和附加一些其餘功能或者 props 給它。

這裏經過一個函數來返回一個高階組件:

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));
        }
    };

首先,高階組件其實也是渲染的原始組件(傳入的組件)。一個好的習慣是直接傳入 state 和 props 給它。這將有助於咱們想代理數據和像是用原始組件同樣去使用這個高階組件。

高階組件讓咱們可以控制輸入。這些數據咱們想經過 props 進行傳遞。如今像咱們說的那樣,咱們有一個配置,OriginalComponent 組件須要這個配置的數據,代碼以下:

var config = require('path/to/configuration');

    var enhanceComponent = (Component) =>
        class Enhance extends React.Component {
            render() {
                return (
                    <Component
                        {...this.state}
                        {...this.props}
                        title={ config.appTitle }
                        />
                )
            }
        };

這個配置是隱藏在高階組件中。OriginalComponent 組件只能經過 props 來調用 title 數據。至於 title 數據從哪裏來對於 OriginalComponent 來講並不重要(這就很是棒了!封閉性作的很好)。這是極大的優點,由於它幫助咱們測試獨立組件,以及提供一個好的機制去 mocking 數據。這裏可以這樣使用 title 屬性( 也就是 stateless component[無狀態組件] )。

var OriginalComponent = (props) => <p>{ props.title }</p>;

高階組件是須要另一個有用的模式-依賴注入(dependency injection)。

4、依賴注入(Dependency injection)

源碼

大部分模塊/組件都會有依賴。可以合理的管理這些依賴可以直接影響到項目是否成功。有一個技術叫:依賴注入(dependency injection,以後我就簡稱 DI 吧)。也有部分人稱它是一種模式。這種技術可以解決依賴的問題。

在 React 中 DI 很容易實現,讓咱們跟着應用來思考:

// 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 />;
        }
    };

有一個 "React in patterns" 的字符串,這個字符串以某種方式來傳遞給 Title 組件。

最直接的方式是經過: App => Header => Title 每一層經過 props 來傳遞。然而這樣可能在這個三個組件的時候比較方便,可是若是有多個屬性以及更深的組件嵌套的狀況下將比較麻煩。大量組件將接收到它們並不須要的屬性(由於是逐層傳遞)。

咱們前面提到的高階組件的方式可以用來注入數據。讓咱們用這個技術來注入一下 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 }
                        />
                )
            }
        };

    // Header.jsx
    import enhance from './enhance.jsx';
    import Title from './Title.jsx';

    var EnhancedTitle = enhance(Title);
    export default function Header() {
        return (
            <header>
                <EnhancedTitle />
            </header>
        );
    }

這個 title 是隱藏在中間層(高階組件)中,咱們經過 prop 來傳遞給 Title 組件。這很好的解決了,可是這只是解決了一半問題,如今咱們沒有層級的方式去傳遞 title,可是這些數據都在 echance.jsx 中間層組件。

React 有一個 context 的概念,這個 context 可以在每個組件中均可以訪問它。這個優勢像 event bus 模型,只不過這裏是一個數據。這個方式讓咱們可以在任何地方訪問到數據。

// 咱們定義數據的地方:context => title
    var context = { title: 'React in patterns' };
    class App extends React.Component {
        getChildContext() {
            return context;
        }
    ...
    };
    App.childContextTypes = {
        title: React.PropTypes.string
    };

    // 咱們須要這個數據的地方
    class Inject extends React.Component {
        render() {
            var title = this.context.title;
        ...
        }
    }
    Inject.contextTypes = {
        title: React.PropTypes.string
    };

值得注意的是咱們必須使用 childContextTypes 和 contextTypes 這兩個屬性,定義這個上下文對象的類型聲明。若是沒有聲明,context 這個對象將爲空(經我測試,若是沒有這些類型定義直接報錯了,因此必定要記得加上哦)。這可能有些不太合適的地方,由於咱們可能會放大量的東西在這裏。因此說 context 定義成一個純對象不是很好的方式,可是咱們可以讓它成爲一個接口的方式來使用它,這將容許咱們去存儲和獲取數據,好比:

// 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 組件就從這個 context 中獲取數據:

// 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
    };

最好的方式是咱們在每次使用 context 的時候不想定義 contextTypes。這就是可以使用高階組件包裹一層。甚至更多的是,咱們可以寫一個單獨的函數,去更好的描述和幫助咱們聲明這個額外的地方。以後經過 this.context.get('title') 的方式直接訪問 context 數據。咱們經過高階組件獲取咱們須要的數據,而後經過 prop 的方式來傳遞給咱們的原始組件,好比:

// 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 函數有三個參數:

(1)一個 React 組件

(2)須要依賴的數據,這個數據以數組的方式定義

(3)一個 mapper 的函數,它能接受上下文的原始數據,而後返回一個咱們的 React 組件(好比 Title 組件)實際須要的數據對象(至關於一個 filter 管道的做用)。

這個例子咱們只是經過這種方式傳遞來一個 title 字符串變量。而後在實際應用開發過程當中,它多是一個數據的存儲集合,配置或者其餘東西。所以,咱們經過這種方式,咱們可以經過哪些咱們確實須要的數據,不用去污染組件,讓它們接收一些並不須要的數據。

這裏的 wire 函數定義以下:

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 對象的 dependencies 全部的配置項數組。這個 mapper 函數可以接收 context 的數據,並轉換它,而後給 props 最後傳遞到咱們的組件。

最後來看一下關於依賴注入

在不少解決方案中,都使用了依賴注入的技術,這些都基於 React 組件的 context 屬性。我認爲這很好的知道發生了什麼。在寫這篇文憑的時候,大量流行構建 React 應用的方式會須要 Redux。著名 connect 函數和 Provider 組件,就是使用的 context(如今你們能夠看一下源碼了)。

我我的發現這個技術是真的有用。它是知足了我處理全部依賴數據的須要,使個人組件變得更加純粹和更方便測試。

5、單向數據流(One-way direction data flow)

源碼

在 React 中單向數據流的模式運做的很好。它讓組件不用修改數據,只是接收它們。它們只監聽數據的改變和可能提供一些新的值,可是它們不會去改變數據存儲器裏面實際的數據。更新會放在另外地方的機制下,和組件只是提供渲染和新的值。

讓咱們來看一個簡單的 Switcher 組件的例子,這個組件包含了一個 button。咱們點擊它將可以控制切換(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 對象是單例 咱們有 helper 去設置和獲取 _flag 這個屬性的值。經過 getter,而後組件可以經過外部數據進行更新。大楷咱們的應用工做流看起來是這樣的:

User's input
     |
Switcher -------> Store

讓咱們假設咱們要經過 Store 給後端服務去保存這個 flag 值。當用戶返回的時候,咱們必須設置合適的初始狀態。若是用戶離開後在後來,咱們必須展現 "lights on" 而不是默認的 "lights off"。如今它變得困難,由於咱們的數據是在兩個地方。UI 和 Store 中都有本身的狀態,咱們必須在它們之間交流: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 組件和咱們就增長了本身 App 的複雜度。

單向數據流就解決了這個問題。它消除了這種多種狀態的狀況,只保留一個狀態,這個狀態通常是在 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 的方式,但這種方式不推薦使用。通常狀況可以使用高階組件進行從新渲染。咱們使用 forceUpdate 只是簡單的演示。

由於這個改變,Switcher 變得比以前簡單。咱們不須要內部的 state:

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 數據的一個填鴨式組件。它是真的讓 React 組件變成了純粹的渲染層。咱們寫咱們的應用是聲明的方式,而且只在一個地方處理一些複雜的數據。

這個應用的工做流就變成了:

Service communicating
with our backend
    ^
    |
    v
Store <-----
    |        |
    v        |
Switcher ---->
    ^
    |
    |
User input

咱們看到這個數據流都是一個方向流動的,而且在咱們的系統中,不須要同步兩個部分(或者更多部分)。單向數據流不止能基於 React 應用,這些就是它讓應用變得更簡單的緣由,這個模式可能還須要更多的實踐,可是它是確實值得探索的。

6、結語

固然,這不是在 React 中全部的設計模式/技術。還可能有更多的模式,你可以 checkout github.com/krasimir/react-in-patterns 進行更新。我將努力分享我新的發現。

原文出自:http://krasimirtsonev.com/blog/article/react-js-in-design-patterns

相關文章
相關標籤/搜索