使用 react
有段時間了,總感受用的不夠深刻,連最基本異步處理方案 redux-saga
也纔是前端時間剛學的。鑑於此,在 github
上搜了下相關的 react
項目,找到了一個外國人寫的一個項目,看了內部 react
以及一些庫的使用,整個 react
生態用的很不錯,不少地方我都沒有接觸過,因此對一些寫法和庫的使用上作一些記錄和總結。前端
因爲查看的那個項目是 2017年 寫的,因此有一些庫不太同樣了,這裏我結合自身的使用狀況總結下一個還算完整的 react
項目可能會用到庫:vue
庫名 | 用途 | 相似功能的庫 |
---|---|---|
react |
核心庫 | / |
react-dom |
核心庫 | / |
prop-types |
props校驗庫 | / |
react-router-dom |
路由庫 | reach/router |
redux |
狀態管理庫 | Mobx 、rematch |
react-redux |
鏈接 react 、redux |
/ |
redux-saga |
redux 中間件,解決異步問題 |
redux-thunk 、redux-promise |
redux-devtools-extension |
chrome 的 redux 調試工具 |
/ |
reselect |
store 上取值可以緩存 |
/ |
immutable |
不可變數據 | / |
因而可知,react
全家桶一次性學習下來,仍是有必定的門檻的,接下來彙總下基本使用套路。react
老實說,react
是一個學習、使用至關平滑的庫,因此簡單的使用仍是比較容易的,主要學習的難點仍是在 redux
以及像 immutable
這樣的不多用的庫。以前,我是沒有用過immutable
和 reselect
,這裏就對着別人項目記錄下。git
redux
自己是一個很純粹的狀態管理庫,和 react
自己沒有任何瓜葛,可是用 react-redux
能夠把 react
和 redux
結合起來。具體細節 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
是我想要渲染的組件,reducer
、rootSaga
是我業務相關的內容,而其餘內容能夠發現,基本都是固定的,下一個項目基本能夠照搬過來。redux
這裏先看個人很通常的 reducer
寫法,再看一下別人結合 immutable
的 reducer
。api
// 個人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; } }
相比個人直接對象,這裏用了 immutable
的 Record
,在 reducer
內部須要修改 state
的時候,直接調用 set
方法就去修改了,在層級很深的對象的時候是很是方便的。
當初有點恐懼學習 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
,一塊是處理,watch
用while
死循環 take
或者takeEvery
、takeLatest
去 watch
對應的 action/type
, 而後調用另外一個 sagas
,在另外一個 sagas
用 call
之類的去調用異步的 api/service
。
扯了半天 redux
,看最後是怎麼把 redux
上的store數據關聯到視圖層上,以及視圖如何去改變store裏面的值,主要仍是 react-redux
用 connect
把 store
的數據以及 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
中傳兩個參數 mapStateToProps
、mapDispatchToProps
過去就完事了,這樣組件須要什麼值,須要什麼方法都能提供。
順帶一提,用上 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
。
上面介紹了那麼多 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
常見用法,以便往後忘記了能夠翻閱。