筆者升級了
dva
的版本,同時新增了umi
的使用,具體可以參考這篇文章 dva理論到實踐——幫你掃清dva的知識盲點
本文中我會介紹一下相應的dva的相應知識點和實戰練習。
同時我也會介紹使用dva的流程,以及介紹使用dva中的坑。希望大家通過這篇文章,能大致了解dva的使用流程。
一,Dva簡介
1,借鑒 elm 的概念,Reducer, Effect 和 Subscription
2,框架,而非類庫
3,基于 redux, react-router, redux-saga 的輕量級封裝
二,Dva的特性
1,僅有 5 個 API,僅有5個主要的api
,其用法我們會在第三節詳細介紹。
2,支持 HMR,支持模塊的熱更新。
3,支持 SSR (ServerSideRender),支持服務器端渲染。
4,支持 Mobile/ReactNative,支持移動手機端的代碼編寫。
5,支持 TypeScript,支持TypeScript,個人感覺這個會是javascript
的一個趨勢。
6,支持路由和 Model 的動態加載。
7,…...
三,Dva的5個API
1,app = dva(Opts):創建應用,返回 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(),
});
- 關于react-router中的
hashHistory
和browserHistory
的區別大家可以看:react-router。 -
initialState
:指定初始數據,優先級高于 model 中的 state,默認是{}
,但是基本上都在modal里面設置相應的state。
2,app.use(Hooks):配置 hooks 或者注冊插件。
這里最常見的就是dva-loading插件的配置,
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
?
但是一般對于全局的loading
我們會根據業務的不同來顯示相應不同的loading
圖標,我們可以根據自己的需要來選擇注冊相應的插件。
?
3,app.model(ModelObject):這個是你數據邏輯處理,數據流動的地方。
?
modal
是dva
里面與我們真正進行項目開發,邏輯處理,數據流動的地方。這里面涉及到的namespace
、Modal
、effects
和reducer
等概念都很重要,我們會在第四部分詳細講解。
?
4,app.router(Function):注冊路由表,我們做路由跳轉的地方。
一般都是這么寫的
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(狀態)
? 初始值,我們在 dva()
初始化的時候和在 modal
里面的 state
對其兩處進行定義,其中 modal
中的優先級低于傳給 dva()
的 opts.initialState
如下:
// dva()初始化
const app = dva({
initialState: { count: 1 },
});
// modal()定義事件
app.model({
namespace: 'count',
state: 0,
});
?
2,Action:表示操作事件,可以是同步,也可以是異步
action
的格式如下,它需要有一個 type
,表示這個 action
要觸發什么操作;payload
則表示這個 action
將要傳遞的數據
{
type: String,
payload: data,
}
我們通過 dispatch
方法來發送一個 action
Action
Action 表示操作事件,可以是同步,也可以是異步
{
type: String,
payload: data
}
格式
dispatch(Action);
dispatch({ type: 'todos/add', payload: 'Learn Dva' });
?
其實我們可以構建一個Action
創建函數,如下
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
//我們直接dispatch(addTodo()),就發送了一個action。
dispatch(addTodo())
具體可以查看文檔:redux——action
?
3,Model
model
是 dva
中最重要的概念,Model
非 MVC
中的 M
,而是領域模型,用于把數據相關的邏輯聚合到一起,幾乎所有的數據,邏輯都在這邊進行處理分發
-
state
這里的
state
跟我們剛剛講的state
的概念是一樣的,只不過她的優先級比初始化的低,但是基本上項目中的state
都是在這里定義的。 -
namespace
model
的命名空間,同時也是他在全局state
上的屬性,只能用字符串,我們發送在發送action
到相應的reducer
時,就會需要用到namespace
。 -
Reducer
以
key/value
格式定義reducer
,用于處理同步操作,唯一可以修改state
的地方。由action
觸發。其實一個純函數。 -
Effect
用于處理異步操作和業務邏輯,不直接修改
state
,簡單的來說,就是獲取從服務端獲取數據,并且發起一個action
交給reducer
的地方。其中它用到了redux-saga,里面有幾個常用的函數。
*add(action, { call, put }) { yield call(delay, 1000); yield put({ type: 'minus' }); },
在項目中最主要的會用到的是 put
與 call
。
-
Subscription
subscription
是訂閱,用于訂閱一個數據源,然后根據需要dispatch
相應的action
。在app.start()
時被執行,數據源可以是當前的時間、當前頁面的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
的數據。如下:
import { connect } from 'dva';
function App() {
return <div>App</div>;
}
function mapStateToProps(state) {
return { todos: state.todos };
}
export default connect(mapStateToProps)(App);
?
五,整體架構
我簡單的分析一下這個圖:
首先我們根據 url
訪問相關的 Route-Component
,在組件中我們通過 dispatch
發送 action
到 model
里面的 effect
或者直接 Reducer
當我們將action
發送給Effect
,基本上是取服務器上面請求數據的,服務器返回數據之后,effect
會發送相應的 action
給 reducer
,由唯一能改變 state
的 reducer
改變 state
,然后通過connect
重新渲染組件。
當我們將action
發送給reducer
,那直接由 reducer
改變 state
,然后通過 connect
重新渲染組件。
這樣我們就能走完一個流程了。
六,項目案例
這一節我們會根據dva的快速搭建一個計數器。官方的例子是都把所有的邏輯寫在了入口文件HomePage.js
里,我會在下面的demo中,把例子中的各個模塊抽出來,放在相應的文件夾中。讓大家能更加清楚每一個模塊的作用。
?
1,首先全局安裝dva-cli
,我的操作在桌面進行的,大家可以自行選擇項目目錄。
$ npm install -g dva-cli
?
2,接著使用dva-cli
創建我們的項目文件夾
$ dva new myapp
?
3,進入myapp
目錄,安裝依賴,執行如下操作。
$ cd myapp
$ npm start
?
瀏覽器會自動打開一個窗口,如下圖。
?
4,目錄結構介紹
.
├── mock // mock數據文件夾
├── node_modules // 第三方的依賴
├── public // 存放公共public文件的文件夾
├── src // 最重要的文件夾,編寫代碼都在這個文件夾下
│ ├── assets // 可以放圖片等公共資源
│ ├── components // 就是react中的木偶組件
│ ├── models // dva最重要的文件夾,所有的數據交互及邏輯都寫在這里
│ ├── routes // 就是react中的智能組件,不要被文件夾名字誤導。
│ ├── services // 放請求借口方法的文件夾
│ ├── utils // 自己的工具方法可以放在這邊
│ ├── index.css // 入口文件樣式
│ ├── index.ejs // ejs模板引擎
│ ├── index.js // 入口文件
│ └── router.js // 項目的路由文件
├── .eslintrc // bower安裝目錄的配置
├── .editorconfig // 保證代碼在不同編輯器可視化的工具
├── .gitignore // git上傳時忽略的文件
├── .roadhogrc.js // 項目的配置文件,配置接口轉發,css_module等都在這邊。
├── .roadhogrc.mock.js // 項目的配置文件
└── package.json // 當前整一個項目的依賴
?
5,首先是前端的頁面,我們使用 class
形式來創建組件,原例子中是使用無狀態來創建的。react
創建組件的各種方式,大家可以看React創建組件的三種方式及其區別
?
我們先修改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
引入相關的 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);
?
打開網頁:你應該能看到下圖:
?
7,通過 +
發送 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
模擬一個數據接口請求,返回之后,通過 yield put()
改變相應的 state
?
首先我們替換相應的 models/example.js
的 effect
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
?
這里的 delay
,是我這邊寫的一個延時的函數,我們在 utils
里面編寫一個 utils.js
,一般請求接口的函數都會寫在 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
時候觸發添加數字的 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
現在你可以按住 command+up
就可以使 current
加1了。
?
10,例子中我們看到當我們不斷點擊+
按鈕之后,我們會看到current
會不斷加一,但是1s過后,他會自動減到零。
官方的demo
的代買沒有實現gif圖里面的效果,大家看下圖:
?
要做到gif里面的效果,我們應該在effect
中發送一個關于添加的action
,但是我們在effect
中不能直接這么寫:
effects: {
*add(action, { call, put }) {
yield put({ type: 'add' });
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
?
因為如果這樣的話,effect
與reducers
中的add
方法重合了,這里會陷入一個死循環,因為當組件發送一個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' });
},
},
?
這樣我們就實現了gif圖中的效果:
?
至此我們的簡單的demo
就結束了,通過這個例子大家可以基本上了解dva
的基本概念。
如果還想深入了解dva的各個文件夾中文件的特性,大家可以看快速上手dva的一個簡單demo,這里面會很詳細的講到我們該怎么寫 model
、怎么使用effect
請求接口數據等等。
?
這段時間我也利用業余時間,使用dva
+thinkphp
構建一個類似boss直聘的手機端web應用,項目還沒全部做完,大家如果感興趣的話,可以下載下來看看,一起探討相關思路哦。
?
?