dva 中的响应编程
思維盲區
我最開始學習使用 dva 是從《12 步 30 分鐘,完成用戶管理的 CURD 應用》開始的,這同時也是 dva 的官方教程。然而因為領悟能力太差,最開始完全沒理解。前 4 步還跟得上,第 5 步創建 model 和改造 service 就懵逼了。硬著頭皮照抄代碼,抄到最后數據沒出來,我還不知道自己哪兒錯了。
大寫的尷尬。
現在再看這篇教程,發現從第 5 步的 model 開始,dva 的作者就試圖推廣一種最近流行的理念:響應型編程。
就目前來說,渲染數據流有至少兩種方式。由外界改變組件內部狀態的主動型,以及由組件監聽外界改變而渲染自身的響應型。很多人 —— 尤其是 oop 重度患者 —— 的慣性思維是第一種。無論把負責業務的 service 類扔的多遠,service 和 controller 都是直接連接的。
以請求數據為例,我還停留在拿取數據推送到組件中進行渲染的階段。有那么一段時間,我對 container 和 component 之間的值交換還局限在屬性傳值和回調函數層面上。雖然回調函數實現了一定程度上的事件響應,但組件之間仍舊脫離不開互相直連的主動型編碼的怪圈。
吃火鍋的正確姿勢
舉一個吃火鍋的例子來解釋主動型編程和響應型編程的差別:
一般情況下,吃火鍋的時候都是點了菜和肉擺在自己臉跟前,想吃什么自己夾了往鍋里扔,看看快熟了就撈出來吃掉。
這是主動型。
但是這么吃火鍋有三個問題:
第一,一切都要親力親為。想吃肉就要親自把肉放到鍋里去,想吃菜也要親自把菜放進去。如果還想吃豆腐、蘑菇、粉條、羊尾魚丸…想吃的東西越多,操作就越復雜。
第二,既然是親自放東西,就得把東西擺在自己面前。桌子一共就那么點兒地方,想吃的東西越多占用的空間就越大。既不容易留出足夠的空間吃燙熟的茼蒿,也不容易把想吃的牛肉片從眼前一大堆的蔬菜里挑出來。萬一要換桌,還得的把這一大堆吃的一起打包帶走,漏掉一樣就吃不到。
第三,如果我想吃撒尿牛丸和蝦滑、魷魚,旁邊的哥們海鮮過敏。是應該我負責往鍋里放然后燙熟撈出來,他從我這里撈他能吃的;還是我倆各放各的,自己撈自己想吃的東西?前者雖然一個人做了共同的事情,但是別人一起吃的時候難免會撈錯;后者雖然看起來互不干擾,但是兩個人都在燙牛丸,多少是浪費。萬一是個鴛鴦鍋我還不吃辣怎么辦?燙到最后全亂套了。
用編程的術語說,便是:低內聚,高耦合。
或許正統的 oop 語言(比如 Java)可以用封裝、繼承、多態來某種程度的緩解這個問題(僅僅是某種程度上),但是 JavaScript 想從語言的角度實現就會無比操蛋(JavaScript 用 prototype 模擬 oop 實現,而 es6 里的 class 和 Java 里的 class 又完全不是一個東西)。
現在我們換種方式吃火鍋:分出一個人來啥也不吃,把所有吃的都放在他面前。想吃蘑菇就對他說一聲,讓他替你把蘑菇放進火鍋燙熟,替你把熟蘑菇放進蘸料碟里。
你唯一要做的事情就是吼一嗓子,然后從自己的蘸料碟里夾蘑菇,吃。
哎呀,這個就太爽了。
想吃豬腦,“來盤豬腦”;想吃鴨血,“來盤鴨血”;想吃 10 盤地瓜片就大喊“來10盤地瓜片”,用不著自己費事一盤一盤的往鍋里倒。
而且既然食材都堆在另一個地方,自己面前留一個吃東西的蘸料碟就夠用,十干凈整潔。桌子隨便換,揮揮衣袖帶雙筷子走就可以了。
一群人組團吃,大家各點各的吃互不干擾。燙火鍋的也始終只有一個人,既不會造成資源浪費,又不必讓其他人關心額外的東西。
這就是所謂的響應型編碼。
被分出去的那個人,在 React 體系里就是 Redux(或者相同功能的庫);具體到 dva 框架中,就是 model。
(理想情況下)所有的組件只和 model 連接,互相之間完全沒有直接交集,這便是響應型編碼思想在 dva 框架中的體現。
dva 中的響應型編碼
有了響應型編碼的理論以后,我很容易的就理解第 5 步的操作。
此時我的情況是:
- 可以通過 http://localhost:8000/user 訪問到 user
?
?
- 可以通過 http://localhost:8000/api/users 訪問到 user 數據
?
?
通過 dva g model user 可以很方便的創建 model/user.js 并注冊進 index.js 中(命令行萬歲!) ,雖然目前還什么都沒有:
?
?
我需要做的事情就是把數據從 api/user接口拉下來,渲染進 route/user 里(component 可以等等再談)。
把大象...我是說數據渲染進 route/user 需要三步:
編寫請求接口的方法
dva 的新手大禮包里已經提供了基礎的網絡請求函數 utils/resquest.js ,雖然大多數情況下都會對其進行一些擴展才能滿足現實項目的需求,但是就目前來說暫且是夠用的。
?
?
以 oop 觀點來看,utils/resquest.js 相當于項目所有請求函數的基類(base class)。如果需要進行具體業務的編寫,應該新建一個繼承 utils/resquest.js 的子類。但 JavaScript 不算是純種 oop 的語言,所以慣例都是新建一個具體的業務類 services/user.js,通過在 services/user.js 中 import 的方式調用 utils/resquest.js。
// 在 services 目錄下新建 services/user.js,負責具體的 user 業務import request from '../utils/request';export function getUserData() { // 偷懶,暫時把 example.js 的代碼拷貝過來return request('api/users'); // 這里是一個 promise 對象 }實際上這個時候如果直接把請求函數寫在 route/user.js 里已經可以渲染頁面了。
// 這是一個錯誤的示范import React, { Component, PropTypes } from 'react'; import * as userService from '../services/user';class User extends Component {static propTypes = {className: PropTypes.string,};constructor(props) {super(props);this.state = {list : []}}componentDidMount() {this.getData();}getData = () => {userService.getUserData().then((res) => {this.setState({list: res.data});})}buildContent = () => {const {list} = this.state;return list.map( (itm, index) => {return <div key={index}>{itm.name}</div>})}render() {return (<div>{this.buildContent()}</div>);} }export default User;這明顯是主動型編程寫法,和 dva 的響應型理念背道而馳。也許簡單或者低交互度的界面這么寫起來會很省事,但是可擴展性接近于零。一旦復雜度和交互度提升,組件的會變得越來越復雜,最后變成一個巨大的坑。
在 model 中使用 services 函數并獲得數據
有了 services/user.js 函數,可以進行具體的請求動作,在 model/user.js 請求數據了。
應該寫在 model/user.js 哪里呢?
?
?
這里可能又要多說一點所謂純函數的概念,即對于給定的輸入有唯一不變的輸出并不含任何明顯可見的副作用(side effects)的函數(可參考這篇英文文章或者中文版)。
請求網絡數據自帶副作用屬性(異步操作),而副作用(side effect)看起來確實和 model/user.js 里的某個屬性有點相似...
dva 的官方說法是:
真實情況最常見的副作用就是異步操作,所以 dva 提供了 effects 來專門放置副作用,不過可以發現的是,由于 effects 使用了 Generator Creator,所以將異步操作同步化,也是純函數。
dva 負責處理異步的是封裝后的 redux-saga 模塊。也就是說,需要使用 call 方法。所以 dva 的請求數據套路是這樣的:
effects: {*getData(action, { call, put }) { // 第一個參數是 dispatch 所發出的 action,第二個參數是 dva 封裝的 saga 函數。可以直接用 es 6 的解構風格拿取所需要的具體函數。const temp = yield call(userService.getUserData, {}); // 因為現在沒有參數console.log('temp', temp); // 純函數中不應有副作用(把數據轉移到控制臺也算副作用),這里只是方便在 chrome 里查看,}},寫完了?并沒有。
贊美太陽...呸!dispatch!
我眼中 dva 里 dispatch-atcion 與 model/effect 的原理有點像 Android 四大組件之一的廣播:
使用 dispatch 的好處是顯而易見的:切分業務模塊。
組件不必再負責具體的業務操作(自己動手涮肉),只需要 dispatch action (大喊一聲) 到對應的 model 里(給那個負責上菜的人)。
需要用戶列表數據的組件未必只有 route/user.js,其他需要數據的組件可以在自己里面 dispatch action。
同時 model/user.js 的 getData 方法是獨一份,你 dispatch 多少 type 為
user/getData (如果在 model 內 dispatch 可以省略前綴)的 action 都得歸到我這來處理。
高內聚(業務處理集中),低耦合( 隨時隨地隨便哪個組件隨意姿勢 dispatch)。
官方教程中給出的做法是在 model 里的訂閱部分 subscriptions寫一個監聽,根據監聽到具體的事件(進入 /user 頁面)進行特定操作(dispatch action)。
subscriptions: {setup({ dispatch, history }) { // eslint-disable-linereturn history.listen( ({pathname, query}) => {if(pathname === '/user') {dispatch({type: 'getData',payload: {txt: 'hello, dva'}})}})},},這么做同樣也是進一步切離業務,不必把 dispatch 寫在具體組件的生命周期中,減少組件的復雜程度(其實關鍵還是 dispatch ,訂閱說到底也是為 dispatch 服務的)。
現在應該可以看到輸出后的數據了。
?
?
渲染數據
雖然現在拿到了數據,但是數據還憋在 model/effects 里和 route/user.js 沒什么關系,總的想個辦法把數據和組件關聯起來。
是時候讓 dva 的 state 出場了。
我理解的 dva 中 model 內的 state 屬性,實際上是封裝后的 Redux 全局 store 的一部分。通過不重復的 namespace(桌號) 確定 store(餐館) 中唯一的 model(餐桌),把 model/effects 請求到的原始數據(生食)放進 model/reducer (特定的火鍋)里進行必要的處理(燙熟),再放進 model/state (蘸料碟)里,route/user.js 只需要從這里拿取所需要的數據(吃的)就可以了。
從 effects 里往 reducer 里傳遞數據使用的是 saga 的put 方法,參數同樣也是一個 action 對象,action 中必須包含的 type 屬性的值就是 reducer 屬性里的方法名:
import * as userService from '../services/user';export default {namespace: 'user',state: {},reducers: {dealData(state, action) {// 理論上 reducer 里的函數應該是純函數,此處只是為了方便在控制臺里看參數console.log('state==>', state);console.log('action==>', action);return { ...state }}},effects: {*getData(action, { call, put }) {const temp = yield call(userService.getUserData, {});yield put({type: 'dealData',payload: {temp}});}},subscriptions: {setup({ dispatch, history }) { // eslint-disable-linereturn history.listen( ({pathname, query}) => {if(pathname === '/user') {dispatch({type: 'getData',payload: {txt: 'hello, dva'}})}})},}, };?
?
剩下的做法就是在 model/user.js 的 state 屬性里定義一個屬性并賦值了。
state: {dataList: []},reducers: {dealData(state,{ payload: { temp: { data: dataList } } }// action// { payload: { temp: { data: dataList } }} // 是 es 6 的解構做法,等同于// const {payload} = action;// const {temp} = payload;// const {data} = temp;// const dataList = data;) {return { ...state, dataList }; // 必須有返回值(純函數必須有返回值),否則會報錯// 經評論提醒 修改 // 等同于 // let tmp = Object.assign([], this.state) // tmp.dataList = dataList}},現在需要的數據已經掛在 model/user.js 的 state 屬性里了,最后一步便是在 route/user.js 里使用 connect 和 mapStateToProps 讓組件監聽數據源,實現響應型編碼了。
import React from 'react'; import { connect } from 'dva'; // 0.關鍵的 connect import styles from './User.css'; import * as userService from '../services/user'; function User({ dataList }) { // 5. 這里的屬性就是 3 里的返回值return (<div className={styles.normal}>{!!dataList.length && dataList.map((data, index) => {return <div key={index}>{data.name}</div>})}</div>); }function mapStateToProps(store) { // 1關鍵的 mapStateToPropsconst { dataList } = store.user; // 2.從 model/user.js 拿取需要的數據return { dataList }; // 3.將數據作為屬性返回 }export default connect(mapStateToProps)(User); // 4.連接組件?
?
碎碎念
其實往后的代碼還有蠻多,分頁、封裝、引入 antd 調整樣式。不過都是一些需要花時間慢慢雕琢、順便發發 dispatch 的細節(其實細節也很重要 >_<),至少理解起來比較容易了。
理解第 5 步思路的順序是基于數據流向的,而實際開發中的編寫順序剛好是倒過來:先確定頁面需要的數據,再編寫 model 中的業務,最后把網絡接口掛進來。不過現在這么干已經心里有譜,知道怎么回事了。
可喜可賀。
鏈接:https://juejin.im/post/59946bbcf265da246f37e6d9
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的dva 中的响应编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: windows 下更新 npm 和 no
- 下一篇: Git关于pull,commit,pus