初識 Dva
近期,我們在內部做了一個類似 IDE 性質的應用,基於 electron 和 dva,由於之前一直只關注 node 相關的開發者工具,並未太多接觸 React 等內容,所以這段時間過的有點煎熬同時也很興奮,煎熬來源於非舒適區,而興奮來源於發現基於 dva + electron 給開發者工具帶來了更多的可能性。
此次開發 IDE 專案組織方式已由 sorrycc 同學整理成腳手架 dva-boilerplate-electron。
初識 dva 是此次總結的第一篇文章,第二篇文章我會記錄下在 electron 中的相關沉澱。
迴歸正題,如何在幾天內玩好 React、Dva、Electron。
React 基礎知識
什麼是 React o.o ?
React 的核心目的是建立 UI 元件,也就是說它是 MVC 架構中的 V 層,所以 React 和你的技術架構並沒有關係。
打個比方來說在 AngularJS 1.x 中它通過擴充套件 html 標籤,注入一些結構性的東西(比如 Controllers, Services),所以 AngularJS 1.x 是會侵入到你整個技術的架構,從某些方面來說這些抽象確實能解決一些業務問題,但由此而來的是塔缺乏了靈活性。
React 這種僅僅關注在 Components 的庫,給了開發者非常強的靈活度,因為我不併不會被束縛在某一個技術架構。
Components 在各個生命週期內發生了什麼 ?
總結來講
從最上層來說 React Component 生命週期可以落入到以下三個環節:
- 初始化,Initialization
- state/props 更新,State/Property Updates
- 銷燬, Destruction
在這三個類別下分別對應著一些 React 的抽象方法,這些方法都是在元件特定生命週期中的鉤子,這些鉤子會在元件整個生命週期中執行一次或者多次。明白了這些鉤子的呼叫時機,可以有助於更好的書寫元件。
比如:
componentWillMount
render
之前執行且永遠只執行一次。
componentDidMount
: 元件載入完畢之後立即執行,並且此時才在 DOM 樹中生成了對應的節點,因此我們通過 this.getDOMNode() 來獲取到對應的節點。
等等詳細請看 文件。
component 的幾種建立方式
- React 寫法
import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom'
var SayHi = React.createClass({
getInitialState(){
return {};
},
getDefaultProps(){
return { from: 'pigcan' };
}
propTypes:{
name: PropTypes.string.isRequired,
},
render(){
var name=this.props.name;
return(
{from} says: hello {name}!
); } }) ReactDOM.render( , document.getElementById('demo') )
- ES6 寫法
import React, { Component, PropTypes } from 'react';
import { Popover, Icon } from 'antd';
class PreviewQRCodeBar extends Component { // 元件的宣告方式
constructor(props) { // 初始化的工作放入到建構函式
super(props); // 在 es6 中如果有父類,必須有 super 的呼叫用以初始化父類資訊
this.state = { // 初始 state 設定方式
visible: false,
};
}
// 因為是類,所以屬性與方法之間不必新增逗號
hide() {
this.setState({
visible: false,
});
}
handleVisibleChange(visible) {
this.setState({ visible });
}
render() {
const { dataurl } = this.props;
return (
}
trigger="click"
visible={this.state.visible}
onVisibleChange={this.handleVisibleChange.bind(this)} // 通過 .bind(this) 來繫結
>
);
}
}
// 在 react 寫法中,直接通過 propTypes {key:value} 來約定
PreviewQRCodeBar.proptypes = {
dataurl: PropTypes.string.isRequired,
};
// 在 ES6 類宣告中無法設定 props 只能在類的駐外使用 defaultProps 屬性來完成預設值的設定
// 而在 react 中則通過 getDefaultProps(){} 方法來設定
PreviewQRCodeBar.defaults = {
// obj
}
export default PreviewQRCodeBar;
- Stateless 寫法
import React, { PropTypes } from 'react';
// 元件無 state,pure function
const PreviewDevToolWebview = ({ remoteUrl }) => // 箭頭函式,結構賦值
;
PreviewDevToolWebview.proptype = {
remoteUrl: PropTypes.string.isRequired,
};
export default PreviewDevToolWebview;
// 此類元件不支援 ref 屬性,沒有元件生命週期的相關的時候和方法,僅支援 propTypes
// 此類元件用以簡單呈現資料
如果想了解更多的基礎
Flux 又是什麼鬼
簡而言之 Flux 是一種架構思想,和 MVC 一樣,用以解決軟體結構的問題,如上所說 React 只是涉及了 UI 層所以在搭建大型應用時必須要有與之配套的應用架構。在 React 社群大家普遍使用 Flux 架構的思想來搭建應用,目前 flux 前端框架。
Flux 中最為顯著的特點就是它的單向資料流,核心目的是為了在多元件互動時能避免資料的汙染。
在 flux 模式中 Store 層是所有資料的權利中心,任何資料的變更都需要發生在 store 中,Store 層發生的資料變更隨後都會通過事件的方式廣播給訂閱該事件的 View,隨後 View 會根據接受到的新的資料狀態來更新自己。任何想要變更 Store 層資料都需要呼叫 Action,而這些 Action 則由 Dispatcher 集中排程,在使用 Actions 時需要確保每個 action 對應一個數據更新,並同一時刻只觸發一個 action。
說一說我個人的感受,在以往 MVC 架構中,某一個 Model 的資料可能被多個 View 共享,而每個 View 在通常情況下都會有自己的 Controller 層來代理 Model 和 View,那樣子很顯著的一個問題就出現了,任何一個 Controller 都可能會引發 Model 的資料更新,在現實中我們的應用通常擁有更為複雜的 UI 層,所以使用稍有不當我們的資料流將亂如麻,在除錯中我們也會越來越難以除錯,因為我們很難確定資料變更發生的確切位置。
dva 中的資料流
如何來理解呢?
在 web 應用中,資料的改變通常發生在使用者互動行為或者瀏覽器行為(如路由跳轉等),當此類行為改變資料的時候可以通過 dispatch
發起一個 action,如果是同步行為會直接通過 Reducers
改變 State
,如果是非同步行為會先觸發 Effects
然後流向 Reducers
最終改變 State
,所以在 dva 中,資料流向非常清晰簡明,並且思路基本跟開源社群保持一致。
dva 的基本概念
簡而言之 dva 是基於現有應用架構 (redux + react-router + redux-saga 等)的一層輕量封裝
dva 的基本概念有哪些?
以下內容基本摘自 Dva Concepts
dva - Model
State
State 表示 Model 的狀態資料,通常表現為一個 javascript 物件(immutable data)。
Action
Action 是一個普通 javascript 物件,它是改變 State 的唯一途徑。無論是從 UI 事件、網路回撥,還是 WebSocket 等資料來源所獲得的資料,最終都會通過 dispatch 函式呼叫一個 action,從而改變對應的資料。** 需要注意的是 dispatch
是在元件 connect Models以後,通過 props 傳入的。**
dispatch({
type: 'user/add', // 如果在 model 外呼叫,需要新增 namespace
payload: {}, // 需要傳遞的資訊
});
以上呼叫函式內的物件就是一個 action。
dispatch 函式
用於觸發 action 的函式,action 是改變 State 的唯一途徑,但是它只描述了一個行為,而 dipatch 可以看作是觸發這個行為的方式,而 Reducer 則是描述如何改變資料的。
dva - Reducer
在 dva 中,reducers 聚合積累的結果是當前 model 的 state 物件。通過 actions 中傳入的值,與當前 reducers 中的值進行運算獲得新的值(也就是新的 state)。需要注意的是 Reducer 必須是純函式。
app.model({
namespace: 'todos', //model 的 namespace
state: [], // model 的初始化資料
reducers: {
// add 方法就是 reducer,可以看到它其實非常簡單就是把老的 state 和接收到的資料處理下,返回新的 state
add(state, { payload: todo }) {
return state.concat(todo);
},
},
};
dva - Effect
Effect 被稱為副作用,在我們的應用中,最常見的就是非同步操作,Effects
的最終流向是通過 Reducers
改變 State
。
核心需要關注下 put, call, select。
app.model({
namespace: 'todos',
effects: {
*addRemote({ payload: todo }, { put, call, select }) {
const todos = yield select(state => state.todos); // 這邊的 state 來源於全域性的 state,select 方法提供獲取全域性 state 的能力,也就是說,在這邊如果你有需要其他 model 的資料,則完全可以通過 state.modelName 來獲取
yield call(addTodo, todo); // 用於呼叫非同步邏輯,支援 promise 。
yield put({ type: 'add', payload: todo }); // 用於觸發 action 。這邊需要注意的是,action 所呼叫的 reducer 或 effects 來源於本 model 那麼在 type 中不需要宣告名稱空間,如果需要觸發其他非本 model 的方法,則需要在 type 中宣告名稱空間,如 yield put({ type: 'namespace/fuc', payload: xxx });
},
},
});
dva - Subscription
Subscriptions 是一種從 源 獲取資料的方法,它來自於 elm。
Subscription 語義是訂閱,用於訂閱一個數據源,然後根據條件 dispatch 需要的 action。資料來源可以是當前的時間、伺服器的 websocket 連線、keyboard 輸入、geolocation 變化、history 路由變化等等。
import key from 'keymaster';
...
app.model({
namespace: 'count',
subscriptions: {
keyEvent(dispatch) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
}
});
dva - Router
這裡的路由通常指的是前端路由,由於我們的應用現在通常是單頁應用,所以需要前端程式碼來控制路由邏輯,通過瀏覽器提供的 History API 可以監聽瀏覽器url的變化,從而控制路由相關操作。
dva 例項提供了 router 方法來控制路由,使用的是react-router。
import { Router, Route } from 'dva/router';
app.router(({history}) =>
);
詳見 react-router
dva - Route Components
在 dva 中我們通常以頁面維度來設計 Container Components。
所以在 dva 中,通常需要 connect Model的元件都是 Route Components,組織在/routes/
目錄下,而/components/
目錄下則是純元件(Presentational Components)。
** 通過 connect 繫結資料 **
比如:
import { connect } from 'dva';
function App() {}
function mapStateToProps(state, ownProps) {
// 該方法名已經非常形象的說明了 connect 的作用在於 State -> Props 的轉換,
// 同時自動註冊一個 dispatch 的方法,用以觸發 action
return {
users: state.users,
};
}
export default connect(mapStateToProps)(App);
然後在 App 裡就有了 dispatch
和 users
兩個屬性。
好了,如上就是 dva 中的一些核心概念,起初看的時候可能一下子接收到的資訊量頗大,但是不要著急,後續業務中的使用會讓你對於如上概念越來越清晰。
那麼如何來啟動一個 dva 應用呢
// Install dva-cli
$ npm install dva-cli -g
// Create app and start
$ dva new myapp
$ cd myapp
$ npm install
$ npm start
Done o.o
讓我們來一窺 dva 專案 src
目錄結構,嘗試來明白整體的程式碼的組織方式
.
├── assets
│ └── yay.jpg
├── components
│ └── Example.js
├── index.css
├── index.html
├── index.js
├── models
│ └── example.js
├── router.js
├── routes
│ ├── IndexPage.css
│ └── IndexPage.js
├── services
│ └── example.js
├── tests
│ └── models
│ └── example-test.js
└── utils
└── request.js
assets
: 我們可以把專案 assets 資源丟在這邊components
: 純元件,在 dva 應用中 components 目錄中應該是一些 logicless
的 component, logic 部分均由對應的 route-component 來承載。在安裝完 dva-cli 工具後,我們可以通過 dva g component componentName 的方式來建立一個 component。index.css
: 首頁樣式index.html
: 首頁index.js
: dva 應用啟動 五部曲
,這點稍後再展開models
: 該目錄結構用以存放 model,在通常情況下,一個 model 對應著一個 route-component,而 route-component 則對應著多個 component,當然這取決於你如何拆分,個人偏向於盡可能細粒度的拆分。在安裝完 dva-cli 工具後,我們可以通過 dva g model modelName
的方式來建立一個 model。該 model 會在 index.js
中自動註冊。router.js
: 頁面相關的路由配置,相應的 route-component 的引入routes
: route-component 存在的地方,在安裝完 dva-cli 工具後,我們可以通過 dva g route route-name
的方式去建立一個 route-component,該路由配置會被自動更新到 route.js
中。route-component 是一個重邏輯區,一般業務邏輯全部都在此處理,通過 connect
方法,實現 model 與 component 的聯動。services
: 全域性服務,如傳送非同步請求tests
: 測試相關utils
: 全域性類公共函式
dva 的五部曲
import './index.html';
import './index.css';
import dva from 'dva';
// 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');
好了,以上便是五部曲,看了 dva 官方文件的可能說還少一步
// 4. Connect components and models
const App = connect(mapStateToProps)(Component);
原因是在實際業務中,我們的 connect 行為通常在 route-component 中進行設定。
以上。
對了,人為新增 model 後記得 model 要在 index.js 中予以註冊,當然使用腳手架功能並不存在這個問題。 XD。