react使用指南

使用 react 有段時間了,總感覺用的不夠深入,連最基本異步處理方案 redux-saga 也纔是前端時間剛學的。鑑於此,在 github 上搜了下相關的 react 項目,找到了一個外國人寫的一個項目,看了內部 react 以及一些庫的使用,整個 react 生態用的很不錯,很多地方我都沒有接觸過,所以對一些寫法和庫的使用上做一些記錄和總結。

相關類庫

由於查看的那個項目是 2017年 寫的,所以有一些庫不太一樣了,這裏我結合自身的使用情況總結下一個還算完整的 react 項目可能會用到庫:

庫名 用途 類似功能的庫
react 核心庫 /
react-dom 核心庫 /
prop-types props校驗庫 /
react-router-dom 路由庫 reach/router
redux 狀態管理庫 Mobxrematch
react-redux 連接 reactredux /
redux-saga redux 中間件,解決異步問題 redux-thunkredux-promise
redux-devtools-extension chromeredux 調試工具 /
reselect store 上取值能夠緩存 /
immutable 不可變數據 /

由此可見,react 全家桶一次性學習下來,還是有一定的門檻的,接下來彙總下基本使用套路。

使用套路

老實說,react 是一個學習、使用相當平滑的庫,所以簡單的使用還是比較容易的,主要學習的難點還是在 redux 以及像 immutable 這樣的很少用的庫。之前,我是沒有用過immutablereselect ,這裏就對着別人項目記錄下。

redux初始化

redux 本身是一個很純粹的狀態管理庫,和 react 本身沒有任何瓜葛,但是用 react-redux 可以把 reactredux 結合起來。具體細節 api 不談,直接記錄平時如何使用:

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import {Provider} from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import {composeWithDevTools} from 'redux-devtools-extension'
import reducer from './store/reducers'
import rootSaga from './store/sagas/index'
import { AppWithRouter } from './router/router'
const sagaMiddleware = createSagaMiddleware()
const composeEnhancers = composeWithDevTools({})

const store = createStore(
  reducer,
  composeEnhancers(
      applyMiddleware(sagaMiddleware)
  ),
)

sagaMiddleware.run(rootSaga)

ReactDOM.render(
    <Provider store={store}>
      <AppWithRouter />
    </Provider>,
    document.getElementById('root')
  )

這裏根據 reducer 生成了 store,把 store 掛載到 Provider 上面去了,後面的子組件就會根據 context 去拿到 store 上的值。

這裏的 AppWithRouter 是我想要渲染的組件,reducerrootSaga 是我業務相關的內容,而其他內容可以發現,基本都是固定的,下一個項目基本可以照搬過來。

reducer寫法

這裏先看我的很一般的 reducer 寫法,再看一下別人結合 immutablereducer

// 我的reducer寫法
import {actionTypes} from '../action-type'
export function pageList(state = {
    list: [],
    isLoading: false
}, action) {
    switch (action.type) {
        case actionTypes.FETCH_LIST_SUCCESS:
            return {
                ...state,
                list: action.payload
            }
        case actionTypes.LIST_LOADING:
            return {
                ...state,
                isLoading: action.payload
            }
        default:
            return state
    }
}
// action-type.js
export const actionTypes = {
    // 詳情頁面
    FETCH_DETAIL: 'FETCH_DETAIL',
    FETCH_DETAIL_SUCCESS: 'FETCH_DETAIL_SUCCESS',
    DETAIL_LOADING: 'DETAIL_LOADING',

    // 列表頁面
    FETCH_LIST_SUCCESS: 'FETCH_LIST_SUCCESS',
    LIST_LOADING: 'LIST_LOADING',
    FETCH_LIST: 'FETCH_LIST',

    // tab
    CHANGE_TAB: 'CHANGE_TAB',

    // currentPage
    CHANGE_PAGE: 'CHANGE_PAGE'
}

只要注意把固定的字符串全部寫成變量。

由於 redux 需要保持純函數的特點,所以 redux 是不能直接修改 state 的值,應該返回一個全新的 state ,所以如果 state 的嵌套層數很深的話,要返回全新的 state 就比較麻煩了,所以這裏就引申出來 immutable,同樣在組件 shouldComponentUpdate 時需要對比兩個對象時,immutable 也能幫上很大的忙。

看看別人 reducer 的用法:

import { Record } from 'immutable';
import { searchActions } from './actions';


export const SearchState = new Record({
  currentQuery: null,
  open: false
});


export function searchReducer(state = new SearchState(), {payload, type}) {
  switch (type) {
    case searchActions.LOAD_SEARCH_RESULTS:
      return state.merge({
        open: false,
        currentQuery: payload.query
      });

    case searchActions.TOGGLE_SEARCH_FIELD:
      return state.set('open', !state.open);

    default:
      return state;
  }
}

相比我的直接對象,這裏用了 immutableRecord,在 reducer 內部需要修改 state 的時候,直接調用 set 方法就去修改了,在層級很深的對象的時候是非常方便的。

saga的寫法

當初有點恐懼學習 redux-saga,實際去學習和使用的時候發現還是很不錯的,相比redux-thunk 去強行讓 action 能夠是個函數,redux-saga 還是保持 action 是一個對象,髒活累活全丟給 saga 去做,redux 的那一塊邏輯依然保持之前一樣純淨。先上例子:

import { call, fork, select, take, takeLatest } from 'redux-saga/effects';
import { fetchSearchResults } from 'src/core/api';
import history from 'src/core/history';
import { getTracklistById } from 'src/core/tracklists';
import { searchActions } from './actions';


export function* loadSearchResults({payload}) {
  const { query, tracklistId } = payload;
  const tracklist = yield select(getTracklistById, tracklistId);
  if (tracklist && tracklist.isNew) {
    yield call(fetchSearchResults, tracklistId, query);
  }
}


//=====================================
//  WATCHERS
//-------------------------------------

export function* watchLoadSearchResults() {
  yield takeLatest(searchActions.LOAD_SEARCH_RESULTS, loadSearchResults);
}

export function* watchNavigateToSearch() {
  while (true) {
    const { payload } = yield take(searchActions.NAVIGATE_TO_SEARCH);
    yield history.push(payload);
  }
}


//=====================================
//  ROOT
//-------------------------------------

export const searchSagas = [
  fork(watchLoadSearchResults),
  fork(watchNavigateToSearch)
];

這玩意按我目前的理解,saga分兩塊,一塊專門用來 watch,一塊是處理,watchwhile 死循環 take 或者takeEverytakeLatestwatch 對應的 action/type, 然後調用另一個 sagas ,在另一個 sagascall 之類的去調用異步的 api/service

store和視圖

扯了半天 redux,看最後是怎麼把 redux 上的store數據關聯到視圖層上,以及視圖如何去改變store裏面的值,主要還是 react-reduxconnectstore 的數據以及 dispatch 給組件,這樣組件就能獲取數據以及修改數據了。

先看我的常規做法:

import React from 'react'
import {compose} from 'redux'
import {withRouter} from 'react-router-dom'
import {connect} from 'react-redux'
import {actionTypes} from '../store/action-type'

class DetailPage extends React.Component {
    componentDidMount () {
        const {fetchDetail} = this.props
        fetchDetail()
    }
    render () {
        const {detail, isLoading} = this.props
        return (
           xxx
        )
    }
}

const mapStateToProps = state => {
    return {
        detail: state.detailData.data,
        isLoading: state.detailData.isLoading
    }
}

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        fetchDetail() {
            dispatch({
                type: actionTypes.FETCH_DETAIL,
                payload: ownProps.match.params.id
            })
        }
    }
}

export default compose(
    withRouter,
    connect(mapStateToProps, mapDispatchToProps),
)(DetailPage)

還是很簡單的,直接在 connect 中傳兩個參數 mapStateToPropsmapDispatchToProps 過去就完事了,這樣組件需要什麼值,需要什麼方法都能提供。

順帶一提,用上 withRouter,這樣路由信息也能給到組件。

看下用了 reselect 之後,是怎麼用的:

import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import classNames from 'classnames';
import { List } from 'immutable';
import PropTypes from 'prop-types';
import { getBrowserMedia, infiniteScroll } from 'src/core/browser';
import { audio, getPlayerIsPlaying, getPlayerTrackId, playerActions } from 'src/core/player';
import { getCurrentTracklist, getTracksForCurrentTracklist, tracklistActions } from 'src/core/tracklists';

export class Tracklist extends React.Component {
  static propTypes = {
    compactLayout: PropTypes.bool,
    displayLoadingIndicator: PropTypes.bool.isRequired,
    isMediaLarge: PropTypes.bool.isRequired,
    isPlaying: PropTypes.bool.isRequired,
    loadNextTracks: PropTypes.func.isRequired,
    pause: PropTypes.func.isRequired,
    pauseInfiniteScroll: PropTypes.bool.isRequired,
    play: PropTypes.func.isRequired,
    selectTrack: PropTypes.func.isRequired,
    selectedTrackId: PropTypes.number,
    tracklistId: PropTypes.string.isRequired,
    tracks: PropTypes.instanceOf(List).isRequired
  };

  componentDidMount() {
    infiniteScroll.start(
      this.props.loadNextTracks,
      this.props.pauseInfiniteScroll
    );
  }

  componentWillUpdate(nextProps) {
    if (nextProps.pauseInfiniteScroll !== this.props.pauseInfiniteScroll) {
      if (nextProps.pauseInfiniteScroll) {
        infiniteScroll.pause();
      }
      else {
        infiniteScroll.resume();
      }
    }
  }

  componentWillUnmount() {
    infiniteScroll.end();
  }

  render() {
    const { compactLayout, isMediaLarge, isPlaying, pause, play, selectedTrackId, selectTrack, tracklistId, tracks } = this.props;

    return (
      xxxx
    );
  }
}


//=====================================
//  CONNECT
//-------------------------------------

const mapStateToProps = createSelector(
  getBrowserMedia,
  getPlayerIsPlaying,
  getPlayerTrackId,
  getCurrentTracklist,
  getTracksForCurrentTracklist,
  (media, isPlaying, playerTrackId, tracklist, tracks) => ({
    displayLoadingIndicator: tracklist.isPending || tracklist.hasNextPage,
    isMediaLarge: !!media.large,
    isPlaying,
    pause: audio.pause,
    pauseInfiniteScroll: tracklist.isPending || !tracklist.hasNextPage,
    play: audio.play,
    selectedTrackId: playerTrackId,
    tracklistId: tracklist.id,
    tracks
  })
);

const mapDispatchToProps = {
  loadNextTracks: tracklistActions.loadNextTracks,
  selectTrack: playerActions.playSelectedTrack
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Tracklist);
// selector.js
export function getBrowserMedia(state) {
  return state.browser.media;
}

具體關注下用了 reselect 之後,mapStateToProps 和我之前的寫法發生了變化,正如給的例子那樣用 createSelector 包了一層,同時傳入兩個參數進去,第一個參數是個從 state 上取值的函數,就像上面的 getBrowserMedia 這個例子一樣。至於 mapDispatchToProps 的寫法,在我的用法是寫一個接受 dispatch 的函數同時返回一個對象,當然也可以像上面一樣傳入一個對象,這個對象 redux 就默認做爲 action

props 驗證

上面介紹了那麼多 redux 相關寫法,redux 確實算是 react 學習上的一個難點,現在講點輕鬆點的。redux 推崇容器組件展示組件,實際上在寫 react 應用的時候,你也可能不太會注意到,其實用 connect 這個高階函數包裝過的組件就是所謂的容器組件,而傳給 connect 的組件,其實就是我們寫的展示組件,寫的多了就會發現哈,我們越來越少地用到了組件內部的 state 去控制組件,反而大部分情況都是直接用 props 去控制組件,這也得益於 redux 能夠提供類似全局變量 store 的取值和改變值的方式。所以說回來,對於一個 react 組件而言,state 對應內部狀態,props 對應外部傳入值,props 由於 redux 等狀態管理庫盛行,使用頻率也大幅增加,所以我們需要嚴格要求好外部傳入的 props的類型要符合組件規定的。prop-types 就是解決這個問題的,當然你也可以不去校驗 props 的類型。

import React from 'react'
export default class Test extends React.Component {
    static propTypes = {
        compactLayout: PropTypes.bool,
        displayLoadingIndicator: PropTypes.bool.isRequired,
        isMediaLarge: PropTypes.bool.isRequired,
        isPlaying: PropTypes.bool.isRequired,
        loadNextTracks: PropTypes.func.isRequired,
        pause: PropTypes.func.isRequired,
        pauseInfiniteScroll: PropTypes.bool.isRequired,
        play: PropTypes.func.isRequired,
        selectTrack: PropTypes.func.isRequired,
        selectedTrackId: PropTypes.number,
        tracklistId: PropTypes.string.isRequired,
        tracks: PropTypes.instanceOf(List).isRequired
    };
    static defaultProps = {
        compactLayout: true
    }
    render () {
        return xxx
    }
}

總結

react 本身不難,甚至我覺得比起 vue 而言更爲簡單,使用難點主要還是在於一些第三方庫的搭配使用,所以本文也是基於這個點,記錄下一些 react 常見用法,以便日後忘記了可以翻閱。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章