react 設計模式與最佳實踐

黃大發丶發表於2019-01-19

本文是閱讀米凱萊·貝爾託利 《React設計模式與最佳實踐》 一書的讀書筆記,支援作者請點這裡購買。


Take is cheap, just show me the code.

廢話不少說,直接上乾貨的哈。

關於 render 函式裡面的條件判斷

在 React 裡,有一種情況是,我們經常需要根據條件判斷決定是否渲染某些元件。就像是這樣:

<div>
    { isLoggedIn ? <LogoutButton /> : <LoginButton /> }
    { visible && <Modal /> }
</div>
複製程式碼

當條件判斷變得更復雜的請求下,我們可以使用方法和計算屬性來取代三目運算和與或判斷。

handleShowLoginButton() {
    return this.isLoggedIn && this.isAuthed;
}
get getVisible() {
    return this.visible && this.displayMode === "normal"
}

render() {
    return (<div>
        { handleShowLoginButton() ? <LogoutButton /> : <LoginButton /> }
        { getVisible && <Modal /> }
    </div>)
}
複製程式碼

然後黑科技來了,當我們想要把這些判斷邏輯從 render 渲染函式裡抽離出來以讓渲染函式只負責渲染的時候。我們就需要用到 render-if render-only-if jsx-control-statements 這些輔助依賴了。 客官請看:

const isShowLoginButton = renderIf(
    this.isLoggedIn && this.isAuthed
)
return (<div>
    { isShowLoginButton(<LoginButton />) } {/* 完了結果 LogoutButton 我還需要另外寫一個 isShowLogoutButton 的 renderIf 去判斷顯示與否嗎 */}
</div>)
複製程式碼

然後 render-only-if 本質上是一個高階函式,它形式上會比 render-if 優雅些。

const LoginButtonOnlyIf = onlyIf(
    ({ isLoggedIn && isAuthed }) => {
        return isLoggedIn && isAuthed
    }
)(LoginButton)

return (
    <LoginButtonOnlyIf 
        isLoggedIn={isLoggedIn}
        isAuthed={isAuthed}
    />
)
複製程式碼

總結:

  • 如果只是簡單的條件判斷,三目和與或運算子已經滿足大多數人的需求;
  • 如果想讓關注點分離,renderIf 是個不錯的注意;
  • 最後如果你希望你的判斷邏輯能夠被複用(就好像多個頁面多個元件都需要用到判斷登入狀態和使用者許可權的邏輯),可以使用 onlyIf 構建專案內可複用的高階元件。

然後我們最後看看 jsx-control-statements 這個噁心東西的用法:

<If condition={this.handleShowLoginButton}>
    <LoginButton />
</If>

<When condition={this.handleShowLoginButton}>
    <LoginButton /> 
</When>
<When condition={!this.handleShowLoginButton}>
    <LogoutButton /> // => 好了這下終於看見我們的 LogoutButton 出現了
</When>
<Otherwise>
    <p>oops.. no condition matched.</p>
</Otherwise>


<ul>
    <For each="resultItem" of={this.resultList}>
        <li>{resultItem.name}</li>
    </For> 
    // => {resultList.map(resultItem => <li>{resultItem.name}</li>)}
</ul>
複製程式碼

開發可複用元件

這是關於效能和可維護性的課題吶。

始終牢記,設定狀態會觸發元件重新渲染。因此,應該只將渲染方法要用到的值儲存在狀態中。

以下是 Dan Abramov (我並不知道他是誰) 建立的幫助我們做出正確狀態選擇的步驟:

function shouldIKeepSomethingInReactState() {
    if (canICalculateItFromProps()) {
        // 不要把 props 屬性直接在 state 狀態中使用,
        // 應該直接在 render() 函式裡計算使用它們
        return false
    }
    if (!amIUsingItInRenderMethod()) {
        // 不要把沒有參與渲染的資料放進 state 狀態裡,
        // 換句話說就是隻有需要涉及到元件 render 渲染更新的資料才放到 state 裡
        return false
    }
    // 除了以上情況,都可以使用狀態。
    return true;
}
複製程式碼

關於 prop 型別檢驗,React 提供了元件的 propTypes 屬性給我們使用:

const Button = ({text}) => <button>{text}</button>

Button.propTypes = {
    text: React.PropTypes.string
}
複製程式碼

但其實在 TypeScript 的世界裡,我們直接可以使用模板類的形式給我們 React 元件宣告 prop 屬性介面:

interface IButtonProps = {
    text: string;
}

class ButtonClass extend React.Component<IButtonProps, IButtonStates> {}
// => 順帶連 state 屬性檢驗也可以加進來
複製程式碼

接下來為元件自動生成文件,使用 react-docgen 這個工具。

import React from 'react';

/**
 * Sheet 元件
 */
const Sheet = ({title}) => <div>{title}</div>

Sheet.prototype = {
    /**
     * Sheet 標題
     */
    title: React.PropTypes.string
}
複製程式碼

執行 react-docgen Sheet.js 後結果產出如下 json 描述:

{
    "description": "Sheet 元件",
    "displayName": "Sheet",
    "methods": [],
    "props": {
        "title": {
            "type": {
                "name": "string"
            },
            "required": false,
            "description": "Sheet 標題"
        }
    }
}
複製程式碼

把這個 json 檔案作為團隊前端文件專案的輸入,就可以自動化地生成可用的元件文件說明啦啦啦。

好了,接下來祭出業內大殺器 storybook

storybook

npm i --save @kadira/react-storybook-addon
複製程式碼

(貌似 @kadira/react-storybook-addon 已經報廢了,建議小夥伴還是在官網按照文件寫自己的 storybook 吧)

故事文件放在 stories 的資料夾中,我們在 stories 資料夾下建立 sheet.js 定義我們上面定義元件的故事文件。

// => stories/sheet.js
import React from 'react';
import Sheet from '../src/components/Sheet';
import { storiesOf } from '@kadira/storybook'

storiesOf('Sheet', module)
    .add('沒有 title 屬性的 Sheet 的故事..', () => (
        <Sheet/>
    ))
複製程式碼

但是我們要寫故事還得在根目錄下先來配置好 storybook :

// => .storybook/config.js => 根目錄下建立 .storybook 資料夾
import { configure } from '@kadira/storybook';

function loadStories() {
    require('../src/stories/sheet')
}

configure(loadStories, module)
複製程式碼

最後我們給我們的 package.json 加上 script 來執行我們的故事。

    "storybook": "start-storybook -p 9001"
複製程式碼

執行之後在 9001 埠就可以看到故事文件啦啦啦。

react-official storybook


我們再來深入地擼一下元件這個東西

關於容器元件和傻瓜元件,我在這裡就不說了哈。畢竟是初級內容,不好濫竽充數。我們直接直奔主題。

比如,最簡單的,實現一個給元件加上類名的高階元件:

const withClassName = Component => props => (
    <Component {...props} className="my-class" />
)
複製程式碼

上面只是動了一個 prop 而已哈,實際上除了 prop 其他的一切元件屬性我們都可以動哈。

const withTimer = Component => (
    class extends React.Component {
        constructor(props) {
            super(props)
            this.state = {
                timer: null
            }
        }
        componentDidMount() {
            this.timer = setTimeInterval(() => {
                console.log('每個1.5s列印一次日誌哈哈哈')
            }, 1500)
        }
        componentWillUnmount() {
            clearInterval(this.timer)
        }
        render() {
            // => 原封不動把接收到的 props 傳給 Component
            //    state 傳入 Compnent 其實是可選項,根據實際需求決定
            return <Component {...this.props} {...this.state} />
        }
    }
)

// => 然後我們就可以給普通的元件加上定時列印日誌的功能啦
const SheetWithTimer = withTimer(Sheet);
複製程式碼

然後 recompose 這個庫已經幫我們提供了一些很實用場景的高階元件,開箱即用哈。

recompose

接下來我們來搞點噱頭,看看函式子元件怎麼玩。首先,我們上面那個 withClassName 顯然太 low 了,居然 className 是寫死的!?凡事都不要寫死,需求以後分分鐘給你改。

還改不改需求

顯然,我們需要在 withClassName 元件裡面再做多一層邏輯,判斷好後再動態傳 className 給子元件。這個時候我們為了搞噱頭,決定採用函式子元件的模式。

const withClassName = ({children}) => children('my-class'); // => wft,這裡 'my-class' 還不照樣是寫死的...

<withClassName>
    {(classname) => <Component className={classname} />}
</withClassName>
複製程式碼

然後,我們就看到了無限可能... 雖然 withClassName 現在還是個無狀態元件哈,但是我們完全可以像 withTimer 元件那樣給它加上生命鉤子和函式方法還有狀態。然後在 render 裡不同的是(我們不直接使用 <Component /> 而是執行 children()):


render() {
    return <Component {...props} /> // => 一般做法

    renturn {children(props)} // => 函式子元件做法
}
複製程式碼

或許更貼切的例子是高階元件需要做 http 請求的場景吧,把請求回來的資料再傳入子元件進行渲染。

<Fetch url="...">
    {data => <MyComp data={data} />}
</Fetch>
複製程式碼

最後我們來談談 CSS

首先,簡單粗暴的我們可以在 html 元素裡直接寫 style,在 jsx 的世界裡是長這樣的:

<div style={{ fonSize: this.state.fontSize }} />
複製程式碼

然後 style 行內樣式有個缺點,就是你不能直接寫媒體查詢偽類偽元素,當然動畫(插播小廣告:動畫 react 庫請使用 react-motion)你也沒法寫。

所以 Radium 應運而生。

有了 Radium ,你可以任性地這樣操作:

import radium from 'radium'

const myStyle = {
    fontSize: '12px',
    ':hover': {
        color: '#abc'
    },
    '@media (min-width: 720px)': {
        color: '#121212'
    }
}

const Div = () => <div style={myStyle} />

export default radium(Div);
複製程式碼

當然使用媒體查詢你還需要在最外層保一個 styleroot 元素,要不然 Radium 哪知道你媒體查詢根元素在哪裡喲。

import { StyleRoot } from 'radium'

class App extends Component {
    render() {
        return (
            <StyleRoot>
                <router-view />
            </StyleRoot>
        )
    }
}
複製程式碼

當然我們完全可以不使用行內樣式,而是使用基於 className 的 css 模組。

import styles from './index.less'

render() {
    return {
        <div className={styles.myDiv} />
    }
}
複製程式碼
/* index.less */
.myDiv {
    font-size: 12px
}

/* 預設模組會生成一堆我們看不懂的英文類名,如果想要讓類名不作用在區域性而是全域性,可以使用 :global */
:global .myGlobalDiv {
    font-size: 15px
}

/* 我們還可以使用 composes 把其他類的樣式混進來 */
.myDivCop {
    composes: .myDiv;
    color: '#101010'
}
複製程式碼

再有,如果你的 className 不想寫 style.[類名] 的形式而是想直接寫字串類名的形式,你可以借用 react-css-modules 這個庫。

然後這種姿勢使用:

import cssModules from 'react-css-modules'
import styles from './index.less'

class DivComp extends Component {
    render() {
        return (
            <div className='myDiv' />
        )
    }
}

export cssModules(DivComp, styles)
複製程式碼

然後還有個可能是以後趨勢的叫 styled-components 的傢伙,因為樓主實在是學不動了所以這裡就不展開講了哈。


掃碼打賞告訴我你下期想要閱讀的內容主題(萌賤臉)

開玩笑的哈,歡迎評論區留言告訴我你想要閱讀的內容主題,我會只選我會的,不會的都不選哈哈哈 我會盡量抽出時間來擼 demo 和進行延伸閱讀的。也歡迎大家關注督促我下期更文。

黃大發的讚賞碼

相關文章