前端笔记之React(七)redux-saga&Dva&路由
一、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/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代:
5代:
还可以不用官方这个路由,用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))}}
推荐阅读
-
前端笔记知识点整合之JavaScript(七)深入函数&DOM那点事
-
前端笔记之React(八)上传&图片裁切
-
前端笔记之NodeJS(二)路由&REPL&模块系统&npm
-
前端笔记之Vue(七)Vue-router&axios&Vue插件&Mock.js&cookie|session&加密
-
前端笔记之React(七)redux-saga&Dva&路由
-
前端笔记之React(六)ES6的Set和Map&immutable和Ramda和lodash&redux-thunk
-
前端笔记之React(五)Redux深入浅出
-
前端笔记之React(一)初识React&组件&JSX语法
-
前端笔记之React(八)上传&图片裁切
-
前端笔记之React(二)组件内部State&React实战&表单元素的受控