Giter Site home page Giter Site logo

dva-generator's Introduction

##基于react的dva代码生成器

一键生成model、route、less、proxy文件。同时内置perfect-fetch,你只要像如下代码调用就可以了:

return sendrequest(`${nodeserver}/xx/xx/xx`, {
	id: 1,
	name: "jnotnull"
})

perfect-fetch请参见https://github.com/jnotnull/perfect-fetch

##安装

npm install dva-generator -g

##执行

generate-dva bot

dva-generator's People

Contributors

jnotnull avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

dva-generator's Issues

DVA源码阅读-初始化篇

DVA是一个优秀的框架,它很好的集成了Redux和Saga,极大的方便了开发者的异步处理,为我们快速开发提供可能。为什么说DVA是一个框架,而不是库呢。下面我们从源码中给出答案。

在调用 const app = dva(); 初始化之后,dva为我们提供了三个参数入口,分别为app.model、app.router、app.start。那我们就先看下这个最核心部分的初始化过程。

在createDva文件中的dva方法下的如下部分:

const app = {
  // properties
  _models: [],
  _router: null,
  _store: null,
  _history: null,
  _plugin: plugin,
  _getProvider: null,
  // methods
  use,
  model,
  router,
  start,
};
return app;

properties我们先不看,先看下methods。use是插件相关的也先不管,重点看model、router和start三个方法。

  1. model和router也很简单,就是分别在_models数组中注入model以及初始化_router

     function model(model) {
       this._models.push(checkModel(model, mobile));
     }
    
     function router(router) {
       invariant(typeof router === 'function', 'app.router: router should be function');
       this._router = router;
     }
    
  2. 现在重点看下start方法

     const onError = plugin.apply('onError', (err) => {
         throw new Error(err.stack || err);
       });
       const onErrorWrapper = (err) => {
         if (err) {
           if (typeof err === 'string') err = new Error(err);
           onError(err, app._store.dispatch);
         }
       };
    

    这里的重要功能是在onError中注入dispatch,这样在捕获异常后能够继续执行dispatch。

     for (const m of this._models) {
         reducers[m.namespace] = getReducer(m.reducers, m.state);
         if (m.effects) sagas.push(getSaga(m.effects, m, onErrorWrapper));
       }
    

    遍历model,初始化reducers和sagas。这里比较重要的是getSaga函数:

     function getSaga(effects, model, onError) {
       return function *() {
         for (const key in effects) {
           if (Object.prototype.hasOwnProperty.call(effects, key)) {
             const watcher = getWatcher(key, effects[key], model, onError);
             const task = yield sagaEffects.fork(watcher);
             yield sagaEffects.fork(function *() {
               yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
               yield sagaEffects.cancel(task);
             });
           }
         }
       };
     }
    

    通过fork创建了新任务,因为fork本身是无阻塞的,所以当执行了fork的时候,也就执行了${model.namespace}/@@CANCEL_EFFECTS监听,而take是阻塞的,当它被触发时,就调用cancel取消task。在unmodel方法中可以找到:

     store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });
    

    在redux中,创建store的方法如下:

     const store = createStore(
       reducer,
       applyMiddleware(createSagaMiddleware(helloSaga))
     )
    

    第一个参数是reducer,第二个参数是applyMiddleware方法传入各个中间件。现在再来看下dva的创建store过程

     const store = this._store = createStore(
         createReducer(),
         initialState,
         compose(...enhancers),
       );
    

    这里多了initialState,这个是createStore的第二个参数,可选的。第一个参数调用了下面这个方法:

     function createReducer(asyncReducers) {
         return reducerEnhancer(combineReducers({
           ...reducers,
           ...extraReducers,
           ...asyncReducers,
         }));
       }
    

    其中reducerEnhancer是plugin,可有可无的。combineReducers用于合并所有的reducer。这里有意思的还是第三个参数:compose(...enhancers)。 enhancers的定义如下:

     const enhancers = [
         applyMiddleware(...middlewares),
         devtools(),
         ...extraEnhancers,
       ];
    

    因为使用了redux的compose方法,所以第一个元素必须是applyMiddleware

     store.runSaga = sagaMiddleware.run;
    

    这个是redux-saga中的主方法

     store.asyncReducers = {}; 
    

    注册异步reducers

     // setup history
     if (setupHistory) setupHistory.call(this, history);
    
     // run subscriptions
     const unlisteners = {};
     for (const model of this._models) {
     	if (model.subscriptions) {
     	  unlisteners[model.namespace] = runSubscriptions(model.subscriptions, model, this,
     	    onErrorWrapper);
     	}
     }
    

    这些主要是用来处理subscription的,就不用多说了。

  3. 所以整个下来你会发现:

    1. 主要就是围绕着store的创建,以及在创建store过程中的各种扩展。
    2. dva是很好的封装了redux和saga,使得在一个model中暴露了reducer、effect、subscription。它还提供了各种插件机制方便扩展。
    3. 要想使用好dva,你必须对redux和saga了解的非常全面,任何一个短板都会影响你对dva的使用。特别是当前dva还不支持回调处理等,各种机关需要在实战中摸索出来了。react更不用说了,后面有机会再讨论dva下react的编程模式。

所以说我们发现 dva确实是一个优秀的框架,它不是库。

后面会带来第二篇:DVA源码阅读-插件篇

PS:虽然dva已经极大的方便了开发,但是对于一个一个新建model、route、less、proxy文件还是很累的,而dva-generator就是做这件事的,只要generate-dva bot一行命令就可以为你生成该模型下大部分文件,更多请参见:https://github.com/jnotnull/dva-generator

React沉思录

使用React也有一年多时间了,也是时候拿起笔杆子整理下了。以下算是自己对React的一份理解。当然每一点如果放开写的话都可能写上好几页,时间有限,先奉上这么多,有错误欢迎指正。

1. 什么是React

A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES 这是React官方的定义。从这里面我们能得到两层含义:1. 它首先是一个库,不是框架。2. 用来构建用户界面的,也就是说是专注于view层的。
因为是库,所以可以很方便的和前端生态系统中的其他库融合到一起,比如Redux等。通过引入Redux,它就不再简单的处理view层了,Controller层和Model层也收入旗下了。

它有两个重要的特征:一个就是Virtual DOM,另一个就是JSX。当然了Virtual DOM和JSX也可以用在其他框架。

2. React组件的生命周期

使用React开发时候用到最多的就是React的组件了,通过继承React.Component,加入constructor构造函数,实现Render方法即可。这当中React组件为我们提供了很多钩子,以便更加方便精细的控制程序。

钩子包括:componentWillMount/componentDidMount,同时在调用setState时候会触发:componentWillReceiveProps/shouldComponentUpdate/ComponentWillUpdate/render/ComponentDidUpdate。另外在最新发布的16版本中添加了componentDidCatch(error, info) 钩子来为组件做异常边界。

值得注意的是,不能在ComponentWillUpdate使用setState方法,否则会造成循环调用,这是因为componentWillMount是在render前触发的,因此设置state不会触发再次渲染。

3. 什么是JSX

const element = <h1>Hello, world!</h1>; 这就是JSX。通过名字我们就能看出来它是JavaScript的扩展。它就是使用JavaScript的强大功能来供你写UI。因为使用了JavaScript,所以你不用记任何标签语法,唯一你需要记住的可能就是className替代了class熟悉,htmlFor替代了for熟悉,另外绑定事件名称要采用骆驼命名法,比如onClick。对了,因为是JavaScript,所以添加行样式时候采用对象的形式,比如style={{width: 'calc(100% - 20px)', marginLeft: "20px"}},仅此而已。

对于外界一直吐槽的只能返回一个节点的问题,最新发布的16版本中也增加了返回数组元素了。

4. React Components VS. Web Components

按照官方说法,他们两者是互补的,不冲突的,你可以在React中使用Web Components,也可以在Web Components中使用React。但是呢,有兴趣的人终究想要比个一二。具体可以看看这里的帖子 Pros and Cons of Facebook's React vs. Web Components (Polymer) 。事实是,react完全封装了自己一套玩法,和Web Components标准相差太远,React官方也在说它们是解决不同问题的,React侧重于数据同步层面。

5. state数据类别 UI state/Domain state, Local state/Global state

编写好的组件,第一个解决的就是数据问题。一个组件中包含的数据有多种,一种分类就是UI state和Domain state。

先来说说Domain state,它就是我们正常的业务数据,比如说消息数据,我们会通过一些转换然后在界面上显示,当然你也可以去修改。而UI state则是增对组件本身的一些数据,比如说某个按钮的显示,某个checkbox的选择,这些数据一般不会和其他组件共享。

另一种分类是Local state/Global state。

Local state和我们上面说的UI state有一个共同点,就是不会和其他组件共享,但是还有一类数据是属于Local state的,那就是表单的输入数据。这些数据在提交之前只要所属组件知道就可以了。另外一种是Global state,这类数据是从父组件中传递下来的,修改之后还可能需要调用回调方法通知其他组件。

这里为什么要说说state的类别呢,这个其实是和下面说的redux有些关系的,redux管理的state应该是Domain state或者Global state,对于UI state和Local state就不要放进去了,自己setState就可以了。因为毕竟使用redux还是有开销的。

6. 你所不知道的setState

既然前面提到了setState,那我们就来终点说说这个方法。React本身提供给开发者调用的方法很少,这也是React好用的一个重要原因,你不用记着太多的方法签名。而setState就是React提供的很少方法之一。细心的你可能已经知道setState不是同步的,但是你可能还不知道哪些情况却可能是同步的。我们首先来看下setState的方法签名:

setState(updater, [callback])

首先看下第一个参数updater,它可以是一个对象,也可以是函数,函数的签名是

(prevState, props) => stateChange

再来看第二个参数,它的作用就是在执行update成功后执行的回调。

我们重点看下setState执行后到底发生了什么。setState后首先加入到pending队列中,然后判断当前环境是否是Batch Updates,如果是就执行更新,否则就加入到dirtyComponents。那什么时候会处于batch updates状态呢,答案就是在渲染组件的时候。那什么时候不会处于batch updates状态呢,没错,那当然就是脱离React组件能管理的生命周期外的情况了,比如在setTimeout()、addEventListener()等回调中调用setState。

7. SyntheticEvent

SyntheticEvent即合成事件。在JSX中绑定一个事件很简单:

onClick={this.xx.bind(this)}

最终JSX是要转换为VDOM的,而VDOM是在内存中存在的,那对于绑定的事件,React不会直接绑定到某个元素上,而是绑定到元素的最外层,这就是我们所需的事件代理,它的好处我们也是知道的,能极大的提升性能。这就是React封装的SyntheticEvent的原理。

另外按照官方说法,为了性能考虑,SyntheticEvent可以被重复使用的,也就是说使用完后就会被清掉,所以我们只能同步的获取事件。 可以看下官方提供的例子:

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // Won't work. this.state.clickEvent will only contain null values.
  this.setState({clickEvent: event});

  // You can still export event properties.
  this.setState({eventType: event.type});
}

8. Stateful and Stateless Components 以及Stateless Components的功能增强recompose

无状态组件(Stateless Components)因为其书写简单,被众人推崇,看下代码;

import React from ‘react’;

const HelloWorld = ({name}) => (
 <div>{`Hi ${name}`}</div>
);

export default HelloWorld;

看起来非常简单,但是因为没有React组件的生命周期概念,也就不能使用setState/refs/shouldComponentUpdate等属性和方法了,也就有了很大的局限性。除非特别简单的展示组件,否则不推荐使用这种无法交互的组件的。

Stateful Components组件就是我们熟悉的集成React.Component的组件了。

那如果我想在Stateless Components中修改属性怎么做呢,对了,可以使用recompose:

const enhance = withState('counter', 'setCounter', 0)
const Counter = enhance(({ counter, setCounter }) =>
  <div>
    Count: {counter}
    <button onClick={() => setCounter(n => n + 1)}>Increment</button>
    <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
  </div>
)

9. Functional and Class Components

在官方说法中,还有一种组件叫做Functional Components,看下代码:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

接收参数为props,实现起来非常简单。

另一种就是Class Components,它其实和Stateful Components是一个东东,都是继承React.Comonent得到的组件。

10. Smart and Dumb Components / Presentational and Container Components

Smart Components(智能组件)和Dumb Components(木偶组件)也是我们区分组件最常用的方式。

Dumb Components一般也叫做Presentational Components,一般只做展示数据使用,组件内部很少有数据变化,它的数据要依赖外部传入,所以Dumb Components可以使用Stateless Components来定义,一般也是没问题的。

而Smart Components也叫Container Components,我们从名字也能看出来,它是有数据处理能力的,不完全依赖外部数据传入,处理完成之后还可能通知其他组件。

11. Pure components

Pure Components默认会调用 shouldComponentUpdate做数据检查,如果props或者state没有变化的话,就不会触发重新渲染。

Recompose 提供了pure方法,另外React也在v15.3.0提供了React.PureComponent。

import { pure } from 'recompose';
export default pure((props, context) => {
  return <SomeComponent someProp={props.someProp}/>
})
import { PureComponent } from 'react';

export default class Example extends PureComponent {
  render() {
    return <SomeComponent someProp={props.someProp}/>
  }
}
})

12. Height Order Components

Height Order Components即高阶组件,简称HOC。HOC的原理其实很简单,输入一个组件,输出另一个组件。但是它的用途却很大。它其实类似于Decorator,可以增强组件功能。下面来看下它的用法:

var enhanceComponent = (Component) =>
  class Enhance extends React.Component {
    render() {
      return (
        <Component
          {...this.state}
          {...this.props}
        />
      )
    }
  };

export default enhanceComponent;
var OriginalComponent = () => <p>Hello world.</p>;
var EnhancedComponent = enhanceComponent(OriginalComponent);

class App extends React.Component {
  render() {
    return <EnhancedComponent />;
  }
};

这样的写法可以避免我们手工传递一个一个参数,同时增加一个属性也变得非常简单。Dan Abramov说过,我们最好在顶层做HOC,不要在另一个组件中去做HOC,那样因为每次都要重新生成会影响到性能。关于避免深层次传递参数,还有一个有意思的文章推荐: Avoiding deeply nested component trees

13. Redux解决哪一类问题

回答这个问题之前我们先看下什么是Redux。Redux本身使用的**其实我们很熟悉的,就是不要直接操作数据本身。这个我们很熟悉啊,之前的bean的getter和setter不就是这样的。没错,但是它做了更高层的抽象,首先是动作(Action)的抽象。Action定义了可以做的事情,比如add(计数器加1),注意,它只定义一个名称,具体的实现就交给Reducer。所以从这里我们就知道Reducer要做什么事情了把,对的,就是具体事件的定义,比如就给某个属性+1。那我们Action不可能一个吧,多个Action我们就可以抽取出Action Creater。Reducer也不可能一个吧,多个Reducer就形成了reducers集合。那调用reducers也不能说你想用就用,必须有个统一入口,这个就是store,所有的调用都要走store.disptach方法。

这样看下来,似乎没有引入啥新的库,你可能平时或多或少也进行过类似的封装,只可能你封装的抽象程度没有那么高而已。事实也是如此,Redux库本身的源码也不多,除了createStore外,其他大部分是辅助的函数:

  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose

了解了Redux是什么,我们就能知道它是解决哪一类问题了,对了,除了解决数据流问题,其实它还封装了对数据的操作,类似于Controller的概念了。

14. Redux和react-redux的分工是什么,为什么要引入react-router-redux

既然有了Redux,为啥还要有react-redux呢,那是因为Redux不仅仅提供给React,Vue或者Angular也可以使用的。针对React,react-redux主要提供了两个东东,一个是<Provider/>组件 ,一个是 connect()方法。

为啥需要Provider组件呢,那是因为我们在子组件中也可能用到Redux相关东东,我们不能一个组件一个组件把store向下传吧,那样太累了,于是我们在应用的最顶层封装了<Provider/>组件,它直接使用getChildContext 方法将属性传递给子组件,简直就是八神庵的大招啊。connect()方法则是沟通Redux和组件的桥梁,它的方法签名如下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

mapStateToProps顾名思义,定义store 中的数据作为 props 绑定到组件上;
mapDispatchToProps同样,它会将store中的action作为props绑定到组件上;
mergeProps和options默认不用传的。

那为什么要引入react-router-redux呢。我们开发React应用时候,最常用的路由是react-router。Redux是管理应用的状态的,而路由的状态也算是一种状态,自然也要收入到Redux门下了,通过引入react-router-redux,我们只要做下路由和redux的同步就可以了:

this._history = syncHistoryWithStore(history, this._store);

15. Redux middleware

要想扩展Redux的功能,最好的方法就是使用Redux的middleware机制。Redux的middleware类似于KOA的middleware。在项目中用到最多的当然是异步数据请求的中间件saga,看下DVA的源码:

const sagaMiddleware = createSagaMiddleware();
      let middlewares = [
        sagaMiddleware,
        ...flatten(extraMiddlewares),
      ];
      if (routerMiddleware) {
        middlewares = [routerMiddleware(history), ...middlewares];
      }
      let devtools = () => noop => noop;
      if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__) {
        devtools = window.__REDUX_DEVTOOLS_EXTENSION__;
      }
      const enhancers = [
        applyMiddleware(...middlewares),
        devtools(),
        ...extraEnhancers,
      ];
      const store = this._store = createStore(
        createReducer(),
        initialState,
        compose(...enhancers),
      );

从中我们可以看出,多个中间件可以compose的,关于这一段的详细逻辑可以参考我之前的文章:DVA源码阅读-初始化篇

dva tip

  1. 在初始化模型setup中获得url参数
 subscriptions: {
   setup({ dispatch, history}) {
     history.listen((url, state) => {
       if (pathToRegexp('/static/room/ctrlmemberdetail').test(url.pathname)) {
         dispatch({type:'initData', payload: {chatroom_id: url.query.chatroom_id, wechat_id: url.query.wechat_id}});
       }
     });
   }
 }, 
  1. 本地开发请求支持跨域,proxy.cofig.js中增加如下:
module.exports = {
  '/scrm/chat/*': 'http://192.168.5.46:8080/'
};
  1. jsx支持calc
style={{width: 'calc(100% - 20px)', marginLeft: "20px"}}
  1. 解决less中calc冲突
left: e("calc(50% - 70px)");
  1. 支持绝对路径引入组件
webpackConfig.resolve = {
    modulesDirectories: ['node_modules', 'app']
  };
  1. effects中在回调中执行其他effects
subscriptions: {
        setup({ dispatch, history, state, props }) {
            history.listen((query, state) => {
                dispatch({ type: 'initData', payload: { query, namespace, dispatch } });
            });
        }
    },
    effects: {
        initData: [function*({ payload }, { put, call, select }) {
            
            // 执行回调
            let callback = function(params){
                payload.dispatch({ type: 'querysiteinfo', payload: { site_id: params.site_id, origin: params.payload } });
                payload.dispatch({ type: 'querypostdetail', payload });
            };

            yield put({ type: 'querynavdata', payload: { query: payload.query, namespace, callback } });
        }, { type: 'takeLatest' }],

    },

基于React Router的SPA单Route下多view切换方案

场景:针对查询消息历史记录,广播消息事件下有多个群,每个群下有对应的动作,三个view之间的关系如下 eventlist => roomlist => actionlist,点击某个event到达roomlist,点击某个room到达actionlist。因为这三者(eventlist, roomlist, actionlist)依赖关系比较强,而且会被多个地方调用,他们三者就封装为一个组件(Component)了。随着点击的深入,如何返回到上一级就变成现实的问题了,即使你在组件中提供了返回按钮,也不能保证用户那天不点击浏览器的返回按钮。

现有解决方案:

  1. 使用弹出框

    这个在PC端还行,在H5端反复的弹出框就显得蛋疼了, say no。

  2. 服务端渲染

    依靠服务端渲染,由服务端提供基础路由。改动量太大,say no。

  3. 改成多页应用

    这个和服务端渲染类似,只是基础路由由文件夹路径提供而已,改动还是大,say no。

  4. 使用React-Router的childRoutes属性,定义不同路由指向不同的Component,但是面临的问题就是要把组件打散,分配配置每个child路由对应的Compoent,也就是说这里的公共组件至少拆分成三个,而且还不好解决组件共用问题,fuck,这我哪能受得了,say no。

最终方案:BrowerHistory + hash

BrowerHistory提供基础路由,比如这里的广播消息,hash负责分开的试图,这里定义两个hash:#roomlist和#actionlist,hash为空代表进入的是eventlist view。

注意,这里我说的是hash,不是HashHistory,因为React-Router本身只提供一种路由方案,不能混合使用,这里的hash是浏览器原生的location hash。

关于BrowerHistory的配置和用途这里不做说明,下面开始我们的路由之旅。

首先进入到eventlist view。由于点击某个event要进入roomlist,我们绑定下事件:

<span className={styles.process} onClick={this.viewrooms.bind(this, item)} >
{item.ext.status}
</span>

viewrooms方法中修改URL中hash

viewlogrooms(item){
    history.pushState({}, '', '#roomlist');
    let room_ids = item.room_ids;
    queryrooms({room_ids})
}

注意:这里我们我们是前进的过程,使用的是history.pushState,它不会触发window的popstate事件,popstate触发我们用在浏览器点击返回的时候使用。关于onpopstate更多资料,请参考 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onpopstate

而在这里增加hash会触发React-Router监听,在这里我们做下保护,如果有hash则不执行,这样带来的问题是不能带上hash去刷新了。处理方法很暴力,加上location.hash = ''; ,这样就能重新返回到基础路由上了。

{
   path: urlprefix + '/ctrldeskbroadcast',
   name: 'ctrldeskbroadcast',
   getComponent(nextState, cb) {
      if (location.hash) {
        location.hash = '';
        return false;
      }
     require.ensure([], require => {
       registerModel(app, require('./models/ctrldeskbroadcast'));
       cb(null, require('./routes/ctrldeskbroadcast'));
     });
   },
 },

当用户点击浏览器返回按钮时候,会触发onpopstate事件,我们在这里做监听,对view进行切换

window.onpopstate = ()=> {
    if(this._isMounted) {
        const { hash } = location;
        ...
        this.refs.logevent.handlecancelfun();
    }
}

这样就能够完美的实现单个路由下多个view的切换了。

Redux深度揭秘

背景

根据React官方定义,React是解决UI层的库,所以在实际项目中要想完成功能,必须借助其他手段来完成其它层的定义和控制。Redux的出现很好的解决了数据流的问题,完成了其它层的定义和控制。

和传统MVC相比的优势

我们先看下传统的MVC结构。

                         + Input
                         |
                         |
                 +-------v------+
       +---------+  Controller  +---------+
       |         +--------------+         |
       |                                  |
       |                                  |
       |                                  |
+------v-------+                  +-------v------+
|     Model    |                  |      View    |
+--------------+                  +--------------+

从图中我们可以看出以下问题:

  1. 当Controller和Model进行交互时候,他们会改变Model的取值,但是随着项目复杂度的增加,可能会有很多Controll操作相同的Model,带来的问题就是最后不知道有哪些操作了Model,这也带来了数据的不确定性。
  2. 因为不可预测,所以很难做到undo。
                                       +--------+
                      +----------------+ Action <-------------+
                      |                +--------+             |
                      |                                       |
                      |                                       |
+--------+      +-----v------+        +---------+        +----+---+
| Action +------> Dispatcher +--------> Reducer +-------->  View  |
+--------+      +------------+        +---------+        +--------+

而Redux的出现很好的解决了这些问题。根据官方所述,它主要有以下特点。

  1. 因为使用了pure函数,所以任何时候数据的输出都是可预测的,包括UI,这也极大的方便进行单元测试;
  2. 通过记录action,我们能知道谁在什么时候修改了数据,这就让时间旅行成为现实。我们只要记录下修改上下文就可以了。

Redux**

因为Redux有这多好处,那我们现在就来重点看下它到底是何物。首先从它的名字说起吧。

根据维基百科Redux 的解释: brought back, restored可以看出它强调的就是状态的undo,如何做到这一点呢,靠的就是pure函数。pure函数是我们熟悉的了:对于相同的输入值,能够产生相同的输出,并且不依赖外部上下文。

关于Redux名字的讨论,有兴趣的可以看下这个帖子 Redux? Why is it called that? ,全当娱乐了。

下面我们来重点看下Redux组成。它主要分为三个部分 Action、Reducer、及 Store。先看下Reducer,根据名字可以看出来它是类似Reduce的角色。Reduce来源于函数式编程,参考MSDN 的定义,它会对数组中的所有元素调用指定的回调函数。该回调函数的返回值为累积结果,并且此返回值在下一次调用该回调函数时作为参数提供。方法签名如下:

array1.reduce(callbackfn[, initialValue])

这里的callbackfn就是reducer。由此我们可以模仿上面方法签名得出如下表达式:

Final State = [Action1, Action2, ..., ActionN].reduce(reducer, Initial State);

这就形成了Redux的基本核心思路:通过对Action数组的reduce处理,得到最终的状态。不仅如此,为了动作可控,Redux还定义了三个原则:

  1. 单一数据源
  2. State 是只读的
  3. 使用纯函数来执行修改

三个原则中都是针对数据的规范,由此我们可以得出结论,数据就是Redux的心脏,所有动作都是围绕它来做的。

Action和store

说完reducer之后我们再看下Action和store。按照官方所述,Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

Action长啥样子呢?

{
  type: ADD_TODO,
  content: ''
}

可以看到就是一个普通的带有type的对象,type用来区分动作,其他都为参数。那store又是啥呢,很明显,它是对reducer的进一步混装,要想调用reducer里面的方法,必须走store的dispatch方法。

自己实现Redux -- state和action

理解了Redux的核心**后,我们自己动手来实现一个Redux!

因为数据如此重要,我们首先从它开始入手。比如我们想开发一个TODO list,按照Redux单一数据源原则,我们定义如下:

window.state = {
    todos: [],
    nextId: 1
}

按照上面对Action的理解,我们定义如下Action:

{type: `ADD_TODO`}
{type: `UPDATE_TODO`, id:1, content: 'xx' }

因为按照第二个和第三个原则,我们不能直接修改state,所以我们要定义下纯函数:

add(state){
    state.todos[state.nextId] = {
        id: state.nextId,
        content: `TODO${state.nextId}`
      };
    state.nextId++;
    return state;
}
update(state, action){
    state.todos[action.id].content = action.content;
    return state;
}

但是你会发现这么写是有问题的,因为你直接修改了state的值,而对象的引用并没有变,这就无法做到undo了,所以我们必须引入新的概念,那就是Immutability。

add(state){
    const id = state.nextId;
    const newTODO = {
        id,
        content: ''
    };
    return {
        ...state,
        nextId: id + 1,
        todos: {
          ...state.todos,
          [id]: newTODO
        }
    };
}
update(state, action){
    const {id, content} = action;
    const editedTODO = {
        ...state.todos[id],
        content
    };
    return {
        ...state,
        todos: {
          ...state.todos,
          [id]: editedTODO
        }
    };
}

reducer

那现在我们就把它们封装到reducer中了

const CREATE_TODO = 'CREATE_TODO';
const UPDATE_TODO = 'UPDATE_TODO';
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case CREATE_TODO: {
      const id = state.nextId;
      const newTODO = {
        id,
        content: ''
      };
      return {
        ...state,
        nextId: id + 1,
        todos: {
          ...state.todos,
          [id]: newTODO
        }
      };
    }
    case UPDATE_TODO: {
      const {id, content} = action;
      const editedTODO = {
        ...state.todos[id],
        content
      };
      return {
        ...state,
        todos: {
          ...state.todos,
          [id]: editedTODO
        }
      };
    }
    default:
      return state;
  }
};

现在我们用自己的reducer测试下:

const state0 = reducer(undefined, {
  type: CREATE_TODO
});

我们可以看到state0为如下结果:

{nextId:2, todos:{id: 1, content: ''}}

我们再测试下UPDATE方法:

const state1  = reducer(state0, {
  type: UPDATE_TODO,
  id: 1,
  content: 'Hello, world!'
});

我们可以看到state1为如下结果:

{nextId:2, todos:{id: 1, content: 'Hello, world!'}}

看看,测试起来都非常方便。那这两个Action如何一起调用了,很简单:

const actions = [
  {type: CREATE_TODO},
  {type: UPDATE_TODO, id: 1, content: 'Hello, world!'}
];

const state = actions.reduce(reducer, undefined);

我们得到了同样的结果:

{nextId:2, todos:{id: 1, content: 'Hello, world!'}}

store

完成action和reducer的构造之后,我们再来构造store。因为我们已经知道,调用reducer要走dispatch,所以先给出如下结构:

const createStore = (reducer, preloadedState) => {
  let currentState = undefined;
  return {
    dispatch: (action) => {
      currentState = reducer(preloadedState, action);
    },
    getState: () => currentState
  };
};

增加下测试方法:

const store = createStore(reducer, window.state);
store.dispatch({
  type: CREATE_TODO
});
console.log(store.getState());

非常赞,已经有了Redux的影子了,但是光有这些还不够。当数据发生变化时候,必须要进行通知,不然就没法进行界面渲染了。我们修改createStore如下:

const createStore = (reducer, preloadedState) => {
  let currentState = undefined;
  let nextListeners = [];
  return {
    dispatch: (action) => {
      currentState = reducer(preloadedState, action);
      nextListeners.forEach(handler => handler());
    },
    getState: () => currentState,
    subscribe: handler => {
	    nextListeners.push(listener)

	    return function unsubscribe() {
	      var index = nextListeners.indexOf(listener)
	      nextListeners.splice(index, 1)
	    }
     }

  };
};

添加listener到nextListeners后,返回unsubscribe,以供取消订阅。我们写下renderDOM:

store.subscribe(() => {
  ReactDOM.render(
    <div>{JSON.stringify(store.getState())}</div>,
    document.getElementById('root')
  );
});

到此,一个基本的Redux已经完成了。

那如何在React的组件中使用我们自己创建的store呢。只要把store作为属性传递进去就可以了:

const TODOApp = ({todos, handeladd, handeledit}) => (
  <div>
    <ul>
    {
      todos && Object.keys(todos).map((id, content) => (
        <li key={id}>{content}</li>
      ))
    }
    </ul>
    <button onClick={handeladd}>add</button>
  </div>
);

class TODOAppContainer extends React.Component {
  constructor(props) {
    super();
    this.state = props.store.getState();
    this.handeladd = this.handeladd.bind(this);
    this.handeledit = this.handeledit.bind(this);
  }
  componentWillMount() {
    this.unsubscribe = this.props.store.subscribe(() =>
      this.setState(this.props.store.getState())
    );
  }
  componentWillUnmount() {
    this.unsubscribe();
  }
  handeladd() {
    this.props.store.dispatch({
      type: CREATE_TODO
    });
  }
  handeledit(id, content) {
    this.props.store.dispatch({
      type: UPDATE_TODO,
      id,
      content
    });
  }
  render() {
    return (
      <TODOApp
        {...this.state}
        handeladd={this.handeladd}
        handeledit={this.handeledit}
      />
    );
  }
}

ReactDOM.render(
  <TODOAppContainer store={store}/>,
  document.getElementById('root')
);

Provider 和 Connect

但是这样的做法显示是耦合太重了,我们针对React专门提供 Provider 和 Connect 方法,这就是 react-redux。

参考react-redux的做法,我们首先来新建一个Provider来包括APP,它的主要作用就是让store传递到所有子节点上去,getChildContext真是可以做这样的功能。

class Provider extends React.Component {
  getChildContext() {
    return {
      store: this.props.store
    };
  }
  render() {
    return this.props.children;
  }
}

另外,对于store中的数据变化要反映到组件中,我们通过connect来完成。根据定义,connect的方法签名如下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

我们重点关注前面两个参数。

const connect = (mapStateToProps, mapDispatchToProps) => {

	return (Component) => {

	  	class Connected extends React.Component {

		    onStoreOrPropsChange(props) {
		      const {store} = this.context;
		      const state = store.getState();
		      const stateProps = mapStateToProps(state, props);
		      const dispatchProps = mapDispatchToProps(store.dispatch, props);
		      this.setState({
		        ...stateProps,
		        ...dispatchProps
		      });
		    }

		    componentWillMount() {
		      const {store} = this.context;
		      this.onStoreOrPropsChange(this.props);
		      this.unsubscribe = store.subscribe(() =>
		        this.onStoreOrPropsChange(this.props)
		      );
		    }

		    componentWillReceiveProps(nextProps) {
		      this.onStoreOrPropsChange(nextProps);
		    }

		    componentWillUnmount() {
		      this.unsubscribe();
		    }

		    render() {
		      return <Component {...this.props} {...this.state}/>;
		    }
	  	}

	  	Connected.contextTypes = {
			store: PropTypes.object
		};

	  	return Connected;
	}
};

调用方式:

const TODOAppContainer = connect(
	mapStateToProps,
	mapDispatchToProps
)(TODOApp);

再加上Provider

ReactDOM.render(
  <Provider store={store}>
    <TODOAppContainer/>
  </Provider>,
  document.getElementById('root')
);

至此我们已经构造了一个同步的Redux了。

完整代码路径:https://github.com/jnotnull/build-your-own-redux

参考文章:

  1. https://blog.gisspan.com/2017/02/Redux-Vs-MVC,-Why-and-How.html
  2. https://fakefish.github.io/react-webpack-cookbook/
  3. https://blog.pusher.com/the-what-and-why-of-redux/
  4. https://zapier.com/engineering/how-to-build-redux/
  5. http://community.pearljam.com/discussion/95759/redux-why-is-it-called-that
  6. http://www.avitzurel.com/blog/2016/08/03/connected-higher-order-components-with-react-and-redux/

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.