dva-知识地图
注:如果你使用 dva@2,請先忽略這里的路由部分,待更新。
不知大家學 react 或 dva 時會不會有這樣的疑惑:
- es6 特性那么多,我需要全部學會嗎?
- react component 有 3 種寫法,我需要全部學會嗎?
- reducer 的增刪改應該怎么寫?
- 怎么做全局/局部的錯誤處理?
- 怎么發異步請求?
- 怎么處理復雜的異步業務邏輯?
- 怎么配置路由?
- ...
這篇文檔梳理了基于?dva-cli?使用?dva?的最小知識集,讓你可以用最少的時間掌握創建類似?dva-hackernews?的全部知識,并且不需要掌握額外的冗余知識。
目錄
- JavaScript 語言
- 變量聲明
- const 和 let
- 模板字符串
- 默認參數
- 箭頭函數
- 模塊的 Import 和 Export
- ES6 對象和數組
- 析構賦值
- 對象字面量改進
- Spread Operator
- Promises
- Generators
- 變量聲明
- React Component
- Stateless Functional Components
- JSX
- Component 嵌套
- className
- JavaScript 表達式
- Mapping Arrays to JSX
- 注釋
- Spread Attributes
- Props
- propTypes
- 往下傳數據
- 往上傳數據
- CSS Modules
- 理解 CSS Modules
- 定義全局 CSS
- classnames Package
- Reducer
- 增刪改
- 嵌套數據的增刪改
- Effect
- Effects
- put
- call
- select
- 錯誤處理
- 全局錯誤處理
- 本地錯誤處理
- 異步請求
- GET 和 POST
- 統一錯誤處理
- Effects
- Subscription
- 異步數據初始化
- path-to-regexp Package
- 異步數據初始化
- Router
- Config with JSX Element (router.js)
- Route Components
- 通過 connect 綁定數據
- Injected Props (e.g. location)
- 基于 action 進行頁面跳轉
- dva 配置
- Redux Middleware
- history
- 切換 history 為 browserHistory
- 去除 hashHistory 下的 _k 查詢參數
- 工具
- 通過 dva-cli 創建項目
JavaScript 語言
變量聲明
const 和 let
不要用?var,而是用?const?和?let,分別表示常量和變量。不同于?var?的函數作用域,const?和?let?都是塊級作用域。
const DELAY = 1000;let count = 0; count = count + 1;模板字符串
模板字符串提供了另一種做字符串組合的方法。
const user = 'world'; console.log(`hello ${user}`); // hello world// 多行 const content = `Hello ${firstName},Thanks for ordering ${qty} tickets to ${event}. `;默認參數
function logActivity(activity = 'skiing') {console.log(activity); }logActivity(); // skiing箭頭函數
函數的快捷寫法,不需要通過?function?關鍵字創建函數,并且還可以省略?return?關鍵字。
同時,箭頭函數還會繼承當前上下文的?this?關鍵字。
比如:
[1, 2, 3].map(x => x + 1); // [2, 3, 4]等同于:
[1, 2, 3].map((function(x) {return x + 1; }).bind(this));模塊的 Import 和 Export
import?用于引入模塊,export?用于導出模塊。
比如:
// 引入全部 import dva from 'dva';// 引入部分 import { connect } from 'dva'; import { Link, Route } from 'dva/router';// 引入全部并作為 github 對象 import * as github from './services/github';// 導出默認 export default App; // 部分導出,需 import { App } from './file'; 引入 export class App extend Component {};ES6 對象和數組
析構賦值
析構賦值讓我們從 Object 或 Array 里取部分數據存為變量。
// 對象 const user = { name: 'guanguan', age: 2 }; const { name, age } = user; console.log(`${name} : ${age}`); // guanguan : 2// 數組 const arr = [1, 2]; const [foo, bar] = arr; console.log(foo); // 1我們也可以析構傳入的函數參數。
const add = (state, { payload }) => {return state.concat(payload); };析構時還可以配 alias,讓代碼更具有語義。
const add = (state, { payload: todo }) => {return state.concat(todo); };對象字面量改進
這是析構的反向操作,用于重新組織一個 Object 。
const name = 'duoduo'; const age = 8;const user = { name, age }; // { name: 'duoduo', age: 8 }定義對象方法時,還可以省去?function?關鍵字。
app.model({reducers: {add() {} // 等同于 add: function() {}},effects: {*addRemote() {} // 等同于 addRemote: function*() {}}, });Spread Operator
Spread Operator 即 3 個點?...,有幾種不同的使用方法。
可用于組裝數組。
const todos = ['Learn dva']; [...todos, 'Learn antd']; // ['Learn dva', 'Learn antd']也可用于獲取數組的部分項。
const arr = ['a', 'b', 'c']; const [first, ...rest] = arr; rest; // ['b', 'c']// With ignore const [first, , ...rest] = arr; rest; // ['c']還可收集函數參數為數組。
function directions(first, ...rest) {console.log(rest); } directions('a', 'b', 'c'); // ['b', 'c'];代替 apply。
function foo(x, y, z) {} const args = [1,2,3];// 下面兩句效果相同 foo.apply(null, args); foo(...args);對于 Object 而言,用于組合成新的 Object 。(ES2017 stage-2 proposal)
const foo = {a: 1,b: 2, }; const bar = {b: 3,c: 2, }; const d = 4;const ret = { ...foo, ...bar, d }; // { a:1, b:3, c:2, d:4 }此外,在 JSX 中 Spread Operator 還可用于擴展 props,詳見?Spread Attributes。
Promises
Promise 用于更優雅地處理異步請求。比如發起異步請求:
fetch('/api/todos').then(res => res.json()).then(data => ({ data })).catch(err => ({ err }));定義 Promise 。
const delay = (timeout) => {return new Promise(resolve => {setTimeout(resolve, timeout);}); };delay(1000).then(_ => {console.log('executed'); });Generators
dva 的 effects 是通過 generator 組織的。Generator 返回的是迭代器,通過?yield?關鍵字實現暫停功能。
這是一個典型的 dva effect,通過?yield?把異步邏輯通過同步的方式組織起來。
app.model({namespace: 'todos',effects: {*addRemote({ payload: todo }, { put, call }) {yield call(addTodo, todo);yield put({ type: 'add', payload: todo });},}, });React Component
Stateless Functional Components
React Component 有 3 種定義方式,分別是?React.createClass,?class?和?Stateless Functional Component。推薦盡量使用最后一種,保持簡潔和無狀態。這是函數,不是 Object,沒有?this?作用域,是 pure function。
比如定義 App Component 。
function App(props) {function handleClick() {props.dispatch({ type: 'app/create' });}return <div onClick={handleClick}>${props.name}</div> }等同于:
class App extends React.Component {handleClick() {this.props.dispatch({ type: 'app/create' });}render() {return <div onClick={this.handleClick.bind(this)}>${this.props.name}</div>} }JSX
Component 嵌套
類似 HTML,JSX 里可以給組件添加子組件。
<App><Header /><MainContent /><Footer /> </App>className
class?是保留詞,所以添加樣式時,需用?className?代替?class?。
<h1 className="fancy">Hello dva</h1>JavaScript 表達式
JavaScript 表達式需要用?{}?括起來,會執行并返回結果。
比如:
<h1>{ this.props.title }</h1>Mapping Arrays to JSX
可以把數組映射為 JSX 元素列表。
<ul>{ this.props.todos.map((todo, i) => <li key={i}>{todo}</li>) } </ul>注釋
盡量別用?//?做單行注釋。
<h1>{/* multiline comment */}{/*multilinecomment*/}{// single line}Hello </h1>Spread Attributes
這是 JSX 從 ECMAScript6 借鑒過來的很有用的特性,用于擴充組件 props 。
比如:
const attrs = {href: 'http://example.org',target: '_blank', }; <a {...attrs}>Hello</a>等同于
const attrs = {href: 'http://example.org',target: '_blank', }; <a href={attrs.href} target={attrs.target}>Hello</a>Props
數據處理在 React 中是非常重要的概念之一,分別可以通過 props, state 和 context 來處理數據。而在 dva 應用里,你只需關心 props 。
propTypes
JavaScript 是弱類型語言,所以請盡量聲明 propTypes 對 props 進行校驗,以減少不必要的問題。
function App(props) {return <div>{props.name}</div>; } App.propTypes = {name: React.PropTypes.string.isRequired, };內置的 prop type 有:
- PropTypes.array
- PropTypes.bool
- PropTypes.func
- PropTypes.number
- PropTypes.object
- PropTypes.string
往下傳數據
往上傳數據
CSS Modules
理解 CSS Modules
一張圖理解 CSS Modules 的工作原理:
button?class 在構建之后會被重命名為?ProductList_button_1FU0u?。button?是 local name,而?ProductList_button_1FU0u?是 global name 。你可以用簡短的描述性名字,而不需要關心命名沖突問題。
然后你要做的全部事情就是在 css/less 文件里寫?.button {...},并在組件里通過?styles.button?來引用他。
定義全局 CSS
CSS Modules 默認是局部作用域的,想要聲明一個全局規則,可用?:global?語法。
比如:
.title {color: red; } :global(.title) {color: green; }然后在引用的時候:
<App className={styles.title} /> // red <App className="title" /> // greenclassnames?Package
在一些復雜的場景中,一個元素可能對應多個 className,而每個 className 又基于一些條件來決定是否出現。這時,classnames?這個庫就非常有用。
import classnames from 'classnames'; const App = (props) => {const cls = classnames({btn: true,btnLarge: props.type === 'submit',btnSmall: props.type === 'edit',});return <div className={ cls } />; }這樣,傳入不同的 type 給 App 組件,就會返回不同的 className 組合:
<App type="submit" /> // btn btnLarge <App type="edit" /> // btn btnSmallReducer
reducer 是一個函數,接受 state 和 action,返回老的或新的 state 。即:(state, action) => state
增刪改
以 todos 為例。
app.model({namespace: 'todos',state: [],reducers: {add(state, { payload: todo }) {return state.concat(todo);},remove(state, { payload: id }) {return state.filter(todo => todo.id !== id);},update(state, { payload: updatedTodo }) {return state.map(todo => {if (todo.id === updatedTodo.id) {return { ...todo, ...updatedTodo };} else {return todo;}});},}, };嵌套數據的增刪改
建議最多一層嵌套,以保持 state 的扁平化,深層嵌套會讓 reducer 很難寫和難以維護。
app.model({namespace: 'app',state: {todos: [],loading: false,},reducers: {add(state, { payload: todo }) {const todos = state.todos.concat(todo);return { ...state, todos };},}, });下面是深層嵌套的例子,應盡量避免。
app.model({namespace: 'app',state: {a: {b: {todos: [],loading: false,},},},reducers: {add(state, { payload: todo }) {const todos = state.a.b.todos.concat(todo);const b = { ...state.a.b, todos };const a = { ...state.a, b };return { ...state, a };},}, });Effect
示例:
app.model({namespace: 'todos',effects: {*addRemote({ payload: todo }, { put, call }) {yield call(addTodo, todo);yield put({ type: 'add', payload: todo });},}, });Effects
put
用于觸發 action 。
yield put({ type: 'todos/add', payload: 'Learn Dva' });call
用于調用異步邏輯,支持 promise 。
const result = yield call(fetch, '/todos');select
用于從 state 里獲取數據。
const todos = yield select(state => state.todos);錯誤處理
全局錯誤處理
dva 里,effects 和 subscriptions 的拋錯全部會走?onError?hook,所以可以在?onError?里統一處理錯誤。
const app = dva({onError(e, dispatch) {console.log(e.message);}, });然后 effects 里的拋錯和 reject 的 promise 就都會被捕獲到了。
本地錯誤處理
如果需要對某些 effects 的錯誤進行特殊處理,需要在 effect 內部加?try catch?。
app.model({effects: {*addRemote() {try {// Your Code Here} catch(e) {console.log(e.message);}},}, });異步請求
異步請求基于 whatwg-fetch,API 詳見:https://github.com/github/fetch
GET 和 POST
import request from '../util/request';// GET request('/api/todos');// POST request('/api/todos', {method: 'POST',body: JSON.stringify({ a: 1 }), });統一錯誤處理
加入約定后臺返回以下格式時,做統一的錯誤處理。
{status: 'error',message: '', }編輯?utils/request.js,加入以下中間件:
function parseErrorMessage({ data }) {const { status, message } = data;if (status === 'error') {throw new Error(message);}return { data }; }然后,這類錯誤就會走到?onError?hook 里。
Subscription
subscriptions?是訂閱,用于訂閱一個數據源,然后根據需要 dispatch 相應的 action。數據源可以是當前的時間、服務器的 websocket 連接、keyboard 輸入、geolocation 變化、history 路由變化等等。格式為?({ dispatch, history }) => unsubscribe?。
異步數據初始化
比如:當用戶進入?/users?頁面時,觸發 action?users/fetch?加載用戶數據。
app.model({subscriptions: {setup({ dispatch, history }) {history.listen(({ pathname }) => {if (pathname === '/users') {dispatch({type: 'users/fetch',});}});},}, });path-to-regexp?Package
如果 url 規則比較復雜,比如?/users/:userId/search,那么匹配和 userId 的獲取都會比較麻煩。這是推薦用?path-to-regexp?簡化這部分邏輯。
import pathToRegexp from 'path-to-regexp';// in subscription const match = pathToRegexp('/users/:userId/search').exec(pathname); if (match) {const userId = match[1];// dispatch action with userId }Router
Config with JSX Element (router.js)
<Route path="/" component={App}><Route path="accounts" component={Accounts}/><Route path="statements" component={Statements}/> </Route>詳見:react-router
Route Components
Route Components 是指?./src/routes/?目錄下的文件,他們是?./src/router.js?里匹配的 Component。
通過 connect 綁定數據
比如:
import { connect } from 'dva'; function App() {}function mapStateToProps(state, ownProps) {return {users: state.users,}; } export default connect(mapStateToProps)(App);然后在 App 里就有了?dispatch?和?users?兩個屬性。
Injected Props (e.g. location)
Route Component 會有額外的 props 用以獲取路由信息。
- location
- params
- children
更多詳見:react-router
基于 action 進行頁面跳轉
import { routerRedux } from 'dva/router';// Inside Effects yield put(routerRedux.push('/logout'));// Outside Effects dispatch(routerRedux.push('/logout'));// With query routerRedux.push({pathname: '/logout',query: {page: 2,}, });除?push(location)?外還有更多方法,詳見?react-router-redux
dva 配置
Redux Middleware
比如要添加 redux-logger 中間件:
import createLogger from 'redux-logger'; const app = dva({onAction: createLogger(), });注:onAction 支持數組,可同時傳入多個中間件。
history
切換 history 為 browserHistory
import { browserHistory } from 'dva/router'; const app = dva({history: browserHistory, });去除 hashHistory 下的 _k 查詢參數
import { useRouterHistory } from 'dva/router'; import { createHashHistory } from 'history'; const app = dva({history: useRouterHistory(createHashHistory)({ queryKey: false }), });工具
通過 dva-cli 創建項目
先安裝 dva-cli 。
$ npm install dva-cli -g然后創建項目。
$ dva new myapp最后,進入目錄并啟動。
$ cd myapp $ npm start總結
- 上一篇: redux-saga 实践总结
- 下一篇: SpringBoot非官方教程 | 第一