使用 react
有段時間了,總感覺用的不夠深入,連最基本異步處理方案 redux-saga
也纔是前端時間剛學的。鑑於此,在 github
上搜了下相關的 react
項目,找到了一個外國人寫的一個項目,看了內部 react
以及一些庫的使用,整個 react
生態用的很不錯,很多地方我都沒有接觸過,所以對一些寫法和庫的使用上做一些記錄和總結。
相關類庫
由於查看的那個項目是 2017年 寫的,所以有一些庫不太一樣了,這裏我結合自身的使用情況總結下一個還算完整的 react
項目可能會用到庫:
庫名 | 用途 | 類似功能的庫 |
---|---|---|
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
是一個學習、使用相當平滑的庫,所以簡單的使用還是比較容易的,主要學習的難點還是在 redux
以及像 immutable
這樣的很少用的庫。之前,我是沒有用過immutable
和 reselect
,這裏就對着別人項目記錄下。
redux初始化
redux
本身是一個很純粹的狀態管理庫,和 react
本身沒有任何瓜葛,但是用 react-redux
可以把 react
和 redux
結合起來。具體細節 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
是我想要渲染的組件,reducer
、rootSaga
是我業務相關的內容,而其他內容可以發現,基本都是固定的,下一個項目基本可以照搬過來。
reducer寫法
這裏先看我的很一般的 reducer
寫法,再看一下別人結合 immutable
的 reducer
。
// 我的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;
}
}
相比我的直接對象,這裏用了 immutable
的 Record
,在 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
,一塊是處理,watch
用while
死循環 take
或者takeEvery
、takeLatest
去 watch
對應的 action/type
, 然後調用另一個 sagas
,在另一個 sagas
用 call
之類的去調用異步的 api/service
。
store和視圖
扯了半天 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
。
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
常見用法,以便日後忘記了可以翻閱。