筆者升級了
dva
的版本冀偶,同時新增了umi
的使用捌议,具體可以參考這篇文章 dva理論到實踐——幫你掃清dva的知識盲點
本文中我會介紹一下相應的dva的相應知識點和實戰(zhàn)練習拣凹。
同時我也會介紹使用dva的流程狼渊,以及介紹使用dva中的坑测暗。希望大家通過這篇文章宠漩,能大致了解dva的使用流程柜蜈。
一仗谆,Dva簡介
1,借鑒 elm 的概念淑履,Reducer, Effect 和 Subscription
2隶垮,框架,而非類庫
3秘噪,基于 redux, react-router, redux-saga 的輕量級封裝
二狸吞,Dva的特性
1,僅有 5 個 API,僅有5個主要的api
蹋偏,其用法我們會在第三節(jié)詳細介紹便斥。
2,支持 HMR威始,支持模塊的熱更新椭住。
3,支持 SSR (ServerSideRender)字逗,支持服務器端渲染京郑。
4,支持 Mobile/ReactNative葫掉,支持移動手機端的代碼編寫些举。
5,支持 TypeScript俭厚,支持TypeScript户魏,個人感覺這個會是javascript
的一個趨勢。
6挪挤,支持路由和 Model 的動態(tài)加載叼丑。
7,…...
三扛门,Dva的5個API
1鸠信,app = dva(Opts):創(chuàng)建應用,返回 dva 實例论寨。(注:dva 支持多實例)?
在opts
可以配置所有的hooks
?
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
});
這里比較常用的是星立,history的配置,一般默認的是hashHistory
葬凳,如果要配置 history 為 browserHistory
绰垂,可以這樣:
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});
- 關(guān)于react-router中的
hashHistory
和browserHistory
的區(qū)別大家可以看:react-router。 -
initialState
:指定初始數(shù)據(jù)火焰,優(yōu)先級高于 model 中的 state劲装,默認是{}
,但是基本上都在modal里面設(shè)置相應的state昌简。
2占业,app.use(Hooks):配置 hooks 或者注冊插件。
這里最常見的就是dva-loading插件的配置江场,
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
?
但是一般對于全局的loading
我們會根據(jù)業(yè)務的不同來顯示相應不同的loading
圖標纺酸,我們可以根據(jù)自己的需要來選擇注冊相應的插件窖逗。
?
3址否,app.model(ModelObject):這個是你數(shù)據(jù)邏輯處理散休,數(shù)據(jù)流動的地方。
?
modal
是dva
里面與我們真正進行項目開發(fā)晾蜘,邏輯處理扶欣,數(shù)據(jù)流動的地方。這里面涉及到的namespace
音同、Modal
词爬、effects
和reducer
等概念都很重要,我們會在第四部分詳細講解权均。
?
4顿膨,app.router(Function):注冊路由表,我們做路由跳轉(zhuǎn)的地方叽赊。
一般都是這么寫的
import { Router, Route } from 'dva/router';
app.router(({ history }) => {
return (
<Router history={history}>
<Route path="/" component={App} />
<Router>
);
});
但是如果你的項目特別的龐大恋沃,我們就要考慮到相應的性能的問題,但是入門可以先看一下這個必指。對于如何做到按需加載大家可以看10分鐘 讓你dva從入門到精通囊咏,里面有簡單提到router
按需加載的寫法。
5塔橡,app.start([HTMLElement], opts)
啟動應用梅割,即將我們的應用跑起來。
四葛家,Dva九個概念
1户辞,State(狀態(tài))
? 初始值,我們在 dva()
初始化的時候和在 modal
里面的 state
對其兩處進行定義癞谒,其中 modal
中的優(yōu)先級低于傳給 dva()
的 opts.initialState
如下:
// dva()初始化
const app = dva({
initialState: { count: 1 },
});
// modal()定義事件
app.model({
namespace: 'count',
state: 0,
});
?
2咆课,Action:表示操作事件,可以是同步扯俱,也可以是異步
action
的格式如下书蚪,它需要有一個 type
,表示這個 action
要觸發(fā)什么操作迅栅;payload
則表示這個 action
將要傳遞的數(shù)據(jù)
{
type: String,
payload: data,
}
我們通過 dispatch
方法來發(fā)送一個 action
Action
Action 表示操作事件殊校,可以是同步,也可以是異步
{
type: String,
payload: data
}
格式
dispatch(Action);
dispatch({ type: 'todos/add', payload: 'Learn Dva' });
?
其實我們可以構(gòu)建一個Action
創(chuàng)建函數(shù)读存,如下
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
//我們直接dispatch(addTodo()),就發(fā)送了一個action为流。
dispatch(addTodo())
具體可以查看文檔:redux——action
?
3,Model
model
是 dva
中最重要的概念让簿,Model
非 MVC
中的 M
敬察,而是領(lǐng)域模型,用于把數(shù)據(jù)相關(guān)的邏輯聚合到一起尔当,幾乎所有的數(shù)據(jù)莲祸,邏輯都在這邊進行處理分發(fā)
-
state
這里的
state
跟我們剛剛講的state
的概念是一樣的,只不過她的優(yōu)先級比初始化的低,但是基本上項目中的state
都是在這里定義的锐帜。 -
namespace
model
的命名空間田盈,同時也是他在全局state
上的屬性,只能用字符串缴阎,我們發(fā)送在發(fā)送action
到相應的reducer
時允瞧,就會需要用到namespace
。 -
Reducer
以
key/value
格式定義reducer
蛮拔,用于處理同步操作述暂,唯一可以修改state
的地方。由action
觸發(fā)建炫。其實一個純函數(shù)贸典。 -
Effect
用于處理異步操作和業(yè)務邏輯,不直接修改
state
踱卵,簡單的來說廊驼,就是獲取從服務端獲取數(shù)據(jù),并且發(fā)起一個action
交給reducer
的地方惋砂。其中它用到了redux-saga妒挎,里面有幾個常用的函數(shù)。
*add(action, { call, put }) { yield call(delay, 1000); yield put({ type: 'minus' }); },
在項目中最主要的會用到的是 put
與 call
西饵。
-
Subscription
subscription
是訂閱酝掩,用于訂閱一個數(shù)據(jù)源,然后根據(jù)需要dispatch
相應的action
眷柔。在app.start()
時被執(zhí)行期虾,數(shù)據(jù)源可以是當前的時間、當前頁面的url
驯嘱、服務器的websocket
連接镶苞、history
路由變化等等。
4鞠评,Router
? Router
表示路由配置信息茂蚓,項目中的 router.js
。
export default function({ history }){
return(
<Router history={history}>
<Route path="/" component={App} />
</Router>
);
}
-
RouteComponent
?
RouteComponent
表示Router
里匹配路徑的Component
剃幌,通常會綁定model
的數(shù)據(jù)聋涨。如下:
import { connect } from 'dva';
function App() {
return <div>App</div>;
}
function mapStateToProps(state) {
return { todos: state.todos };
}
export default connect(mapStateToProps)(App);
?
五,整體架構(gòu)
我簡單的分析一下這個圖:
首先我們根據(jù) url
訪問相關(guān)的 Route-Component
负乡,在組件中我們通過 dispatch
發(fā)送 action
到 model
里面的 effect
或者直接 Reducer
當我們將action
發(fā)送給Effect
牍白,基本上是取服務器上面請求數(shù)據(jù)的,服務器返回數(shù)據(jù)之后抖棘,effect
會發(fā)送相應的 action
給 reducer
茂腥,由唯一能改變 state
的 reducer
改變 state
狸涌,然后通過connect
重新渲染組件。
當我們將action
發(fā)送給reducer
础芍,那直接由 reducer
改變 state
杈抢,然后通過 connect
重新渲染組件数尿。
這樣我們就能走完一個流程了仑性。
六,項目案例
這一節(jié)我們會根據(jù)dva的快速搭建一個計數(shù)器右蹦。官方的例子是都把所有的邏輯寫在了入口文件HomePage.js
里诊杆,我會在下面的demo中,把例子中的各個模塊抽出來何陆,放在相應的文件夾中晨汹。讓大家能更加清楚每一個模塊的作用。
?
1贷盲,首先全局安裝dva-cli
淘这,我的操作在桌面進行的,大家可以自行選擇項目目錄巩剖。
$ npm install -g dva-cli
?
2铝穷,接著使用dva-cli
創(chuàng)建我們的項目文件夾
$ dva new myapp
?
3,進入myapp
目錄佳魔,安裝依賴曙聂,執(zhí)行如下操作。
$ cd myapp
$ npm start
?
瀏覽器會自動打開一個窗口鞠鲜,如下圖宁脊。
?
4,目錄結(jié)構(gòu)介紹
.
├── mock // mock數(shù)據(jù)文件夾
├── node_modules // 第三方的依賴
├── public // 存放公共public文件的文件夾
├── src // 最重要的文件夾贤姆,編寫代碼都在這個文件夾下
│ ├── assets // 可以放圖片等公共資源
│ ├── components // 就是react中的木偶組件
│ ├── models // dva最重要的文件夾榆苞,所有的數(shù)據(jù)交互及邏輯都寫在這里
│ ├── routes // 就是react中的智能組件,不要被文件夾名字誤導霞捡。
│ ├── services // 放請求借口方法的文件夾
│ ├── utils // 自己的工具方法可以放在這邊
│ ├── index.css // 入口文件樣式
│ ├── index.ejs // ejs模板引擎
│ ├── index.js // 入口文件
│ └── router.js // 項目的路由文件
├── .eslintrc // bower安裝目錄的配置
├── .editorconfig // 保證代碼在不同編輯器可視化的工具
├── .gitignore // git上傳時忽略的文件
├── .roadhogrc.js // 項目的配置文件语稠,配置接口轉(zhuǎn)發(fā),css_module等都在這邊弄砍。
├── .roadhogrc.mock.js // 項目的配置文件
└── package.json // 當前整一個項目的依賴
?
5仙畦,首先是前端的頁面,我們使用 class
形式來創(chuàng)建組件音婶,原例子中是使用無狀態(tài)來創(chuàng)建的慨畸。react
創(chuàng)建組件的各種方式,大家可以看React創(chuàng)建組件的三種方式及其區(qū)別
?
我們先修改route/IndexPage.js
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';
class IndexPage extends React.Component {
render() {
const { dispatch } = this.props;
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: 1</div>
<div className={styles.current}>2</div>
<div className={styles.button}>
<button onClick={() => {}}>+</button>
</div>
</div>
);
}
}
export default connect()(IndexPage);
?
同時修改樣式routes/IndexPage.css
.normal {
width: 200px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ccc;
box-shadow: 0 0 20px #ccc;
}
.record {
border-bottom: 1px solid #ccc;
padding-bottom: 8px;
color: #ccc;
}
.current {
text-align: center;
font-size: 40px;
padding: 40px 0;
}
.button {
text-align: center;
button {
width: 100px;
height: 40px;
background: #aaa;
color: #fff;
}
}
?
此時你的頁面應該是如下圖所示
?
6衣式,在 model
處理 state
寸士,在頁面里面輸出 model
中的 state
?
首先我們在index.js
中將models/example.js
檐什,即將model下一行的的注釋打開。
import dva from 'dva';
import './index.css';
// 1. Initialize
const app = dva();
// 2. Plugins
// app.use({});
// 3. Model
app.model(require('./models/example')); // 打開注釋
// 4. Router
app.router(require('./router'));
// 5. Start
app.start('#root');
?
接下來我們進入 models/example.js
弱卡,將namespace
名字改為 count
乃正,state
對象加上 record
與 current
屬性。如下:
export default {
namespace: 'count',
state: {
record: 0,
current: 0,
},
...
};
?
接著我們來到 routes/indexpage.js
頁面婶博,通過的 mapStateToProps
引入相關(guān)的 state
瓮具。
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';
class IndexPage extends React.Component {
render() {
const { dispatch, count } = this.props;
return (
<div className={styles.normal}>
<div className={styles.record}>
Highest Record: {count.record} // 將count的record輸出
</div>
<div className={styles.current}>
{count.current}
</div>
<div className={styles.button}>
<button onClick={() => {} } >
+
</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return { count: state };
} // 獲取state
export default connect(mapStateToProps)(IndexPage);
?
打開網(wǎng)頁:你應該能看到下圖:
?
7,通過 +
發(fā)送 action
凡人,通過 reducer
改變相應的 state
?
首先我們在 models/example.js
名党,寫相應的 reducer
。
export default {
...
reducers: {
add1(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1 };
},
},
};
?
在頁面的模板 routes/IndexPage.js
中 +
號點擊的時候挠轴,dispatch
一個 action
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';
class IndexPage extends React.Component {
render() {
const { dispatch, count } = this.props;
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: {count.record}</div>
<div className={styles.current}>{count.current}</div>
<div className={styles.button}>
<button
+ onClick={() => { dispatch({ type: 'count/add1' });}
}>+</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return { count: state.count };
}
export default connect(mapStateToProps)(IndexPage);
?
效果如下圖:
?
8传睹,接下來我們來使用 effect
模擬一個數(shù)據(jù)接口請求,返回之后岸晦,通過 yield put()
改變相應的 state
?
首先我們替換相應的 models/example.js
的 effect
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
?
這里的 delay
欧啤,是我這邊寫的一個延時的函數(shù),我們在 utils
里面編寫一個 utils.js
启上,一般請求接口的函數(shù)都會寫在 servers
文件夾中邢隧。
export function delay(timeout) {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}
?
接著我們在 models/example.js
導入這個 utils.js
import { delay } from '../utils/utils';
?
9,訂閱訂閱鍵盤事件碧绞,使用 subscriptions
府框,當用戶按住 command+up
時候觸發(fā)添加數(shù)字的 action
在 models/example.js
中作如下修改
+import key from 'keymaster';
...
app.model({
namespace: 'count',
+ subscriptions: {
+ keyboardWatcher({ dispatch }) {
+ key('?+up, ctrl+up', () => { dispatch({type:'add'}) });
+ },
+ },
});
在這里你需要安裝 keymaster
這個依賴
npm install keymaster --save
現(xiàn)在你可以按住 command+up
就可以使 current
加1了。
?
10讥邻,例子中我們看到當我們不斷點擊+
按鈕之后迫靖,我們會看到current
會不斷加一,但是1s過后兴使,他會自動減到零系宜。
官方的demo
的代買沒有實現(xiàn)gif圖里面的效果,大家看下圖:
?
要做到gif里面的效果发魄,我們應該在effect
中發(fā)送一個關(guān)于添加的action
盹牧,但是我們在effect
中不能直接這么寫:
effects: {
*add(action, { call, put }) {
yield put({ type: 'add' });
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
?
因為如果這樣的話,effect
與reducers
中的add
方法重合了励幼,這里會陷入一個死循環(huán)汰寓,因為當組件發(fā)送一個dispatch
的時候,model
會首先去找effect
里面的方法苹粟,當又找到add
的時候有滑,就又會去請求effect
里面的方法。
?
我們應該更改reducers
里面的方法嵌削,使它不與effect
的方法一樣毛好,將reducers
中的add
改為add1
望艺,如下:
reducers: {
add1(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield put({ type: 'add1' });
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
?
這樣我們就實現(xiàn)了gif圖中的效果:
?
至此我們的簡單的demo
就結(jié)束了,通過這個例子大家可以基本上了解dva
的基本概念肌访。
如果還想深入了解dva的各個文件夾中文件的特性找默,大家可以看快速上手dva的一個簡單demo,這里面會很詳細的講到我們該怎么寫 model
吼驶、怎么使用effect
請求接口數(shù)據(jù)等等惩激。
?
這段時間我也利用業(yè)余時間,使用dva
+thinkphp
構(gòu)建一個類似boss直聘的手機端web應用旨剥,項目還沒全部做完咧欣,大家如果感興趣的話浅缸,可以下載下來看看轨帜,一起探討相關(guān)思路哦。
?
?