本项目完整github地址
一、redux
实践
在介绍dva
之前,我们先介绍一下redux
, dva
是一个基于redux
和redux-saga
的数据流方案。然后为了简化开发体验,dva
还额外内置了 react-router
和 fetch
,所以也可以理解为一个轻量级的应用框架。
1.为何使用redux?
随着 JavaScript
单页应用开发日趋复杂,JavaScript
需要管理比任何时候都要多的 state
(状态)。 这些 state
可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据
,也包括UI
状态,如激活的路由
,被选中的标签
,是否显示加载动效或者分页器
等等。管理不断变化的 state
非常困难。如果一个model
的变化会引起另一个 model
变化,那么当view
变化时,就可能引起对应 model
以及另一个 model
的变化,依次地,可能会引起另一个 view
的变化。直至你搞不清楚到底发生了什么。state
在什么时候,由于什么原因,如何变化已然不受控制。
使用Redux前:各个来源的数据将改变state,而state之间又相互影响,进而影响页面,这样会产生不可预估的影响。
使用Redux后:页面触发一个dispatch方法,传入action,通过传入的action来改变reducer对应的store,通过返回新的store改变页面,这样写有一个好处就是改变state,也就是redux中的store只有唯一的途径,单向数据流保证数据的流向唯一
2.页面搭建
安装对应依赖包
//src/index.js程序入口页面
import React from 'react';
import ReactDom from 'react-dom';
import ReduxClass from './routes/ReduxClass';
ReactDom.render(<ReduxClass />,document.getElementById("root"));
//routes文件夹放置我们的路由页面
//src/routes/ReduxClass页面
import React from 'react';
class ReduxClass extends React.Component {
render() {
return (
<div>
<input />
</div>
)
}
}
export default ReduxClass;
实现效果如下:
只有一个最简单的input
框,我们输入一下,发现值是有变化的。
3.可控
组件和不可控
组件
网上关于可控组件的文章有很多,这里不详细细说。只有一点。可控组件的值是由state
来控制的。如下:
import React from 'react';
class ReduxClass extends React.Component {
constructor(props){
super(props);
this.state={
inputValue:"aaa"
}
}
handleChangeInput=(e)=>{
this.setState({
inputValue:e.target.value
})
}
render() {
return (
<div>
<input value={this.state.inputValue} onChange={this.handleChangeInput}/>
</div>
)
}
}
export default ReduxClass;
4.获取redux
中的state
上例子中我们使用state
管控了一个可控的input
,接下来我们将class
组件中的state
提升至redux
中进行管理。
首先新建redux.js
文件。
//安装redux.js
npm install redux --save-dev
//store/redux.js页面
import { createStore } from 'redux';
//定义一个初始化的state redux三要素之一store
const defaultState={
inputValue:"initial-redux"
}
//actions的类型 redux三要素之一action
const actionsType={
INPUT_CHANGE:"INPUT_CHANGE"
}
//reducer定义处理数据的逻辑 redux三要素之一reducer
function inputReducer(state,action){
switch(action.type){
case actionsType.INPUT_CHANGE:
const newState=state;
newState.inputValue=action.value;
return newState;
default :
return state;
}
}
let store=createStore(inputReducer,defaultState);
export {
store,
actionsType
}
createStore
:传入一个reducer
和初始化的state
,返回一个实例对象。
这个对象有三个方法:
getState()
:无需传参,获取state
。dispatch(action)
:参数为一个action
,action
实际上就是一个普通的js对象,用于更新state
。subscribe(listener)
:参数为一个函数,当state
改变时会触发这个函数。
上文中的defaultState
是redux
中的store
,actionsTypes
是redux
的action
的类型,inputReducer
是redux
的reducer
,他是一个纯函数,传入state
和action
。我们在createStore
方法中传入inputReducer
,返回store
实例。我们将store
和actionTypes
导出。
//我们将store实例导入
import { store } from './../store/redux';
//将redux中的state放进管控input的受控state里面,这样input就变成了一个受控组件
constructor(props){
super(props);
let reduxState=store.getState();
this.state={
inputValue:reduxState.inputValue
}
}
handleChangeInput=(e)=>{
let reduxState=store.getState();
this.setState({
inputValue:reduxState.inputValue
})
}
可以查看下效果,redux state
里面的inputValue
的值已经移进输入框内了。
我们可以实现一个createStore
函数,这个函数的目的
- 传入一个
reducer
,一个state
,返回一个对象。- 对象有一个
getState
方法,可以获取全部的state
。
创建一个utils/redux.js
文件:
const initialState={
inputValue:"initial-value-i"
}
const inputReducer=(state,action)=>{
return state;
}
function createStore(reducer,initialState){
const state=initialState;
function getState(){
return state;
}
const store={
getState
}
return store;
}
let store=createStore(inputReducer,initialState);
export {
store
}
上面我们创造了一个createStore
方法,传入reducer
、初始化state
,并返回一个对象,包含一个getState
方法。
//真实的redux
import { store } from './../store/redux';
//为了避免命名冲突,as 可以将导入的名字转成另一个名字
import { store as storeI } from './../utils/redux';
constructor(props){
super(props);
let reduxState=store.getState();
let reduxStateI= storeI.getState();
//打印出reduxStateI,可以看出initialValue的值
console.log(reduxStateI)
this.state={
inputValue:reduxState.inputValue
}
}
到这里,
getState
方法算是已经实现了,可以实现获取state
的功能。
5.更新state
接着上次那个例子,我们在输入框输入,发现并无任何反应,我们查看input
的onChange
事件,每次修改都会注入redux
的state
,但是我们redux
中的state
是不会变化的,所以输入框就没有变化,这下子我们就要研究如何更新state
。我们将utils/redux.js
中的inputRedux
。
function inputReducer(state,action){
switch(action.type){
case actionsType.INPUT_CHANGE:
const newState=state;
newState.inputValue=action.inputValue;
return newState;
default :
return state;
}
}
然后在组件的change
事件中加入dispatch
来改变redux
的state
。
handleChangeInput=(e)=>{
let reduxState=store.getState();
store.dispatch({
type:actionsType.INPUT_CHANGE,
inputValue:e.target.value
})
this.setState({
inputValue:reduxState.inputValue
})
}
这里我们可以看到dispatch
函数的参数action
,可以看出action
是一个js对象
。这个action
就对应reducer
的第二个参数。可以改变state
。
{
type:actionTypes.INPUT_CHANGE,
inputValue:e.target.value
}
我们输入框输些内容,发现内容确实是有变化,我们改变state
确实成功了。接下来我们来实现这个dispatch
,上节课我们的reducer
定义了,但是并没有使用,这节课我们来进行使用。
function createStore(reducer,initialState){
let state=initialState;
function dispatch(action){
state=reducer(initialState,action);
}
function getState(){
return state;
}
const store={
getState,
dispatch
}
return store;
}
我们将reducer
传到dispatch
的方法里面计算出state
。
这样我们就算是完成了
dispatch
方法来改变redux
的state
了
6.实现监听方法
在store
中还有一个重要的方法:
subscribe
方法:这个方法是在state
发生改变的时候,执行一个回调,触发方法。
store.subscribe(()=>{
console.log("state已经变化,变化后的state为:"+reduxState.inputValue)
})
当我们input
框输入值时,可以在控制台打印出输出。
接下来我们可以实现一个subscribe
。
let listeners=[];
function subscribe(listener){
listener.push(listener);
}
function dispatch(action){
state=reducer(initialState,action);
for(let i=0;i<listeners.length;i++){
const listener=listeners[i];
listener();
}
}
上面这是著名的
发布-订阅
模式。发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知
。本次当state
发生改变,注册的监听
会执行一遍。
这算是实现了
redux
的subscribe
方法。
7.尝试合并reducer
我们尝试增加一个redux
的state
值,来初始化文字。
//文字
<p>文字:{text}</p>
<button onClick={this.handleChangeText}></button>
//change文字的方法
handleChangeText=()=>{
store.dispatch({
type:actionsType.TEXT_CHANGE,
textValue:"改变文字的值"
})
}
//store.redux定义action类型
//actions的类型 redux三要素之一action
const actionsType={
INPUT_CHANGE:"INPUT_CHANGE",
TEXT_CHANGE:"TEXT_CHANGE"
}
//text的reducer
function textReducer(state,action){
switch(action.type){
case actionsType.TEXT_CHANGE:
const newState=state;
newState.textValue=action.textValue;
return newState;
default :
return state;
}
}
这次我们有了2个reducer
,但是我们的createStore
方法只有第一个参数是接受reducer
的,那我们有没有可能将2个reducer
合成一个reducer
呢?在redux
中就有这样一个方法。
//导入合并后的reducer
import { createStore,combineReducers } from 'redux';
//redux
let store=createStore(combineReducers({
input:inputReducer,
text:textReducer
}),defaultState);
//修改redux的值
import React from 'react';
import { store, actionsType } from './../store/redux';
import { store as storeI ,actionsType as actionsTypeI} from './../utils/redux';
class ReduxClass extends React.Component {
constructor(props){
super(props);
this.state={
inputValue:"redux-init",
textValue:"默认文字"
}
}
handleChangeInput=(e)=>{
store.dispatch({
type:actionsType.INPUT_CHANGE,
inputValue:e.target.value
})
this.setState({
inputValue:store.getState().input.inputValue
})
}
handleChangeText=()=>{
store.dispatch({
type:actionsType.TEXT_CHANGE,
textValue:"改变文字的值"
})
this.setState({
textValue:store.getState().text.textValue
})
}
render() {
const { inputValue,textValue }=this.state;
return (
<div>
<input value={inputValue} onChange={this.handleChangeInput}/>
<p>文字:{textValue}</p>
<button onClick={this.handleChangeText}>改变文字</button>
</div>
)
}
}
export default ReduxClass;
我们看到已经有了效果:
那么究竟这个合成reducer
的方法到底有什么魔力呢?我们一起来实现一下这个reducer
。
function combineReducers(reducers){
const reducerKeys=Object.keys(reducers);
return function(state={},action){
//生成新的state
const nextState={}
//遍历执行所有的reducers,整合成为一个你的state
for(let i=0;i<reducerKeys.length;i++){
const key=reducerKeys[i];
const reducer=reducers[key];
/**
* key对应的state
*/
const previousStateForKey=state[key]
const nextStateForKey=reducer(previousStateForKey,action)
nextState[key]=nextStateForKey
}
return nextState;
}
}
这下子
combineReducers
我们算是也实现好了。
8.回顾前章
在我们前面的章节中,我们实现了4个api
//完整
const initialState={
input:{
inputValue:"initial-redux"
},
text:{
textValue:"默认文字"
}
}
//actions的类型 redux三要素之一action
const actionsType={
INPUT_CHANGE:"INPUT_CHANGE",
TEXT_CHANGE:"TEXT_CHANGE"
}
function inputReducer(state,action){
switch(action.type){
case actionsType.INPUT_CHANGE:
const newState=state;
newState.inputValue=action.inputValue;
return newState;
default :
return state;
}
}
function textReducer(state,action){
switch(action.type){
case actionsType.TEXT_CHANGE:
const newState=state;
newState.textValue=action.textValue;
return newState;
default :
return state;
}
}
function combineReducers(reducers){
const reducerKeys=Object.keys(reducers);
return function(state={},action){
//生成新的state
const nextState={}
//遍历执行所有的reducers,整合成为一个你的state
for(let i=0;i<reducerKeys.length;i++){
const key=reducerKeys[i];
const reducer=reducers[key];
/**
* key对应的state
*/
const previousStateForKey=state[key]
const nextStateForKey=reducer(previousStateForKey,action)
nextState[key]=nextStateForKey
}
return nextState;
}
}
function createStore(reducer={},initialState){
let state=initialState;
let listeners=[];
function subscribe(listener){
listeners.push(listener);
}
function dispatch(action){
state=reducer(initialState,action);
for(let i=0;i<listeners.length;i++){
const listener=listeners[i];
listener();
}
}
function getState(){
return state;
}
const store={
getState,
dispatch,
subscribe
}
return store;
}
let store=createStore(combineReducers({
input:inputReducer,
text:textReducer
}),initialState);
export {
store,
actionsType
}
getState
用于获取到state
dispatch
派发action
,改变state
subscribe
注册监听监听state
的变化combineReducers
集合多个reducer
为一个reducer
在createStore
函数中加一行。
//dispatch触发一个不匹配action的值,来初始化state
dispatch({ type: Symbol() })
9.redux
中间件
Redux
本身就提供了非常强大的数据流管理功能,但这并不是它唯一的强大之处,它还提供了利用中间件来扩展自身功能,以满足用户的开发需求。 网上找的图,本人画图功底不厚,故找网图。
正常的没有中间件的流程
加入中间件的流程 在增加了中间件middleware
后,我们就可以在这途中对 action
进行截获,并进行改变。而触发actio
n的流程就是dispatch
,所以其实中间件就是重写dispatch
。
1.现在考虑一个业务需求,我们需要写一个中间件记录dispatch
前的state
,dispatch
后的state
。
let store=createStore(combineReducers({
input:inputReducer,
text:textReducer
}),defaultState);
const next = store.dispatch;
/**
* 重写action.dispatch
*/
store.dispatch=(action)=>{
console.log("this state",store.getState());
next(action);
console.log("next state",store.getState())
}
我们这里重写了dispatch
,并在中间件执行期间执行原来的dispatch
。
const next = store.dispatch;
/**
* 重写action.dispatch
*/
const loggerMiddleware=(action)=>{
console.log("this state",store.getState());
next(action);
console.log("next state",store.getState());
}
store.dispatch=(action)=>{
loggerMiddleware(action)
}
2.再写一个中间件
,来打印异常
const exceptionMiddleware = (action) => {
try {
next(action)
} catch (err) {
console.error('错误报告: ', err)
}
}
3.插件组合
现在我们想让处理异常的中间件中处理日志打印
const loggerMiddleware=(action)=>{
console.log("this state",store.getState());
next(action);
console.log("next state",store.getState());
}
const exceptionMiddleware = (action) => {
try {
loggerMiddleware(action)
} catch (err) {
console.error('错误报告: ', err)
}
}
store.dispatch=(action)=>{
exceptionMiddleware(action)
}
但是这样有一个问题,loggerMiddleware
是写死的,我们希望这个中间件是动态的,也可以传其他中间件进去,我们来改一下。
const loggerMiddleware=(action)=>{
console.log("this state",store.getState());
next(action);
console.log("next state",store.getState());
}
const exceptionMiddleware =(middle)=> (action) => {
try {
middle(action)
} catch (err) {
console.error('错误报告: ', err)
}
}
store.dispatch=exceptionMiddleware(loggerMiddleware)
同时我们也可以改变一下loggerMiddleware
让其支持动态传入。
const loggerMiddleware=(middle)=>(action)=>{
console.log("this state",store.getState());
middle(action);
console.log("next state",store.getState());
}
const exceptionMiddleware =(middle)=> (action) => {
try {
middle(action)
} catch (err) {
console.error('错误报告: ', err)
}
}
store.dispatch=exceptionMiddleware(loggerMiddleware(next))
4.中间件再抽离
let store=createStore(combineReducers({
input:inputReducer,
text:textReducer
}),initialState);
const next = store.dispatch;
/**
* 重写action.dispatch
*/
const loggerMiddleware=(store)=>(middle)=>(action)=>{
console.log("this state",store.getState());
middle(action);
console.log("next state",store.getState());
}
const exceptionMiddleware =(store)=>(middle)=> (action) => {
try {
middle(action)
} catch (err) {
console.error('错误报告: ', err)
}
}
let loggerMiddle=loggerMiddleware(store);
let exceptionMiddle=exceptionMiddleware(store);
store.dispatch=exceptionMiddle(loggerMiddle(next))
5.applyMiddleware
实现
但是中间件一多,这样看上去就不是很优雅
let loggerMiddle=loggerMiddleware(store);
let exceptionMiddle=exceptionMiddleware(store);
store.dispatch=exceptionMiddle(loggerMiddle(next))
applyMiddleware
可以让我们很优雅的应用多个中间件
/*接收旧的 createStore,返回新的 createStore*/
const newCreateStore = applyMiddleware(exceptionMiddleware, loggerMiddleware)(createStore);
/*返回了一个 dispatch 被重写过的 store*/
const store = newCreateStore(reducer);
那我们如何实现applyMiddleware呢?
function applyMiddleware(...middlewares) {
//返回一个重写createStore的方法
return function rewriteCreateStoreFunc(oldCreateStore) {
//返回一个重写新的createStore
return function newCreateStore(reducer, initState) {
//生成store
const store = oldCreateStore(reducer, initState);
/*给每个 middleware 传下store,相当于 const logger = loggerMiddleware(store);*/
/* const chain = [exception,logger]*/
const chain = middlewares.map(middleware => middleware(store));
let dispatch = store.dispatch;
/* 实现 exception(logger(dispatch))*/
chain.reverse().map(middleware => {
dispatch = middleware(dispatch);
});
/*2. 重写 dispatch*/
store.dispatch = dispatch;
return store;
}
}
}
现在
/*没有中间件的 createStore*/
import { createStore } from './redux';
const store = createStore(reducer, initState);
/*有中间件的 createStore*/
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware,loggerMiddleware);
const newCreateStore = rewriteCreateStoreFunc(createStore);
const store = newCreateStore(reducer, initState);
但是有了中间件代码会显得很臃肿,改良一下
function createStore(reducer = {}, initialState, rewriteCreateStoreFunc) {
/*如果有 rewriteCreateStoreFunc,那就采用新的 createStore */
if(rewriteCreateStoreFunc){
const newCreateStore = rewriteCreateStoreFunc(createStore);
return newCreateStore(reducer, initState);
}
/*****/
}
最终的用法
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware,loggerMiddleware);
const store = createStore(reducer, initState, rewriteCreateStoreFunc);
10.redux-saga
的使用
redux-saga
是用于维护redux
异步操作的状态的一个中间件实现,其中reducer
负责处理state
更新,sagas
负责协调异步操作。它提供了一系列的side-effects
方法,可以让用户很优雅的实现一些异步功能。 本文从源码出发,结合一个简单实现,探索工具的实现原理。
上节我们提到了redux
中间件,redux-saga
就是中间件中的其中一个,用于获取异步数据,原理就是重写了dispatch
。
我们尝试使用一下这个中间件。我们实现点击按钮获取数据进行渲染。
routes/ReduxClass.js
文件:
//ReduxClass.js组件层
//state
this.state={
...
dataValue:[]
}
//render层
render() {
const { ...,dataValue }=this.state;
return (
<div>
...
<button onClick={this.handleFetchData}>获取数据</button>
{
dataValue.map(item=><img width="100" height="100" key={item.id} src={item.author.avatar_url} />)
}
</div>
)
}
//获取数据按钮的点击事件,callback用于回调
handleFetchData=()=>{
storeI.dispatch({
type:actionsTypeI.FETCH_DATA,
callback:(res)=>{
this.setState({
dataValue:res
})
}
})
}
utils/redux.js
文件:
//导入redux-saga中间件
import createSagaMiddleware from 'redux-saga';
//导入effect的配置文件
import { watcher } from './effect';
//初始化data
const initialState = {
...
data:[]
}
//定义2个action用于获取异步数据和数据reducer
//actions的类型 redux三要素之一action
const actionsType = {
...
FETCH_DATA:"FETCH_DATA",
CONCAT_DATA:"CONCAT_DATA"
}
//data的reducer
function dataReducer(state, action) {
switch (action.type) {
case actionsType.FETCH_DATA:
return state;
case actionsType.CONCAT_DATA:
let newState=action.data;
return newState;
default:
return state;
}
}
//初始化saga中间件
const sagaMiddleware=createSagaMiddleware();
//合并中间件
const rewriteCreateStoreFunc = applyMiddleware(...,sagaMiddleware);
//传入datareducer
let store = createStore(combineReducers({
...
data:dataReducer
}), initialState,rewriteCreateStoreFunc);
//run启动中间件
sagaMiddleware.run(watcher);
utils/effects.js
import { call,put,takeEvery} from 'redux-saga/effects';
import { getlist } from '../services/index';
import { actionsType } from './redux';
/**
* 副作用处理effects
* 用于处理异步请求
*/
const effects={
*fetchData({payload,callback}){
const res=yield call(getlist,payload);
if(res.status===200){
yield put({
type:actionsType.CONCAT_DATA,
data:res.data.data,
})
callback(res.data.data)
}
}
}
/**
* 异步action监听
* dispatch对应的action时,调用对应的异步处理方式
*/
function* watcher(){
console.log("soga watcher");
yield takeEvery(actionsType.FETCH_DATA,effects.fetchData);
}
export {
watcher,
effects
}
services/index.js
import axios from 'axios';
const LIST="https://cnodejs.org/api/v1/topics";
async function getlist(){
return axios.get(LIST)
}
export {
getlist
}
我们来看一下执行流程:鼠标点击获取数据按钮
->进入handleFetchData方法
->执行对应的dispatch方法
->走进对应的reducer
->进入saga监听的action方法
->执行获取数据的方法
->获取数据以后执行回调渲染页面
takeEvery
一直监听某一个action
take
只监听一个action
put
触发一个action
call
阻塞调用一个函数,如一个Promise
方法
效果展示:
11.redux
退订
在前文中,我们实现了subscribe
方法达到state
修改,触发监听函数的效果,这个著名的发布订阅模式。那我们可以退订监听方法吗?
//退订方法
function unsubscribe(listener){
const index=listeners.indexOf(listener);
listeners.splice(index,1);
}
//使用
storeI.subscribe(function(){
console.log("subscribe....")
})
storeI.unsubscribe(function(){
console.log("unsubscribe....")
})
12.compose
实现
我们的 applyMiddleware
中,把 [A, B, C]
转换成 A(B(C(next)))
,是这样实现的
const chain = [A, B, C];
let dispatch = store.dispatch;
chain.reverse().map(middleware => {
dispatch = middleware(dispatch);
});
redux
提供了一个 compose
方式,可以帮我们做这个事情
const chain = [A, B, C];
dispatch = compose(...chain)(store.dispatch)
export default function compose(...funcs) {
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
12.replaceReducer
实现
在后面实现dva
时,我们需要使用这个api
,在一般的项目中,reducer
拆分后,和组件是一一对应的。我们就希望在做按需加载的时候,reducer
也可以跟着组件在必要的时候再加载,然后用新的 reducer
替换老的 reducer
。
function replaceReducer(nextReducer) {
reducer = nextReducer
/*刷新一遍 state 的值,新来的 reducer 把自己的默认状态放到 state 树上去*/
dispatch({ type: Symbol() })
}
在redux实现末尾,放出redux.js的完整文件
function compose(...funcs) {
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
function applyMiddleware(...middlewares) {
//返回一个重写createStore的方法
return function rewriteCreateStoreFunc(oldCreateStore) {
//返回一个重写新的createStore
return function newCreateStore(reducer, initState) {
//生成store
const store = oldCreateStore(reducer, initState);
/*给每个 middleware 传下store,相当于 const logger = loggerMiddleware(store);*/
/* const chain = [exception,logger]*/
/*const chain = middlewares.map(middleware => middleware(store));*/
const simpleStore={getState:store.getState,dispatch:store.dispatch};
const chain=middlewares.map(middleware=>middleware(simpleStore));
let dispatch = store.dispatch;
/* 实现 exception(logger(dispatch))*/
// chain.reverse().map(middleware => {
// dispatch = middleware(dispatch);
// });
dispatch = compose(...chain)(store.dispatch)
/*2. 重写 dispatch*/
store.dispatch = dispatch;
return store;
}
}
}
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
return function (state = {}, action) {
//生成新的state
const nextState = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
const reducer = reducers[key];
/**
* key对应的state
*/
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
}
return nextState;
}
}
function createStore(reducer = {}, initialState, rewriteCreateStoreFunc) {
if (typeof initialState === 'function'){
rewriteCreateStoreFunc = initialState;
initialState = undefined;
}
/*如果有 rewriteCreateStoreFunc,那就采用新的 createStore */
if(rewriteCreateStoreFunc){
const newCreateStore = rewriteCreateStoreFunc(createStore);
return newCreateStore(reducer, initialState);
}
let state = initialState;
let listeners = [];
function subscribe(listener) {
listeners.push(listener);
}
function replaceReducer(nextReducer) {
reducer = nextReducer
/*刷新一遍 state 的值,新来的 reducer 把自己的默认状态放到 state 树上去*/
dispatch({ type: Symbol() })
}
function unsubscribe(listener){
const index=listeners.indexOf(listener);
listeners.splice(index,1);
}
function dispatch(action) {
state = reducer(initialState, action);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
dispatch({ type: Symbol() })
function getState() {
return state;
}
const store = {
getState,
dispatch,
subscribe,
unsubscribe,
replaceReducer
}
return store;
}
export {
createStore,
combineReducers,
applyMiddleware
};
二、react-redux
实践
在上章中我们完成了redux
的部分api
,也使用我们的api
完成了一个很简单的小型demo
,但是我们回想一下我们以前在react
中使用redux
的时候,是不是这样?
<Provider store={store}>
<Router />
....
</Provider>
class demo extends ...{
...
}
const mapStateToProps=()=>({
...
})
const mapDispatchToProps=()=>({
...
})
export default connect(mapStateToProps,mapDispatchToProps)(demo);
Provider
和connect
就是本篇文章的重点,我们一步步来实现这个api
。
在上章中我们实现了这个demo
,我们将demo
进行目录化的拆分,在实际项目中我们肯定不止在一个页面里,所以我们引入react-router-dom
进行路由配置。
1.react-redux
入门
//src/index.js
//项目根目录文件
import React from 'react';
import ReactDom from 'react-dom';
import ReduxInputClass from './routes/ReduxInputClass';
import ReduxImgClass from './routes/ReduxImgClass';
import { Route , HashRouter } from 'react-router-dom';
ReactDom.render(
<HashRouter>
<Route path="/" component={ReduxInputClass}/>
<Route path="/img" component={ReduxImgClass}/>
</HashRouter>
,document.getElementById("root"));
//src/routes/ReduxImgClass
//img图片的页面
import React from 'react';
class ReduxImgClass extends React.Component {
constructor(props){
super(props);
this.state={
dataValue:[]
}
}
render() {
const { dataValue }=this.state;
return (
<div>
<button>获取数据</button>
{
dataValue.map(item=><img width="100" height="100" key={item.id} src={item.author.avatar_url} />)
}
</div>
)
}
}
export default ReduxImgClass;
//src/routes/ReduxInputClass
//带有Input框的页面
import React from 'react';
class ReduxInputClass extends React.Component {
constructor(props){
super(props);
this.state={
inputValue:""
}
}
render() {
const { inputValue }=this.state;
return (
<div>
<input value={inputValue} onChange={()=>{}} />
</div>
)
}
}
export default ReduxInputClass;
//redux入口文件 为了不与前章内容重合我们将文件写在model目录下
//src/model/index.js
import { createStore,combineReducers,applyMiddleware } from './redux';
import createSagaMiddleware from 'redux-saga';
import { watcher } from './effect';
import imgReducer from './reducer/imgReducer';
import inputReducer from './reducer/inputReducer';
let sagaMiddleware=createSagaMiddleware();
const store=createStore(combineReducers({
img:imgReducer,
input:inputReducer
}),
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(watcher);
export default store;
//由于项目中需要获取数据,我们引入redux-saga
//src/model/effect.js
import { call,put,takeEvery} from 'redux-saga/effects';
import { getlist } from '../services/index';
import { FETCH_DATA,CONCAT_DATA } from './action/imgAction';
/**
* 副作用处理effects
* 用于处理异步请求
*/
const effects={
*fetchData({payload,callback}){
const res=yield call(getlist,payload);
if(res.status===200){
yield put({
type:CONCAT_DATA,
data:res.data.data,
})
callback(res.data.data)
}
}
}
/**
* 异步action监听
* dispatch对应的action时,调用对应的异步处理方式
*/
function* watcher(){
console.log("初始化watcher");
yield takeEvery(FETCH_DATA,effects.fetchData);
}
export {
watcher,
effects
}
//使用我们上章实现的redux
//src/model/redux.js
function compose(...funcs) {
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
function applyMiddleware(...middlewares) {
//返回一个重写createStore的方法
return function rewriteCreateStoreFunc(oldCreateStore) {
//返回一个重写新的createStore
return function newCreateStore(reducer, initState) {
//生成store
const store = oldCreateStore(reducer, initState);
/*给每个 middleware 传下store,相当于 const logger = loggerMiddleware(store);*/
/* const chain = [exception,logger]*/
/*const chain = middlewares.map(middleware => middleware(store));*/
const simpleStore={getState:store.getState,dispatch:store.dispatch};
const chain=middlewares.map(middleware=>middleware(simpleStore));
let dispatch = store.dispatch;
/* 实现 exception(logger(dispatch))*/
// chain.reverse().map(middleware => {
// dispatch = middleware(dispatch);
// });
dispatch = compose(...chain)(store.dispatch)
/*2. 重写 dispatch*/
store.dispatch = dispatch;
return store;
}
}
}
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
return function (state = {}, action) {
//生成新的state
const nextState = {}
//遍历执行所有的reducers,整合成为一个你的state
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
const reducer = reducers[key];
/**
* key对应的state
*/
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
}
return nextState;
}
}
function createStore(reducer = {}, initialState, rewriteCreateStoreFunc) {
if (typeof initialState === 'function'){
rewriteCreateStoreFunc = initialState;
initialState = undefined;
}
/*如果有 rewriteCreateStoreFunc,那就采用新的 createStore */
if(rewriteCreateStoreFunc){
const newCreateStore = rewriteCreateStoreFunc(createStore);
return newCreateStore(reducer, initialState);
}
let state = initialState;
let listeners = [];
function subscribe(listener) {
listeners.push(listener);
}
function unsubscribe(listener){
const index=listeners.indexOf(listener);
listeners.splice(index,1);
}
function dispatch(action) {
state = reducer(initialState, action);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
dispatch({ type: Symbol() })
function getState() {
return state;
}
const store = {
getState,
dispatch,
subscribe,
unsubscribe
}
return store;
}
export {
createStore,
combineReducers,
applyMiddleware
};
//2个页面对应的action
//src/model/action/imgAction
export const FETCH_DATA="FETCH_DATA";
export const CONCAT_DATA="CONCAT_DATA";
//src/model/action/inputAction
export const INPUT_CHANGE="INPUT_CHANGE";
//2个页面对应的reducer
//src/model/reducer/imgReducer
import {FETCH_DATA,CONCAT_DATA} from './../action/imgAction'
const initialState = {
data:[]
}
export default function dataReducer(state=initialState, action) {
switch (action.type) {
case FETCH_DATA:
return state;
case CONCAT_DATA:
return action.data;
default:
return state;
}
}
//src/model/reducer/inputReducer
import {INPUT_CHANGE} from './../action/inputAction'
const initialState = {
inputValue: "initial-redux"
}
export default function inputReducer(state=initialState, action) {
switch (action.type) {
case INPUT_CHANGE:
const newState = state;
newState.inputValue = action.inputValue;
return newState;
default:
return state;
}
}
我们这里虽然引入了redux
,实际上并没有使用redux
,回顾之前我们使用redux
是引入store
然后通过store
的dispatch
来触发reducer
改变state
。那么在实际项目中,如果这么使用肯定不行,重复的引入store
,而且不够优雅。我们使用react-redux
。
//安装所需要的库
npm install react-redux
//通过Provider注入store
...
import store from './model/index';
import { Provider } from 'react-redux';
ReactDom.render(
<Provider store={store}>
<HashRouter>
<Route path="/" component={ReduxInputClass}/>
<Route path="/img" component={ReduxImgClass}/>
</HashRouter>
</Provider>
,document.getElementById("root"));
//引入connect函数包裹组件,传递store的属性给组件
import React from 'react';
import { connect } from 'react-redux';
class ReduxInputClass extends React.Component {
constructor(props){
super(props);
this.state={
inputValue:""
}
}
render() {
const { inputValue }=this.state;
console.log(this.props);
return (
<div>
<input value={inputValue} onChange={()=>{}} />
</div>
)
}
}
const mapStateToProps=({input})=>({
input
})
export default connect(mapStateToProps)(ReduxInputClass);
可以看出state
已经注入到了组件
2.connect
方法详解
上节课我们已经一回到了connect
的神奇作用了,这节课我们重点介绍一个connect
。
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
链接组件和数据,把
redux
中的数据放到组件的属性中
1.第一个参数[mapStateToProps(state, [ownProps]): stateProps] (Function)
如果定义该参数,组件将会监听
Redux store
的变化。任何时候,只要Redux store
发生改变,mapStateToProps
函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的props
合并。如果你省略了这个参数,你的组件将不会监听Redux store
。
state
:整个Redux store
的state
,它返回一个要作为props
传递的对象。ownProps
:这个组件自有的props
属性。
可以使用reselect
去有效地组合选择器和计算衍生数据。
//isInitial会传入组件的props里面
const mapStateToProps=({input},ownProps)=>({
input,
isInitial:input.inputValue==="initial-redux"
})
2.第二个参数[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function)
如果不传第二个参数,组件里面会默认传递
dispatch
,通过dispatch
可以触发reducer
。
dispatch:store
对象里面的dispatch
。ownProps
:这个组件自有的props
属性。
3.第三个参数mergeProps
stateProps,dispatchProps
,自身的props
将传入到这个函数中。默认是Object.assign({}, ownProps, stateProps, dispatchProps)
。
4.第四个参数options
pure = true
; // 默认值,这时候connector
将执行shouldComponentUpdate
并且浅对比mergeProps
的结果,避免不必要的更新。
3.bindActionCreators
的实现
bindActionCreators
其实是redux
的一个api
,但是由于这个方法与react-redux
关系密切,我们就在react-redux
实现这个api
。
他通过闭包,把
dispatch
和actionCreator
隐藏起来,让其他地方感知不到redux
的存在。
const reducer = combineReducers({
counter: counterReducer,
info: infoReducer
});
const store = createStore(reducer);
/*返回 action 的函数就叫 actionCreator*/
function increment() {
return {
type: 'INCREMENT'
}
}
function setName(name) {
return {
type: 'SET_NAME',
name: name
}
}
const actions = {
increment: function () {
return store.dispatch(increment.apply(this, arguments))
},
setName: function () {
return store.dispatch(setName.apply(this, arguments))
}
}
/*注意:我们可以把 actions 传到任何地方去*/
/*其他地方在实现自增的时候,根本不知道 dispatch,actionCreator等细节*/
actions.increment(); /*自增*/
actions.setName('吴'); /*修改 info.name*/
乍一看重复代码有点多,我们提取一下,希望达到下面的效果
const actions = bindActionCreators({ increment, setName }, store.dispatch);
一起来实现一个bindActionCreators
。
/*核心的代码在这里,通过闭包隐藏了 actionCreator 和 dispatch*/
function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(this, arguments))
}
}
/* actionCreators 必须是 function 或者 object */
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error()
}
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
4.Provider api
的实现
这里需要借助React
的高级属性Context
,便于Connect
能方便的获取store
import React,{Component} from "react";
import { ReactReduxContext } from './Context';
class Provider extends Component{
// 定义一个Provider组件,以便Connect组件能够获取到store对象
constructor(props) {
super(props);
this.store = props.store; // 保存通过props属性注入到Provider组件的store对象
}
render(){
return(
<ReactReduxContext.Provider value={this.store}>
{this.props.children}
</ReactReduxContext.Provider>
)
}
}
export default Provider;
5.Context
中间层的实现
其实就是一个React
的中间层的概念,方便下级组件获取上级组件的属性
import React from 'react';
//创造context方便store传递
export const ReactReduxContext=React.createContext(null)
6.Connect
组件的实现
import React,{Component} from 'react';
import { ReactReduxContext } from './Context';
export default (mapStateToProps, mapDispatchToProps)=>(WrappedComponent)=>{
class NewComponent extends Component{
static contextType = ReactReduxContext;
constructor(props){
super(props);
}
UNSAFE_componentWillMount(){
let value = this.context;
this.updateState(value)
//监听store的变化
value.subscribe(()=>{this.updateState(value)});
}
updateState=(store)=>{
let stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {} // 防止 mapStateToProps 没有传入
let dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: {} // 防止 mapDispatchToProps 没有传入
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render(){
return <ReactReduxContext.Consumer>
{(value) => {
return <WrappedComponent {...this.state.allProps} />
}}
</ReactReduxContext.Consumer>
}
}
return NewComponent;
}
7.总结
其实我们已经完成了redux、react-redux
的大部分常用功能,只是简化了代码错误验证的机制等等,再改良一下用在项目里面也未尝不可。
三、dva
的实践
什么是dva
?dva
封装了react-redux
,react-router
,react-saga
,将原本复杂的配置简化。使得开发者更关注于业务逻辑的开发。由于dva
基于react-redux
和redux
,我们将利用我们前面学到的知识来完善我们的dva
库。但是由于react-saga
与react-router
与我们的研究课题无关,故使用react-saga
,react-router
。
1.dva
概括
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过
dispatch
发起一个action
,如果是同步行为会直接通过Reducers
改变State
,如果是异步行为(副作用)会先触发Effects
然后流向Reducers
最终改变State
,所以在dva
中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致 。
app.router(require('./routes/indexAnother'));
配置路由app.model(require('./models/example'));
配置model
dva
完成了 使用React
解决view
层、redux
管理model
、saga
解决异步,使得react
应用分层管理,一般来说,model
文件夹下放置redux
配置文件、routes
放置react
路由页面、services
层放置异步请求接口。
2.dva
项目搭建
我们在上章src
下新建dva
文件夹下存放本章节相关的代码。
models
存储react-redux
相关的代码
router
放置路由相关文件
routes
放置路由组件页面
.babelrc
是编译babel
的配置
dva.js
是我们需要实现的dva
index.js
是我们的dva
入口页面
3.model
层reducer
与state
创建
我们知道state
与reducer
可谓是相辅相成,今天我们就来实现dva
的这2个api
。让我们看下dva-cli
创建的脚手架入口页面。
import dva from './dva';
// 1. Initialize
const app = dva();
// 2. Plugins
// app.use({});
// 3. Model
app.model(require('./models/global').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
对于上面的
require().default
,同学们可能会有点懵,我们知道babel6+
用require
引export default
导出的组件 还要加个require().default
。
由上面的dva
入口文件,我们大致可以分析出dva.js
文件的大致结构。
// dva/dva.js文件
export default function(){
let app={
_models:[],
_router:null,
model,
router,
start,
}
function model(mo){
app._models.push(mo);
}
function router(router){
app._router=router;
}
function start(container){
ReactDOM.render(app._router,document.querySelector(container))
}
return app;
}
废话不多说,新建个路由配置页面。
// router/index.js
import React from 'react';
import TestReducerPage from './../routes/TestReducer';
import { Route , HashRouter,Switch } from 'react-router-dom';
export default ()=>(
<HashRouter>
<Switch>
<Route path="/" exact component={TestReducerPage}/>
</Switch>
</HashRouter>
)
新建测试reducer
的页面,这里我们仿照普通的dva
项目,实现点击按钮达到切换p标签的效果。
import React from 'react';
//dva中导出的是react-redux的connect,我们可以导出我们实现的react-redux。
import { connect } from '../dva';
@connect(({ global }) => ({
global
}))
class TestReducer extends React.Component {
handleShow = () => {
const { global:{show},dispatch } = this.props;
dispatch({
type:"global/toggle",
payload:{
show:!show
}
})
}
render() {
const {
global:{show}
}=this.props;
return (
<div>
{show && <h1>hello</h1>}
<button onClick={this.handleShow}>点击出现/消失</button>
</div>
)
}
}
export default TestReducer;
接下来重点就是dva.js
文件的实现,直接放源码,如果认真看了前面的内容,这章也会略显简单。
//前章实现的redux api
import { createStore,combineReducers } from '../model/redux';
//前章实现的react-redux api
import { Provider,connect } from '../model/component';
import React from 'react';
import ReactDom from 'react-dom';
function getReducer(app){//将namespace对应每个reducer
let reducers={};
for(let m of app._models){//m是每个model的配置
reducers[m.namespace]=function(state=m.state,action){//组织每个模块的reducer
let allReducers=m.reducers//reducers的配置对象,里面是对象
let reducer=allReducers[action.type];//是否存在reducer
if(reducer){
return reducer(state,action);
}
return state;
}
}
return combineReducers(reducers);
}
function prefix(model){//是为了给model加namespace前缀
let allReducers=model.reducers;
let reducers=Object.keys(allReducers).reduce((prev,next)=>{
let newkey=model.namespace+"/"+next;
prev[newkey]=allReducers[next];
return prev;
},{})//初始化prev为{} next为函数名
model = { ...model, reducers }
return model;
}
export default function(){
let app={
_models:[],
_router:null,
model,
router,
start,
}
function model(m){
let prefixmodel=prefix(m);
app._models.push(prefixmodel);
}
function router(router){
app._router=router;
}
function start(container){
let reducer=getReducer(app);
let store=createStore(reducer);
ReactDom.render(<Provider store={store}>
{app._router()}
</Provider>,document.querySelector(container));
}
return app;
}
//导出connect以便使用
export {connect}
这下子dva
的架构算是完成了,我们来验证一下是否达到了这样的一个效果:
我们需要将
webpack
配置的入口文件替换成dva/index.js
,然后启动yarn start
我们会发现我们已经达到了点击隐藏/显示
的功能了。
4.model
层的effects
创建
我们知道,异步数据的获取是不可缺少的一部分,那么我们如何实现这个功能呢?
effects
是扩展reducer
,所以我们和reducer
一样需要加上namespace
前缀,我们将prefix
函数改造一下。
//加前缀的方法
function prefix(obj, namespace) {
return Object.keys(obj).reduce((prev, next) => {//prev收集,next是每个函数名
let newkey = namespace + '/' + next
prev[newkey] = obj[next]
return prev
}, {})
}
//给reducers和effects加前缀
function prefixResolve(model) {
if (model.reducers) {
model.reducers = prefix(model.reducers, model.namespace)
}
if (model.effects) {
model.effects = prefix(model.effects, model.namespace)
}
return model
}
//注册model时需要改变
function model(m){
let prefixmodel = prefixResolve(m)
app._models.push(prefixmodel)
}
在前章redux-saga
文章中,我们大概了解了使用react-saga
的流程,将saga
中间件加入createStore
,写好saga
配置文件,run
启动saga
中间件的监听,也就是action
的监听,接下来我们就集成使用。
//前文中的监听配置
/**
* 异步action监听
* dispatch对应的action时,调用对应的异步处理方式
*/
//监听FETCH_DATA action,执行了FETCH_DATA action就运行effects.fetchData方法
function* watcher(){
console.log("soga watcher");
yield takeEvery(actionsType.FETCH_DATA,effects.fetchData);
}
//以下代码对于不了解react-saga可能晦涩难懂,大概是run监听多个effects进程,如果监听到了就执行对应的方法。
function prefixType(type, model) {
if (type.indexOf('/') == -1) {//这个判断有点不严谨,可以自己再捣鼓下
return model.namespace + '/' + type
}
return type//如果有前缀就不加,因为可能派发给别的model下的
}
//主要是为了改写Put方法
function getWatcher(key, effect,model) {//key为获取effects的名字,effect为函数
function put(action) {
return sagaEffects.put({ ...action, type: prefixType(action.type, model) })
}
return function* () {
yield sagaEffects.takeEvery(key, function* (action) {//对action进行监控,调用下面这个saga
yield effect(action, {...sagaEffects,put})
})
}
}
function getSagas(app) {//遍历effects
let sagas = []
for (let m of app._models) {
sagas.push(function* () {
for (const key in m.effects) {//key就是每个函数名
const watcher = getWatcher(key, m.effects[key],m)
yield sagaEffects.fork(watcher) //用fork不会阻塞
}
})
}
return sagas
}
dva.js
源码完整如下:
import { createHashHistory } from 'history';//一个history库,库里面有各种方法帮助我们实现history
import { createStore,combineReducers,applyMiddleware } from '../model/redux';
import { Provider,connect } from '../model/component';
import createSagaMiddleware from 'redux-saga';
//saga的功能 call请求 put触发action select选择等等
import * as sagaEffects from 'redux-saga/effects';
import React from 'react';
import ReactDom from 'react-dom';
//废弃
function __prefix(model){
let allReducers=model.reducers;
let reducers=Object.keys(allReducers).reduce((prev,next)=>{
let newkey=model.namespace+"/"+next;
prev[newkey]=allReducers[next];
return prev;
},{})//初始化prev为{} next为函数名
model = { ...model, reducers }
return model;
}
function getReducer(app){
let reducers={};
for(let m of app._models){//m是每个model的配置
reducers[m.namespace]=function(state=m.state,action){//组织每个模块的reducer
let allReducers=m.reducers//reducers的配置对象,里面是对象
let reducer=allReducers[action.type];//是否存在reducer
if(reducer){
return reducer(state,action);
}
return state;
}
}
return combineReducers(reducers);
}
function prefix(obj, namespace) {
return Object.keys(obj).reduce((prev, next) => {//prev收集,next是每个函数名
let newkey = namespace + '/' + next
prev[newkey] = obj[next]
return prev
}, {})
}
function prefixResolve(model) {
if (model.reducers) {
model.reducers = prefix(model.reducers, model.namespace)
}
if (model.effects) {
model.effects = prefix(model.effects, model.namespace)
}
return model
}
function prefixType(type, model) {
if (type.indexOf('/') == -1) {//这个判断有点不严谨,可以自己再捣鼓下
return model.namespace + '/' + type
}
return type//如果有前缀就不加,因为可能派发给别的model下的
}
function getWatcher(key, effect,model) {//key为获取effects的名字,effect为函数
function put(action) {
return sagaEffects.put({ ...action, type: prefixType(action.type, model) })
}
return function* () {
yield sagaEffects.takeEvery(key, function* (action) {//对action进行监控,调用下面这个saga
yield effect(action, {...sagaEffects,put})
})
}
}
function getSagas(app) {//遍历effects
let sagas = []
for (let m of app._models) {
sagas.push(function* () {
for (const key in m.effects) {//key就是每个函数名
const watcher = getWatcher(key, m.effects[key],m)
yield sagaEffects.fork(watcher) //用fork不会阻塞
}
})
}
return sagas
}
export default function(){
let app={
_models:[],
_router:null,
model,
router,
start,
}
function model(m){
let prefixmodel = prefixResolve(m)
app._models.push(prefixmodel)
}
function router(router){
app._router=router;
}
function start(container){
let reducer=getReducer(app);
let sagas=getSagas(app);
let sagaMiddleware = createSagaMiddleware();
let store=createStore(reducer,applyMiddleware(sagaMiddleware));
sagas.forEach(sagaMiddleware.run)
ReactDom.render(<Provider store={store}>
{app._router()}
</Provider>,document.querySelector(container));
}
return app;
}
export {connect}
model
文件下加入effects
...
effects:{
*getImages({payload},{call,put}){
const response=yield call(getlist);
yield put({
type:"toggle",
payload:{
data:response.data.data
}
})
}
},
...
路由页面增加触发异步的方法:
//路由文件如下修改
...
handleFetch=()=>{
const { dispatch } = this.props;
dispatch({
type:"global/getImages",
})
}
...
<button onClick={this.handleFetch}>获取图片</button>
{
data.map(item=><img width="100" height="100" key={item.id} src={item.author.avatar_url} />)
}
</div>
)
}
}
export default TestReducer;
5.dva
路由创建
在dva
中有2种跳转方式:
- 利用
Link
进行路由跳转
import { Link } from 'dva/router'
<Link to='/maintain/eventstatisticsdetial'>查看</Link>
这种我们直接把react-router-dom
中导入dva
中再导出:
// dva.js
import { Link } from 'react-router-dom';
.....
.....
.....
export {
connect,
Link
}
// 路由界面
import { connect,Link } from '../dva';
···
···
···
<Link to="/router"><button>Link标签跳转</button></Link>
- 基于
dva/router
的routerRedux
进行跳转
/**
pathname: 路由路径
search: 路由跳转时携带的参数,路由跳转后可以通过 this.props.location.search 获取传递的参数
**/
this.props.dispatch(
routerRedux.push({ pathname, search })
);
为了实现上面的这种函数式跳转,我们需要引入几个包:
react-router-redux
connected-react-router
(基于react-router
里Router
做的,相当于Router
外面套一层来监听路由变化派发action
改变state
,Router
是通过上下文传入history
和location
来条件渲染的)
//引入相关的包
import * as routerRedux from 'react-router-redux';
import { routerMiddleware, connectRouter, ConnectedRouter } from 'connected-react-router';
//将connectRouter传进reducer 方便改变state
function getReducer(app){
let reducers={
router: connectRouter(app._history)
};
for(let m of app._models){//m是每个model的配置
reducers[m.namespace]=function(state=m.state,action){//组织每个模块的reducer
let allReducers=m.reducers//reducers的配置对象,里面是对象
let reducer=allReducers[action.type];//是否存在reducer
if(reducer){
return reducer(state,action);
}
return state;
}
}
return combineReducers(reducers);
}
//使用history库
import { createHashHistory } from 'history';//一个history库,库里面有各种方法帮助我们实现history
let app={
...
_history:opt.history||createHashHistory(),
...
}
//将路由中间件传入 原路由使用ConnectedRouter包裹
function start(container){
let reducer=getReducer(app);
let sagas=getSagas(app);
let sagaMiddleware = createSagaMiddleware();
app._store=createStore(reducer,applyMiddleware(routerMiddleware(app._history),sagaMiddleware));
for(let m of app._models){
if(m.subscriptions){
for(let key in m.subscriptions){
let subscription=m.subscriptions[key];
subscription({history,dispatch:app._store.dispatch})
}
}
}
sagas.forEach(sagaMiddleware.run)
ReactDom.render(<Provider store={app._store} >
<ConnectedRouter history={app._history} store={app._store} context={ReactReduxContext}>
{app._router({app,history:app._history})}
</ConnectedRouter>
</Provider>,document.querySelector(container));
}
新建TestRouter.js
测试,发现可以正常跳转。
import React from 'react';
import { connect,routerRedux } from '../dva';
@connect(()=>{
})
class TestRouter extends React.Component {
toIndex=()=>{
this.props.dispatch(routerRedux.push("/"))
}
render() {
return (
<div>
<h1>TestRouter</h1>
<button onClick={this.toIndex}>跳转到/路径</button>
</div>
)
}
}
export default TestRouter;
6.subscriptions
实现
和subscription
类似,以 key/value
格式定义 subscription
。subscription
是订阅,用于订阅一个数据源,然后根据需要 dispatch
相应的 action
。在 app.start()
时被执行,数据源可以是当前的时间、服务器的 websocket 连接
、keyboard 输入
、geolocation
变化、history 路由变化
等等。这个实现较前面的内容简单一些,在start
的时候执行下传递history和dispatch
就行。
for(let m of app._models){
if(m.subscriptions){
for(let key in m.subscriptions){
let subscription=m.subscriptions[key];
subscription({history:app._history,dispatch:app._store.dispatch})
}
}
}
在页面中使用
subscriptions:{
listener({history,dispatch}){)
history.listen(({ pathname }) => {
if (pathname === '/router') {
console.log("当前页面在/router路径")
}
});
}
}
当跳转到
/router
下,会进入方法打印。
subscriptions就大功告成了。
7.dva.use(hooks)
app.use(hooks)
配置 hooks
或者注册插件(插件最终返回的是 hooks
)。相当于dva
的生命周期函数,在某个实际如effects
被触发时、actions
被触发时运行对应的钩子函数。hooks
有以下几个api
:
extraReducers
指定额外的reducer
。
onError((err, dispatch) => {})effect
执行错误或subscription
通过done
主动抛错时触发,可用于管理全局出错状态。
onAction(fn | fn[])
在action
被dispatch
时触发,用于注册redux
中间件。支持函数或函数数组格式。
onStateChange(fn)state
改变时触发,可用于同步state
到localStorage
,服务器端等。
onReducer(fn)
封装reducer
执行。
onEffect(fn)
封装effect
执行。比如dva-loading
基于此实现了自动处理loading
状态。
onHmr(fn)
热替换相关,目前用于babel-plugin-dva-hmr
。
extraEnhancers
指定额外的StoreEnhancer
,比如结合redux-persist
的使用。
1.实现挂载钩子方法use
新建plugins/plugin.js
文件
const hooks=[
];
export function filterHooks(options){//筛选符合钩子名的配置项
return Object.keys(options).reduce((prev,next)=>{
if(hooks.indexOf(next)>-1){
prev[next]=options[next]
}
return prev
},{})
}
export default class Plugin{//用来统一管理
constructor(){//初始化把钩子都做成数组
this.hooks=hooks.reduce((prev,next)=>{
prev[next]=[];
return prev;
},{})
}
use(plugin){//因为会多次使用use 所以就把函数或者对象push进对应的钩子里
const {hooks}=this;
for(let key in plugin){
hooks[key].push(plugin[key])
}
}
get(key){//不同的钩子进行不同处理
if(key==="extraReducers"){//处理reducer就把所有对象并成总对象,这里只能是对象形式才能满足后面并入combine的操作。
return Object.assign({},...this.hooks[key])
}else{
return this.hooks[key]//其他钩子就返回用户配置的函数或对象
}
}
}
//在dva.js中引入
import Plugin, { filterHooks } from './plugins/plugin';
let app={
...
_plugin:null
}
function use(useOption){
app._plugin=new Plugin();
app._plugin.use(filterHooks(useOption))
}
2.实现extraReducer
钩子
如其名字一样,额外的reducer
,肯定是通过combineReducer
组合进去。
//修改plugins/plugin.js
const hooks=[
"extraReducers"//添加reducer
];
//dva.js
//将插件传入reducer进行reducer的合成
let reducer=getReducer(app,app._plugin);
function getReducer(app,plugin){
let reducers={
router: connectRouter(app._history)
};
for(let m of app._models){//m是每个model的配置
reducers[m.namespace]=function(state=m.state,action){//组织每个模块的reducer
let allReducers=m.reducers//reducers的配置对象,里面是对象
let reducer=allReducers[action.type];//是否存在reducer
if(reducer){
//更新state,如果不更新state不更新会默认返回初始值
m.state=reducer(state,action);
return reducer(state,action);
}
return state;
}
}
let extraReducers = plugin.get('extraReducers')
return combineReducers({
...reducers,
...extraReducers
});
}
3.实现onEffects
钩子
在effect
被触发时执行,相当于我们自己写effects
的中间件,可以拿到要执行的effects
。
//plugins/plugin.js
const hooks=[
"onEffect",//effect中间件
"extraReducers"//添加reducer
];
//传入saga进行saga的配置,这里配置稍有些复杂,需要熟悉saga,这里可以直接把他理解为重写dispatch的逻辑。
let sagas=getSagas(app,app._plugin);
function getSagas(app,plugin) {//遍历effects
let sagas = []
for (let m of app._models) {
sagas.push(function* () {
for (const key in m.effects) {//key就是每个函数名
const watcher = getWatcher(key, m.effects[key],m,plugin.get("onEffect"))
yield sagaEffects.fork(watcher) //用fork不会阻塞
}
})
}
return sagas
}
function getWatcher(key, effect,model,onEffect) {//key为获取effects的名字,effect为函数
function put(action) {
return sagaEffects.put({ ...action, type: prefixType(action.type, model) })
}
return function* () {
yield sagaEffects.takeEvery(key, function* (action) {//对action进行监控,调用下面这个saga
if (onEffect) {
for (const fn of onEffect) {//oneffect是数组
effect = fn(effect, { ...sagaEffects, put }, model, key)
}
}
yield effect(action, {...sagaEffects,put})
})
}
}
4.基于extraReducer
和onEffects
实现dva-loading
dva-loading
可以监听effects
的变化,可以在effects
执行过程中调用reducer
计算出state
,可以使我们减少重复的loading/unloading
代码。大致意思就是在effects
执行时改变state
数据。
const SHOW="@@DVA_LOADING/SHOW";
const HIDE="@@DVA_LOADING/HIDE";
const NAMESPACE="loading";
export default function createLoading(options){
let initialState={
global:false,//全局
model:{},//用来确定每个namespace是true还是false
effects:{}//用来收集每个namespace下的effects是true还是false
}
const extraReducers={//这里直接把写进combineReducer的reducer准备好,键名loading
[NAMESPACE](state=initialState,{type,payload}){
let {namespace,actionType}=payload||{};
switch(type){
case SHOW:
return {
...state,
global:true,
model:{
...state.model,[namespace]:true
},
effects:{
...state.effects,[actionType]:true
}
}
case HIDE:
{
let effects={...state.effects,[actionType]:false}//这里state被show都改成true了
let model={
//然后需要检查model的effects是不是都是true
...state.model,
[namespace]:Object.keys(effects).some(actionType=>{//查找修改完的effects
let _namespace=actionType.split("/")[0]//把前缀取出
if(_namespace!=namespace){//如果不是当前model的effects就继续
return false;
}//用some只要有一个true就会返回,是false就继续
return effects[actionType]//否则就返回这个effects的true或者false
})
}
let global=Object.keys(model).some(namespace=>{//要有一个namespace是true那就返回
return model[namespace]
})
return {
effects,
model,
global
}
}
default:
return state;
}
}
}
function onEffect(effects,{put},model,actionType){//actiontype就是带前缀的saga名
const { namespace }=model;
return function * (...args){
try{//这里加上try,防止本身的effects执行挂了,然后就一直不会hide,导致整个功能失效。
yield put({type:SHOW,payload:{namespace,actionType}});
yield effects(...args)
}finally{
yield put({type:HIDE,payload:{namespace,actionType}});
}
}
}
return {
onEffect,
extraReducers
}
}
我们大致来试用下自己的loading。
import DvaLoading from './plugins/loading';
...
// 2. Plugins
app.use(DvaLoading());
//路由页面
@connect(({global,loading})=>({
....
imgLoading:loading.effects["global/getImages"]
}))
...
{imgLoading && <p>Loadinging...</p>}
可以看见这样的效果,当点击获取图片按钮显示出来,loading...
会显示出来。请求结束后,图片显示出来了,loading...
字样也消失了,代表请求结束。这样使得代码异常简洁。dva-loading
全部功能就已经完成了。贴下dva.js
完整代码。
import { createHashHistory } from 'history';//一个history库,库里面有各种方法帮助我们实现history
import * as routerRedux from 'react-router-redux';
import { routerMiddleware, connectRouter, ConnectedRouter } from 'connected-react-router';
import { createStore,combineReducers,applyMiddleware } from '../model/redux';
import { Link } from 'react-router-dom';
import { Provider,connect,ReactReduxContext } from '../model/component';
import createSagaMiddleware from 'redux-saga';
//saga的功能 call请求 put触发action select选择等等
import * as sagaEffects from 'redux-saga/effects';
import React from 'react';
import ReactDom from 'react-dom';
import Plugin, { filterHooks } from './plugins/plugin';
//废弃
function __prefix(model){
let allReducers=model.reducers;
let reducers=Object.keys(allReducers).reduce((prev,next)=>{
let newkey=model.namespace+"/"+next;
prev[newkey]=allReducers[next];
return prev;
},{})//初始化prev为{} next为函数名
model = { ...model, reducers }
return model;
}
function getReducer(app,plugin){
let reducers={
router: connectRouter(app._history)
};
let extraReducers = plugin.get('extraReducers');
for(let m of app._models){//m是每个model的配置
reducers[m.namespace]=function(state=m.state,action){//组织每个模块的reducer
let allReducers=m.reducers//reducers的配置对象,里面是对象
let reducer=allReducers[action.type];//是否存在reducer
if(reducer){
m.state=reducer(state,action);
return reducer(state,action);
}
return state;
}
}
return combineReducers({
...reducers,
...extraReducers
});
}
function prefix(obj, namespace) {
return Object.keys(obj).reduce((prev, next) => {//prev收集,next是每个函数名
let newkey = namespace + '/' + next
prev[newkey] = obj[next]
return prev
}, {})
}
function prefixResolve(model) {
if (model.reducers) {
model.reducers = prefix(model.reducers, model.namespace)
}
if (model.effects) {
model.effects = prefix(model.effects, model.namespace)
}
return model
}
function prefixType(type, model) {
if (type.indexOf('/') == -1) {//这个判断有点不严谨,可以自己再捣鼓下
return model.namespace + '/' + type
}
return type//如果有前缀就不加,因为可能派发给别的model下的
}
function getWatcher(key, effect,model,onEffect) {//key为获取effects的名字,effect为函数
function put(action) {
return sagaEffects.put({ ...action, type: prefixType(action.type, model) })
}
return function* () {
yield sagaEffects.takeEvery(key, function* (action) {//对action进行监控,调用下面这个saga
if (onEffect) {
for (const fn of onEffect) {//oneffect是数组
effect = fn(effect, { ...sagaEffects, put }, model, key)
}
}
yield effect(action, {...sagaEffects,put})
})
}
}
function getSagas(app,plugin) {//遍历effects
let sagas = []
for (let m of app._models) {
sagas.push(function* () {
for (const key in m.effects) {//key就是每个函数名
const watcher = getWatcher(key, m.effects[key],m,plugin.get("onEffect"))
yield sagaEffects.fork(watcher) //用fork不会阻塞
}
})
}
return sagas
}
export default function(opt={}){
let app={
_models:[],
_router:null,
model,
router,
start,
use,
_history:opt.history||createHashHistory(),
_store:{},
_plugin:null
}
function use(useOption){
app._useOption=useOption;
app._plugin=new Plugin();
app._plugin.use(filterHooks(useOption))
}
function model(m){
let prefixmodel = prefixResolve(m)
app._models.push(prefixmodel)
}
function router(router){
app._router=router;
}
function createState(){
let reducer=getReducer(app,app._plugin);
let sagas=getSagas(app,app._plugin);
let sagaMiddleware = createSagaMiddleware();
app._store=createStore(reducer,applyMiddleware(routerMiddleware(app._history),sagaMiddleware));
for(let m of app._models){
if(m.subscriptions){
for(let key in m.subscriptions){
let subscription=m.subscriptions[key];
subscription({history:app._history,dispatch:app._store.dispatch})
}
}
}
sagas.forEach(sagaMiddleware.run)
return app._store;
}
function start(container){
createState()
ReactDom.render(<Provider store={app._store} >
<ConnectedRouter history={app._history} store={app._store} context={ReactReduxContext}>
{app._router({app,history:app._history})}
</ConnectedRouter>
</Provider>,document.querySelector(container));
}
return app;
}
export {
connect,
Link,
routerRedux
}
5.onAction
实现
注册中间件压入
store
里面,还记得我们前面提到了打印日志中间件吗?我们完善下,加点样式。
// plugin/logger.js文件
const logger = ({ dispatch, getState }) => next => action => {
let prevState = getState()
next(action)
let nextState = getState()
console.group(
`%caction %c${action.type} %c@${new Date().toLocaleTimeString()}`,
`color:grey;font-weight:lighter`,
`font-weight:bold`,
'color:grey;font-weight:lighter'
)
console.log('%cprev state', `color:#9E9E9E; font-weight:bold`, prevState);
console.log('%caction', `color:#03A9F4; font-weight:bold`, action);
console.log('%cnext state', `color:#4CAF50; font-weight:bold`, nextState);
console.groupEnd()
}
export default function (){
return logger;
}
// dva/index.js
import DvaLogger from './plugins/logger';
// 1. Initialize
const app = dva({
onAction:DvaLogger()
});
// dva/dva.js
let extraMiddleware=opt.onAction;
app._store=createStore(reducer,applyMiddleware(routerMiddleware(app._history),sagaMiddleware,extraMiddleware));
6.onStateChage实现
state
更改时触发
// 传入onStateChange
// 1. Initialize
const app = dva({
...
onStateChange(state){
localStorage.setItem('state', JSON.stringify(state))
}
});
// store变化时触发函数
function start(container){
createState()
const { onStateChange }=opt;
app._store.subscribe(()=>{
onStateChange(app._store.getState());
})
ReactDom.render(<Provider store={app._store} >
<ConnectedRouter history={app._history} store={app._store} context={ReactReduxContext}>
{app._router({app,history:app._history})}
</ConnectedRouter>
</Provider>,document.querySelector(container));
}