阅读 674

💥手撸JS系列:手写一个Dva框架

本项目完整github地址

一、redux实践

在介绍dva之前,我们先介绍一下reduxdva是一个基于reduxredux-saga的数据流方案。然后为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架。

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,返回一个实例对象。

这个对象有三个方法:

  1. getState():无需传参,获取state
  2. dispatch(action):参数为一个actionaction实际上就是一个普通的js对象,用于更新state
  3. subscribe(listener):参数为一个函数,当state改变时会触发这个函数。

上文中的defaultStateredux中的store,actionsTypesreduxaction的类型,inputReducerreduxreducer,他是一个纯函数,传入stateaction。我们在createStore方法中传入inputReducer,返回store实例。我们将storeactionTypes导出。

//我们将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函数,这个函数的目的

  1. 传入一个reducer,一个state,返回一个对象。
  2. 对象有一个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

接着上次那个例子,我们在输入框输入,发现并无任何反应,我们查看inputonChange事件,每次修改都会注入reduxstate,但是我们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来改变reduxstate

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方法来改变reduxstate

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发生改变,注册的监听会执行一遍。

这算是实现了reduxsubscribe方法。

7.尝试合并reducer

我们尝试增加一个reduxstate值,来初始化文字。

//文字
<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
}   
复制代码
  1. getState 用于获取到state
  2. dispatch 派发action,改变state
  3. subscribe 注册监听监听state的变化
  4. combineReducers 集合多个reducer为一个reducer

createStore函数中加一行。

//dispatch触发一个不匹配action的值,来初始化state
dispatch({ type: Symbol() })
复制代码

9.redux中间件

Redux本身就提供了非常强大的数据流管理功能,但这并不是它唯一的强大之处,它还提供了利用中间件来扩展自身功能,以满足用户的开发需求。 网上找的图,本人画图功底不厚,故找网图。

正常的没有中间件的流程

加入中间件的流程
在增加了中间件 middleware 后,我们就可以在这途中对 action 进行截获,并进行改变。而触发action的流程就是dispatch,所以其实中间件就是重写dispatch

1.现在考虑一个业务需求,我们需要写一个中间件记录dispatch前的statedispatch后的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);
复制代码

Providerconnect就是本篇文章的重点,我们一步步来实现这个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然后通过storedispatch来触发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

  1. state:整个Redux storestate,它返回一个要作为props 传递的对象。
  2. 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

  1. dispatch:store对象里面的dispatch
  2. 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

他通过闭包,把 dispatchactionCreator 隐藏起来,让其他地方感知不到 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的实践

什么是dvadva封装了react-reduxreact-router,react-saga,将原本复杂的配置简化。使得开发者更关注于业务逻辑的开发。由于dva基于react-reduxredux,我们将利用我们前面学到的知识来完善我们的dva库。但是由于react-sagareact-router与我们的研究课题无关,故使用react-saga,react-router

1.dva概括

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致 。

  1. app.router(require('./routes/indexAnother')); 配置路由
  2. app.model(require('./models/example'));配置model

dva 完成了 使用 React 解决 view 层、redux管理 modelsaga 解决异步,使得react应用分层管理,一般来说,model文件夹下放置redux配置文件、routes放置react路由页面、services层放置异步请求接口。

2.dva项目搭建

我们在上章src下新建dva文件夹下存放本章节相关的代码。

  1. models存储react-redux相关的代码
  1. router放置路由相关文件
  1. routes放置路由组件页面
  1. .babelrc是编译babel的配置
  1. dva.js是我们需要实现的dva
  1. index.js是我们的dva入口页面

3.modelreducerstate创建

我们知道statereducer可谓是相辅相成,今天我们就来实现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+requireexport 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种跳转方式:

  1. 利用 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>
复制代码
  1. 基于 dva/routerrouterRedux进行跳转
/**
  pathname: 路由路径
  search: 路由跳转时携带的参数,路由跳转后可以通过 this.props.location.search 获取传递的参数
**/
this.props.dispatch(
  routerRedux.push({ pathname, search })
);
复制代码

为了实现上面的这种函数式跳转,我们需要引入几个包:

  1. react-router-redux
  1. connected-react-router(基于react-routerRouter做的,相当于Router外面套一层来监听路由变化派发action改变state,Router是通过上下文传入historylocation来条件渲染的)
//引入相关的包
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 格式定义 subscriptionsubscription 是订阅,用于订阅一个数据源,然后根据需要 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

  1. extraReducers指定额外的 reducer
  1. onError((err, dispatch) => {})effect执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态。
  1. onAction(fn | fn[])actiondispatch 时触发,用于注册 redux 中间件。支持函数或函数数组格式。
  1. onStateChange(fn)state改变时触发,可用于同步 statelocalStorage,服务器端等。
  1. onReducer(fn)封装 reducer 执行。
  1. onEffect(fn) 封装 effect 执行。比如 dva-loading 基于此实现了自动处理 loading 状态。
  1. onHmr(fn)热替换相关,目前用于 babel-plugin-dva-hmr
  1. 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.基于extraReduceronEffects实现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));
    
}

复制代码
关注下面的标签,发现更多相似文章
评论