dva 是基於 redux 最佳實踐 實現的 framework,簡化使用 redux 和 redux-saga 時很多繁雜的操作
數據流向
數據的改變發生通常是通過用戶交互行爲或者瀏覽器行爲(如路由跳轉等)觸發的,當此類行爲會改變數據的時候可以通過 dispatch 發起一個 action,如果是同步行爲會直接通過 Reducers 改變 State ,如果是異步行爲(副作用)會先觸發 Effects 然後流向 Reducers 最終改變 State,所以在 dva 中,數據流向非常清晰簡明,並且思路基本跟開源社區保持一致
Modul
Subscription
Subscriptions 是一種從 源 獲取數據的方法,它來自於 elm。
Subscription 語義是訂閱,用於訂閱一個數據源,然後根據條件 dispatch 需要的 action。數據源可以是當前的時間、服務器的 websocket 連接、keyboard 輸入、geolocation 變化、history 路由變化等等。
subscriptions: {
setup({ dispatch, history }) {
history.listen(async ({ pathname }, action) => {
const re =
pathToRegexp('/group-member/list/:groupId').exec(pathname)
||
pathToRegexp('/group-member/del/:groupId').exec(pathname)
if (action !== 'POP' && re && re[1]) {
const groupId = +re[1]
dispatch({ type: 'initList' })
dispatch({ type: 'fetchGroupMemberList', groupId })
}
})
},
},
Effect
Effect 被稱爲副作用,在我們的應用中,最常見的就是異步操作。它來自於函數編程的概念,之所以叫副作用是因爲它使得我們的函數變得不純,同樣的輸入不一定獲得同樣的輸出。
dva 爲了控制副作用的操作,底層引入了redux-sagas做異步流程控制,由於採用了generator的相關概念,所以將異步轉成同步寫法,從而將effects轉爲純函數。
effects: {
* fetchGroupMemberList({ groupId }, { call, put }) {
try {
const { succeed, data: { role, member: { list: briefs } } } =
yield call(fetch.get, `${GROUP_MEMBER_URL}/${groupId}/1/${GROUP_MEMBER_PAGE_SIZE}`)
if (succeed) {
yield put({ type: 'nextList', briefs, page: 1 })
yield put({ type: 'setIdAndRole', role })
}
} catch (err) {
console.log('Error when fetch group member list', err.stack)
yield put({ type: 'app/showToast', title: '獲取羣組成員列表錯誤' })
}
},
...
},
Reducer
在 dva 中,reducers 聚合積累的結果是當前 model 的 state 對象。通過 actions 中傳入的值,與當前 reducers 中的值進行運算獲得新的值(也就是新的 state)。需要注意的是 Reducer 必須是純函數,所以同樣的輸入必然得到同樣的輸出,它們不應該產生任何副作用。並且,每一次的計算都應該使用immutable data,這種特性簡單理解就是每次操作都是返回一個全新的數據(獨立,純淨),所以熱重載和時間旅行這些功能才能夠使用。
reducers: {
initList(state) {
console.log('initLists')
return {
...state,
list: [],
}
},
...
},
State
State 表示 Model 的狀態數據,通常表現爲一個 javascript 對象(當然它可以是任何值);操作的時候每次都要當作不可變數據(immutable data)來對待,保證每次都是全新對象,沒有引用關係,這樣才能保證 State 的獨立性,便於測試和追蹤變化
state: {
id: 0,
title: '全部成員',
list: [],
briefs: {},
itemCount: 1,
isManager: false,
},
Action
Action 是一個普通 javascript 對象,它是改變 State 的唯一途徑。無論是從 UI 事件、網絡回調,還是 WebSocket 等數據源所獲得的數據,最終都會通過 dispatch 函數調用一個 action,從而改變對應的數據。action 必須帶有 type 屬性指明具體的行爲,其它字段可以自定義,如果要發起一個 action 需要使用 dispatch 函數;需要注意的是 dispatch 是在組件 connect Models以後,通過 props 傳入的
dispatch({ type: ‘initList’ })
dispatching function 是一個用於觸發 action 的函數,action 是改變 State 的唯一途徑,但是它只描述了一個行爲,而 dipatch 可以看作是觸發這個行爲的方式,而 Reducer 則是描述如何改變數據的。
在 dva 中,connect Model 的組件通過 props 可以訪問到 dispatch,可以調用 Model 中的 Reducer 或者 Effects
Route Components
在 dva 中我們通常將其約束爲 Route Components,因爲在 dva 中我們通常以頁面維度來設計 Container Components。
所以在 dva 中,通常需要 connect Model的組件都是 Route Components,組織在/routes/目錄下,而/components/目錄下則是純組件(Presentational Components)
class GroupMember extends PureComponent {
static propTypes = {
groupId: PropTypes.number.isRequired,
title: PropTypes.string,
list: PropTypes.arrayOf(PropTypes.number),
fetchNext: PropTypes.func,
briefs: PropTypes.instanceOf(Object),
removeMember: PropTypes.func,
showConfirm: PropTypes.func,
url: PropTypes.string.isRequired,
}
static defaultProps = {
title: '',
list: [],
fetchNext: () => {},
briefs: {},
isManager: false,
removeMember: () => {},
showConfirm: () => {},
}
submitRemoveMember = id => () => {
const { removeMember, groupId } = this.props
removeMember(groupId, id)
}
...
render() {
const { title, list, fetchNext, groupId } = this.props
return (
<Container>
<Navigator
title={title}
/>
<MemberListView
ref={(listView) => { this.listView = listView }}
dataSource={ds.cloneWithRows(list)}
renderRow={this.renderMember}
enableEmptySections
onEndReached={() => fetchNext(groupId)}
onEndReachedThreshold={100}
/>
</Container>
)
}
}
connect
通過connect將modul中的元素作爲props的方式傳遞給component
export default connect(({ groupMember, app }, { location }) => {
const url = location.pathname
const groupId = +location.params.groupId
const userId = app.userInfo.userId
let list = [...groupMember.list]
let briefs = { ...groupMember.briefs }
if (url.indexOf('del') !== -1) {
list = list.filter(each => (each !== userId))
briefs = R.dissoc(userId, briefs)
}
return { ...groupMember, groupId, url, list, briefs }
}, { ...actions, ...appActions })(GroupMember)