本项目完整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用于获取到statedispatch派发action,改变statesubscribe注册监听监听state的变化combineReducers集合多个reducer为一个reducer
在createStore函数中加一行。
//dispatch触发一个不匹配action的值,来初始化state
dispatch({ type: Symbol() })
9.redux中间件
Redux本身就提供了非常强大的数据流管理功能,但这并不是它唯一的强大之处,它还提供了利用中间件来扩展自身功能,以满足用户的开发需求。 网上找的图,本人画图功底不厚,故找网图。
正常的没有中间件的流程
middleware 后,我们就可以在这途中对 action 进行截获,并进行改变。而触发action的流程就是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));
}