一、redux-saga解決非同步 redux-thunk 和 redux-saga 使用redux它們是必選的,二選一,它們兩個都可以很好的實現一些複雜情況下redux,本質都是為瞭解決非同步action而生,使redux保持完整性,不至於太過混亂。redux-saga 是一個用於管理Redux 應用 ...
一、redux-saga解決非同步
redux-thunk 和 redux-saga
使用redux它們是必選的,二選一,它們兩個都可以很好的實現一些複雜情況下redux,本質都是為瞭解決非同步action而生,使redux保持完整性,不至於太過混亂。redux-saga 是一個用於管理Redux 應用非同步操作的中間件。 redux-saga 通過創建 Sagas將所有的非同步操作邏輯收集在一個地方集中處理,可以用來代替 redux-thunk 中間件。而且提供了takeLatest/takeEvery可以對事件的僅關註最近事件、關註每一次、事件限頻;reudx-saga更方便測試,等等太多了。
npm install --save redux-saga
https://redux-saga-in-chinese.js.org/
新手教學:
https://redux-saga-in-chinese.js.org/docs/introduction/BeginnerTutorial.html
saga就是攔截action,進行非同步請求,轉發action,再去reducer進行處理請求
在app根目錄創建sagas.js文件,和main.js同級
// ES6的新特性,叫做Generator,產生器 // 是一種可以被調用的時候加next()打斷點的特殊函數 export function* helloSaga() { console.log('Hello Sagas!'); }示例代碼
main.js:
import React from "react"; import ReactDOM from "react-dom"; import { createStore, applyMiddleware} from "redux"; import {Provider} from "react-redux"; import createSagaMiddleware from 'redux-saga'; import App from "./containers/App.js"; import reducer from "./reducers"; //引入sages文件 import { helloSaga } from './sagas.js' //創建saga中間件 const sagaMiddleware = createSagaMiddleware(helloSaga) const store = createStore(reducer, applyMiddleware(sagaMiddleware)); //運行 sagaMiddleware.run(helloSaga) ReactDOM.render( <Provider store={store}> <App></App> </Provider>, document.getElementById("app") );
components/counter/index.js寫一個按鈕:
<button onClick={()=>{this.props.counterActions.addServer()}}>加伺服器數據</button>
actions/counterActions.js文件中發出一個action叫ADDSERVER,但非同步還沒發出:
import { ADD, MINUS, ADDSERVER } from "../constants/COUNTER.js"; export const add = () => ({"type" : ADD}); export const minus = () => ({"type" : MINUS}); export const addServer = () => ({"type" : ADDSERVER});
constants/COUNTER.js
export const ADDSERVER = "ADDSERVER"
改變saga.js:saga開始工作,它要開始劫持監聽ADDSERVER
import { delay } from 'redux-saga' import { put, takeEvery , all} from 'redux-saga/effects' //worker Saga將執行非同步的ADDSERVER任務 function* addServerAsync(){ alert("我是工作saga") } //watcher Saga 創建了一個 Saga watchAddServer。用了redux-saga提供的輔助函數takeEvery,
用於監聽所有的 ADDSERVER action,併在 action 被匹配時執行addServerAsync任務。 function* watchAddServer(){ //takeEvery表示“使用每一個”,當有action的type是ADDSERVER時,就執addServerAsync函數 yield takeEvery('ADDSERVER', addServerAsync); } //向外暴露一個預設的rootSaga,有一個設置的監聽隊列 export default function* rootSaga(){ //創建一系列的監聽隊列 yield all([watchAddServer()]) }
改變main.js:
import React from "react"; import ReactDOM from "react-dom"; import {createStore , applyMiddleware} from "redux"; import {Provider} from "react-redux"; import logger from "redux-logger"; import createSagaMiddleware from 'redux-saga'; import reducer from "./reducers/index.js"; import App from "./containers/App.js"; //引入sagas文件 import rootSaga from './sagas.js'; //創建saga中間件 const sagaMiddleware = createSagaMiddleware(); const store = createStore(reducer, applyMiddleware(logger, sagaMiddleware)); sagaMiddleware.run(rootSaga); ReactDOM.render( <Provider store={store}> <App></App> </Provider> , document.getElementById("app") );
sagas.js 請求伺服器的具體語句,寫在worker saga中。
function* addServerAsync() {
//拉取數據
const {result} = yield fetch("/api").then(data=>data.json());
//put就是發出(轉發)action,action的type為了避諱,加_SYNC尾碼
yield put({ "type": "ADDSERVER_SYNC" , result});
}
reducers/counter.js
export default (state = {"v" : 0} , action) => { if(action.type == "ADD"){ ... }else if(action.type == "MINUS"){ ... }else if(action.type == "ADDSERVER_SYNC"){ return { "v": state.v + action.result } } return state; }
saga就是攔截action,進行非同步請求,轉發action,再去reducer進行處理請求
刪除actions、constants文件夾,然後給reducers/counter.js的type都加上引號!
改counter/index.js,不用bindActionCreators了,直接發出action。
import React from 'react'; import {connect} from "react-redux"; class Counter extends React.Component { constructor(props) { super(props); } render() { return ( <div> <h1>Counter : {this.props.v}</h1> <button onClick={()=>{this.props.add()}}>增加</button> <button onClick={()=>{this.props.minus()}}>減少</button> <button onClick={()=>{this.props.addServer()}}>加伺服器數據</button> </div> ); } } export default connect( ({counter}) => ({ v : counter.v }), (dispatch) => ({ add(){ dispatch({"type" : "ADD"}); }, minus() { dispatch({"type": "MINUS" }); }, addServer() { dispatch({"type": "ADDSERVER" }); } }) )(Counter);
加入pie圖表組件
components/pie/index.js組件,記得在App.js引入組件
import React from 'react'; import {connect} from "react-redux"; class Pie extends React.Component { constructor(props) { super(props); //請求數據 props.loadServerData(); } //組件已經上樹 componentDidMount(){ //echarts是引入的百度的包提供的全局變數 this.pic = echarts.init(this.refs.pic); } componentWillUpdate(nextProps){ var option = { ... }; //設置option組件就能顯示了! this.pic.setOption(option); } render() { return ( <div> <h1>我是pie組件!</h1> <div ref="pic" style={{"width":"300px" ,"height":"300px"}}></div> <button onClick={()=>{this.props.toupiao('a')}}>清晰</button> <button onClick={()=>{this.props.toupiao('b')}}>一般</button> <button onClick={()=>{this.props.toupiao('c')}}>懵逼</button> </div> ); } } export default connect( ({pie})=>({ result: pie.result }), (dispatch)=>({ loadServerData(){ dispatch({ "type": "LOADSERVERDATA"}) }, toupiao(zimu){ dispatch({ "type": "TOUPIAO" , zimu}) } }) )(Pie);
reducers/pie.js,在reducers/index.js中引入
export default (state = {"result" : []} , action) => { if (action.type == "LOADSERVERDATA_SYNC"){ return { ...state , result : action.result } } return state; }
sagas.js
import { delay } from 'redux-saga'; import { put, takeEvery , all} from 'redux-saga/effects'; //worker Saga function* addServer() { //拉取數據,轉發action const {result} = yield fetch("/api").then(data=>data.json()); yield put({ "type": "ADDSERVER_SYNC" , result}); } function* loadServerData() { const {result} = yield fetch("/api2").then(data=>data.json()); yield put({ "type": "LOADSERVERDATA_SYNC", result }); } function* toupiao(action) { const {result} = yield fetch("/toupiao/" + action.zimu).then(data=>data.json()); yield put({ "type": "LOADSERVERDATA_SYNC", result }); } // watcher Saga function* watchAddServer() { yield takeEvery('ADDSERVER', addServer); } function* watchLoadServerData() { yield takeEvery('LOADSERVERDATA', loadServerData); } function* watchToupiao() { yield takeEvery('TOUPIAO', toupiao); } //向外暴露一個預設的rootSaga,有一個all設置的監聽隊列 export default function* rootSaga(){ //創建一系列的監聽隊列 yield all([watchAddServer(), watchLoadServerData() , watchToupiao()]) }
二、Dva簡介和配置
2.1 Dva簡介
文檔:https://github.com/dvajs/dva/blob/master/README_zh-CN.md
React繁文縟節很多,為什麼?
因為React將註意力放到了組件開發上,可以用class App extends React.Component類,它就是一個組件了,可以被任意插拔,<App></App>。
可被預測狀態容器Redux感覺在React“之外”,不“渾然一體”。
比如要配置Redux:
…… var {createStore} from "redux"; …… const store = createStore(reducer); …… <Provider store={store}> <App></App> </Provider> 組件中: connect()(App);示例代碼
如果要使用非同步,更感覺是在React“之外”,不“渾然一體”。
…… var {createStore} from "redux"; …… const store = createStore(reducer , applyMiddleware(saga)); run(rootSaga); …… <Provider store={store}> <App></App> </Provider>示例代碼
這是因為React將註意力放到了組件開發上。
Vue框架要比React好很多,渾然一體方面簡直無敵。
Vue天生帶有Vuex,天生就可以有可被預測狀態容器,有非同步解決的方案。
阿裡巴巴的雲謙,發明瞭dvajs這個庫,“集大成”者,本質的目的就是讓程式“渾然一體”,一方面是方便起步,更大的發明利於團隊組件開發,不用頻繁在組件文件actions、saga、reducers文件之間進行切換了。
2.2 Hello World
npm install --save dva
dva中集成了redux、react-redux、redux-saga、react-router-redux,所以我們的項目裝dva之前,要去掉這4個依賴。
註意:現在的package.js文件沒有以上這些依賴:
{ "name": "react_study", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "babel-core": "^6.26.3", "babel-loader": "^7.1.5", "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.7.0", "babel-preset-react": "^6.24.1" }, "dependencies": { "dva": "^2.4.0", "express": "^4.16.3", "react": "^16.4.2", "react-dom": "^16.4.2", "redux-logger": "^3.0.6" } }
後端app.js提供靜態化路由:
var express = require("express"); var app = express(); app.use(express.static("www")) app.get("/api",(req,res)=>{ res.json({"result":10}) }) app.listen(3000);示例代碼
main.js
import dva from 'dva'; //引入路由 import router from './router.js'; //創建app應用 const app = dva(); //註冊視圖,創建路由 app.router(router) //啟動應用 上樹運行 app.start('#app');示例代碼
router.js是路由,單頁面應用的hash路由,router.js文件暫時不寫任何路由功能,只返回一個標簽。
import React from 'react'; export default ()=>{ return <h1>我是dva</h1> }
2.3使用組件
組件沒有任何的簡化,還是原來的寫法:
components/App.js
import React from 'react' export default class App extends React.Component { constructor(){ super() } render() { return <div> <h1>我是App組件</h1> </div> } }示例代碼
在router.js(路由文件)引入App組件,表示一上來就掛載App組件:
import React from 'react'; import App from './components/App'; export default ()=>{ return <App></App> }示例代碼
2.4使用redux,創建model,組件要問天要數據
這裡非常大的改變,照著Vuex改的
註意:不是創建reducers文件夾,而是創建models文件夾,創建counter.js
export default { "namespace":"counter", //命名空間 "state":{ a : 100 } }示例代碼
內部,dva會自動將counter作為一個reducer的名字,進行combineReducers
main.js
import React from 'react'; import dva from 'dva'; // 引入路由 import router from './router.js'; import counter from './model/counter.js'; //創建app const app = dva(); //創建路由 app.router(router) //創建並使用全局數據 app.model(counter) //上樹運行 app.start('#app');
組件沒有任何變化,只不過connect要從dva中引包。
2.5改變天上數據
dispatch()沒有改變
model文件counter.js創建一個屬性叫reducers,裡面放一個add函數,依然是純函數,不能改變state,只能返回新的state。
export default { "namespace" : "counter", "state" : { "a" : 100 }, "reducers" : { add(state,action){ return { "a" : state.a + action.n } } } }
2.6非同步
改變model中counter.js文件,剛纔已經有了reducers函數,那裡是寫同步的函數。
現在要創建一個effects函數,這裡寫非同步的函數,很像vuex!!
export default { "namespace" : "counter" , "state" : { "a" : 100 }, "reducers" : { //這裡寫同步函數 add(state , action){ return { "a": state.a + action.n} } }, "effects" : { //這裡寫非同步函數 *addServer(action , {put}){ const {result} = yield fetch("/api").then(data=>data.json()); //非同步終點一定是某個同步函數,所以put重新觸發某個reducers中的同步函數 //帶著載荷參數,去改變state的數據 yield put({"type" : "add" , "n" : result}) } } }
組件的按鈕沒有變化:
2.7使用logger插件
import React from 'react'; import dva from 'dva'; import logger from 'redux-logger'; //引入路由 import router from './router.js'; import counter from './models/counter.js'; // 創建app const app = dva({ onAction: logger}); //創建路由 app.router(router); //創建並使用模型 app.model(counter); //上樹運行 app.start("#app");
三、React路由
3.1概述
每個公司用的路由都不一樣,這裡不一樣是很不一樣。
dva中依賴react-router-redux,沒有改變任何語法
但是react-router-redux出了5代版本,它的2、4、5都特別不一樣。
參考文章:
2代:http://www.ruanyifeng.com/blog/2016/05/react_router.html?utm_source=tool.lu
5代:https://blog.csdn.net/isaisai/article/details/78086913
還可以不用官方這個路由,用UI-Router
更可以不用單頁面路由,用伺服器後端的路由
我們介紹的是單頁面的,dva2.3中的react-router-redux 5代的路由寫法。
3.2基本使用
react-router-redux是單層級路由,路由的描述,沒有任何嵌套
直接路由到最內層組件。
main.js
import React from 'react'; import dva from 'dva'; import logger from 'redux-logger'; // 引入路由 import router from './router.js'; //創建app const app = dva({ onAction: logger}); //創建路由 app.router(router) //創建並使用數據模型 app.model(counter) //上樹運行 app.start('#app');
app/router.js路由文件:
import React from 'react'; import { Router, Switch, Route } from 'dva/router'; import Index from "./components/index.js"; import CarList from "./components/carlist/index.js"; import UserList from "./components/userlist/index.js"; export default ({ history, app }) => { return <Router history={history}> <Switch> <Route exact path="/" component={Index} /> <Route exact path="/carlist" component={CarList} /> <Route exact path="/userlist" component={UserList} /> </Switch> </Router> }
這個路由是一個被動的寫法:子組件聲明我被誰嵌套,而不是父組件聲明我嵌套誰。
最內層組件,比如CarList組件,寫法:
import React from 'react'; import {connect} from "dva"; import App from "../../containers/App.js"; export default class CarList extends React.Component { constructor(props) { super(props); } render() { return ( <App menu="汽車"> <div> <h1>我是Carlist組件</h1> </div> </App> ); } }
App.js
import React from 'react'; import { connect } from 'dva'; import { Layout, Menu, Breadcrumb, Icon } from 'antd'; import { push } from 'react-router-redux'; const { SubMenu } = Menu; const { Header, Content, Sider } = Layout; class App extends React.Component { constructor() { super() } render() { return <div> <Layout> <Header className="header"> <div className="logo" /> <Menu theme="dark" mode="horizontal" defaultSelectedKeys={[this.props.menu]} style={{ lineHeight: '64px' }} onClick={(e) => { this.props.dispatch(push(e.item.props.url))}} > <Menu.Item key="首頁" url="/">首頁</Menu.Item> <Menu.Item key="汽車" url="/carlist">汽車</Menu.Item> <Menu.Item key="用戶" url="/userlist">用戶</Menu.Item> </Menu> </Header> <Layout> <Layout style={{ padding: '0 24px 24px' }}> <Breadcrumb style={{ margin: '16px 0' }}> <Breadcrumb.Item>Home</Breadcrumb.Item> <Breadcrumb.Item>List</Breadcrumb.Item> <Breadcrumb.Item>App</Breadcrumb.Item> </Breadcrumb> <Content style={{ background: '#fff', padding: 24, margin: 0, minHeight: 280 }}> {this.props.children} </Content> </Layout> </Layout> </Layout>, </div> } } export default connect()(App);
路由跳轉:
onClick={(e)=>{this.props.dispatch(push(e.item.props.url))}}