react使用指南

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

相關類庫

因爲查看的那個項目是 2017年 寫的,因此有一些庫不太同樣了,這裏我結合自身的使用狀況總結下一個還算完整的 react 項目可能會用到庫:vue

庫名 用途 相似功能的庫
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

使用套路

老實說,react 是一個學習、使用至關平滑的庫,因此簡單的使用仍是比較容易的,主要學習的難點仍是在 redux 以及像 immutable 這樣的不多用的庫。以前,我是沒有用過immutablereselect ,這裏就對着別人項目記錄下。git

redux初始化

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

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 上的值。chrome

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

reducer寫法

這裏先看個人很通常的 reducer 寫法,再看一下別人結合 immutablereducerapi

// 個人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'
}

只要注意把固定的字符串所有寫成變量。promise

因爲 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 常見用法,以便往後忘記了能夠翻閱。

相關文章
相關標籤/搜索