开坛

微信应用号开发知识贮备之打通React Component任督二脉

天地会珠海分舵注:随着微信应用号的呼之欲出,相信新一轮的APP变革即将发生。作为行业内人士,我们很应该去拥抱这个趋势。这段时间在忙完工作之余准备储备一下这方面的知识点,以免将来被微信应用号的浪潮所淹没。

通过上一篇《微信应用号开发知识贮备之Webpack实战》,我们成功的将altjs官方实例教程alt-tutorials的打包工具从browserify切换到当前更火的更接近nodejs编写习惯的weback上来,并且在该过程中学习了webpack相关的基础知识,以便我们今后的扩展。

从今天开始我的目标是通过对项目源码的分析,来学习reactjs和altjs相关的基础知识。但是开始之前,希望大家先去看一下文章最后提及的准备知识,有了这样的基础我们再来看代码就事半功倍了。同时本文的原理之外的一些阐述会引用到这些文章,此处先对这些先行者道声感谢。

今天的目标是分析AltContainer的实现原理,看它是如何将React Component组件的编程复杂度大幅度的降低,却大幅度的提升其代码的可复用性的。

开始之前我们先看下alt和flux的一些基础知识。

1. flux 和 alt

根据官网的描述,flux是facebook用来构建web前端应用的应用架构,它通过数据的单一方向流动,来对react组件式编程(React的view都是通过组件来组织起来的)进行一个补充。而事实上flux更像是一种设计模式(如mvc),而非一个框架。

Flux is the application architecture that Facebook uses for building client-side web applications. It complements React’s composable view components by utilizing a unidirectional data flow. It’s more of a pattern rather than a formal framework, and you can start using Flux immediately without a lot of new code.

所以围绕flux的这个设计模式,会有很多不同的实现。其中alt就是众多实现中的佼佼者。

那么上面说的单一方向数据流动又是怎么回事呢?这里我们很有必要看下flux的一些基本概念。

1.1. 为什么我们需要flux

首先,为什么我们需要flux?根据Andrew Ray 的文章《Flux For Stupid People》的说法,那是因为JavaScript开发前端拥有着各种的库,什么AngularJSjQuery,Backbone,但是,我们依然没有一个很好的框架能将数据流在项目中很好的组织起来,让我们以更好的方式进行开发。

On the Front End™ (HTML, Javascript, CSS), we don’t even have that. Instead we have a big problem: No one knows how to structure a Front End™ application. I’ve worked in this industry for years, and “best practices” are never taught. Instead, “libraries” are taught. jQuery? Angular? Backbone? The real problem, data flow, still eludes us.

作为一个新手,我在这里没有太多的发言权来评判究竟Andrew Ray的说法是否正确,期待有识之士在文章评论下面提出宝贵意见。暂时我先默认他的说法是对的。

那么一言以蔽之,flux就是为了更好的为web前端应用程序处理数据流而生的,且这些数据流都是单向流动的。

1.2. 基本概念

从官方提供的示意图来看,Flux将一个应用分成四个部分。

View: 视图层,负责界面的呈现
Action:动作/命令,代表视图层发出的命令,一个Action其实就是一个javascript的对象,包含了命令和该命令携带的数据。比如点击某个按钮会触发mouseClick事件,在事件处理代码中我们可以触发一个action命令
Dispatcher:用来接收Actions,将 Action 派发到 Store。一个Dispatcher事实上就是一个事件系统,是Actions和Store之间的桥梁,将Actions作为事件广播出去,然后由注册了监听的对应Store接收处理。
Store:用来存放应用的状态,一旦发生变动,就提醒Views要更新页面

那么上面说的数据的单向流动又是怎么一回事呢?其实我们看上面的数据流向的箭头就明白了,数据都是单方向往下走的,由Dispatcher->Store->View->Action,再到Dispatcher,完成了一个不停循环的闭环。这样的话,数据流向就非常清晰,不会造成混乱。

拿我们的alt-tutorial为例子:

现实中的流程就是:

  1. 用户访问 View。比如我们点击alt-tutorial上面的favorite按钮
  2. View 发出用户的 Action。比如点击favorite按钮后发送出一个添加收藏的地点(FavoriteLocation)的命令,命令中携带着该Location的数据,如名称和编号
  3. Dispatcher 收到 Action,要求 Store 进行相应的更新。比如把收藏的地点作为一个state存储在内存中。
  4. Store 更新后,发出一个”change”事件。
  5. View 收到”change”事件后,更新页面。这时我们就能看到新增加的收藏地点显示在页面下方了。

再次强调,更具体的实例和描述,强烈建议大家查看文章后面的给出的文章链接,相信没有谁比他们写得更清楚的了,我自己就更不敢班门弄斧了!

1.3. alt 简介

alt就是flux的一种实现,所以上面flux的概念alt做了更高层的封装,以方便我们更方便的使用。比如,我们基本上不需要实现dispatcher相关的代码,因为alt已经帮我们封装起来,我们只需要根据其框架来填写对应的代码就完了。

相信这系列往后的文章会逐一接触到alt的不同的特性,但今天我们先解决alt的AltContainer这个特性。因为,只有弄清楚了它,我们才能真正明白原来有了alt之后,组件component的编写竟然能变得如此的简单。

2. AltContainer的目的和职责

根据上面flux的描述,在组件中应该去监听Store中state状态的改变。只要View触发一个Action, Dispatcher发现需要更新相应的Store的状态,然后就会通知相应的Store作状态的改变,同时通过Store通知View状态已经发生变化,View监听到后就会进行页面的重新渲染。

我们打开src/components/Location.jsx文件,纵观整个组件文件,我们其实并没有看到任何监听相关的代码。

var React = require('react');
var AltContainer = require('alt-container');
var LocationStore = require('../stores/LocationStore');
var FavoritesStore = require('../stores/FavoritesStore');
var LocationActions = require('../actions/LocationActions');

var Favorites = React.createClass({
  render() {
    return (
      <ul>
        {this.props.locations.map((location, i) => {
          return (
            <li key={i}>{location.name}</li>
          );
        })}
      </ul>
    );
  }
});

var AllLocations = React.createClass({
  addFave(ev) {
    var location = LocationStore.getLocation(
      Number(ev.target.getAttribute('data-id'))
    );
    LocationActions.favoriteLocation(location);
  },

  render() {
    if (this.props.errorMessage) {
      return (
        <div>{this.props.errorMessage}</div>
      );
    }

    if (LocationStore.isLoading()) {
      return (
        <div>
          <img src="ajax-loader.gif" />
        </div>
      )
    }

    return (
      <ul>
        {this.props.locations.map((location, i) => {
          var faveButton = (
            <button onClick={this.addFave} data-id={location.id}>
              Favorite
            </button>
          );

          return (
            <li key={i}>
              {location.name} {location.has_favorite ? '<3' : faveButton}
            </li>
          );
        })}
      </ul>
    );
  }
});

var Locations = React.createClass({
  componentDidMount() {
    LocationStore.fetchLocations();
  },

  render() {
    return (
      <div>
        <h1>Locations</h1>
        <AltContainer store={LocationStore}>
          <AllLocations />
        </AltContainer>

        <h1>Favorites</h1>
        <AltContainer store={FavoritesStore}>
          <Favorites />
        </AltContainer>
      </div>
    );
  }
});

module.exports = Locations;

其实这里的玄机就是下面的AltContainer那段代码:

        <AltContainer store={LocationStore}>
          <AllLocations />
        </AltContainer>

从这个写法我们可以知道,AltContainer就是React的一个组件,它有一个props 属性”store={LocationStore}”,且拥有一个子组件AllLocations,所以我们把AltContainer称作是一个容器。AllLocations这个子组件的主要作用就是从LocationStore中取出所有的Location然后进行渲染。

  render() {
    ...

    return (
      <ul>
        {this.props.locations.map((location, i) => {
          var faveButton = (
            <button onClick={this.addFave} data-id={location.id}>
              Favorite
            </button>
          );
        ...
        })}
      </ul>
    );
  }

但是从上面的代码我们可以看到,子组件明明是从自身的props属性中取得Locations,为什么说是从LocationStore中取得呢?况且LocationStore保存的是state而不是props啊?

答案还是AltContainer! 神奇吧!?

个人认为,AltContainer存在的主要目的主要是一个:

  • 通过职责的解绑,让Component的尽可能的关注在如何进行页面渲染的逻辑上去,而不需要去管该如何获取数据,该如何监听状态是否改变是否需要重新渲染的逻辑,从而让整个组件更容易重用!

因为AltContainer是作为Components的一个容器,一个父组件存在的。所以,只要你把一个只是包含渲染逻辑的组件丢给它,并告诉它需要监听的是哪个(些)Store(s),它就能帮该子组件建立好对相应Store的监听,且把Store的状态数据告诉该子组件,让其进行界面的重新渲染。

所以,AltContainer的主要职责应该是两个:

  • 帮子组件建立对相应的Store的监听,以便Store状态state发生改变时,可以通知到该子组件进行重新渲染
  • 将Store的状态state数据取出来作为子组件的props,来让子组件知道应该去什么数据来进行界面渲染。这里因为子组件只是负责该如何渲染页面,而不需要对数据作任何修改(需要修改的话是通过发送Actions来做的),所以这里用的是子组件的props,而不是state。

3. 源码分析-AltContainer原理

那么AltContainer是如何达成这两个职责的呢?我们不妨跟踪一下其实现源码,如果大家确实对其原理不是很感兴趣的,可以跳过这章。

3.1. AltContainer如何帮助子组件建立Store监听

首先我们看下AltContainer是如何帮助子组件建立对Store的监听的。

_createClass(AltContainer, [
    ..., {
    key: 'componentDidMount',
    value: function componentDidMount() {
      this._registerStores(this.props);
      if (this.props.onMount) this.props.onMount(this.props, this.context);
    }
  },
  ...

AltContainer作为我们的Component子组件AllLocations的父组件,在挂载完成时,会调用成员函数“this._registerStores(this.props)“。

这里的this.props包含我们前面传入的LocationStore(this.props.store):

<AltContainer store={LocationStore}>

那么往下我们看下_registerStores这个成员函数做了什么事情:

function _registerStores(props) {
      var _this2 = this;

      var stores = props.stores;

      if (props.store) {
        this._addSubscription(props.store);
      }
      ...

取出了我们传进去的LocationStore,然后作为参数调用另外一个成员函数_addSubscription。继续往下跟踪:

function _addSubscription(getStore) {
      var store = typeof getStore === 'function' ? getStore(this.props).store : getStore;

      this.storeListeners.push(store.listen(this.altSetState));
    }

这个函数的第一句,首先去判断我们传进来的store是否是一个函数(注意altcontainer支持传进来的是funciton的,详情可以查看AltContainer官方文档),我们这里传进来的是LocationStore,是一个类(应该是类实例),所以这里store变量被赋予的是getStore,在我们这里也就是LocationStore。

第二句就是我们这里的关键点,它首先是通过调用store.listen建立起对store的监听,一旦监听到store的状态发生变化,立刻调用一个叫做altSetState的成员进行处理。

同时因为可能需要监听多个store,所以这里会将这些建立好的监听对象放到一个叫做storeListeners的栈中保存起来方便管理。

到了这里,我们已经清楚AltContainer是如何帮助我们建立好对store的监听的。但是,我们至此只是知道监听到改动后会有一个altSetState的函数进行处理,且这个函数还是属于AltContainer这个类的,而不是子组件的。

所以现在我们依然不清楚当store状态发生变化时,时如何引发组件的数据发生变化,从而自动触发页面的重新渲染的。

那我们就需要继续跟踪altSetState这个方法了,且这一部分和我们刚才说到的AltContainer的第二个作用息息相关,关系到AltContainer是如何“将Store的状态state数据取出来作为子组件的props,来让子组件知道应该去什么数据来进行界面渲染。“

3.2. AltContainer如何将Store的state转换成AltContainer自己的state

那么我们先继续跟踪altSetState的源码:

  this.altSetState = function () {
      _this.setState(reduceState(_this.props));
    };

很直接,内层函数调用reduceState方法来获取到相应的state,然后通过外层函数react的系统方法setState来将该state设置成AltContainer自身的state的成员变量。

那么reduceState方法是从哪里取到的状态数据了,是不是就是从我们预期的store中呢?我们继续跟踪:

var reduceState = function reduceState(props) {
  return (0, _objectAssign2['default'])({}, getStateFromStores(props), getStateFromActions(props), getInjected(props));
};

很幸运,我们看到返回的结果列表中,其中一个结果就是通过一个叫做getStateFromStores的函数返回的,顾名思义,这个应该就是我们要的结果。进去看看:

var getStateFromStores = function getStateFromStores(props) {
  var stores = props.stores;
  if (props.store) {
    return getStateFromStore(props.store, props);
  } else if (props.stores) {
    // If you pass in an array of stores then we are just listening to them
    // it should be an object then the state is added to the key specified
    if (!Array.isArray(stores)) {
      return Object.keys(stores).reduce(function (obj, key) {
        obj[key] = getStateFromStore(stores[key], props);
        return obj;
      }, {});
    }
  } else {
    return {};
  }
};

这个函数可以处理单个store和多个stores的情况:

<AltContainer store={LocationStore}>
          <AllLocations />
        </AltContainer>

以及可能的:

<AltContainer stores={{LocationStoreLocationStore,
                       FavoriteStore: FavoriteStore}}>
          <AllLocations />
        </AltContainer>

但在我们的alt-tutorial中,我们传进来的是第一种,所以,这里我们这里会直接调用getStateFromStore这个方法,其中传进去的第一个参数就是我们的store。

var getStateFromStore = function getStateFromStore(store, props) {
  return typeof store === 'function' ? store(props).value : store.getState();
};

跟上面分析的一样,我们这里的store是一个类而非一个函数,所以这里返回的是store.getState(), 这里的store就是我们的LocationStore,getState函数返回的就是对应store的state。

到了这里,我们弄清楚了AltContainer为子组件建立的store监听处理函数,在store数据状态发生改变时是如何处理的。结果就是将store的修改后状态state取出来,变成AltContainer自己的state!

但是这里我们还有个问题没有搞清楚,AltContainer自己的state是如何变成子组件Component的props的?

3.3. AltContainer如何将自身的state转换成子组件的props

这里的关键就是AltContainer的渲染方法render。如上面所分析,AltContainer将store的数据变化进行监听,一旦发生改变,就会将store最新的数据从store拷贝到自己的state上面,从而导致自身的state发生变化,最终导致自身的重新渲染,也就是调用AltContainer自身的render方法:

unction render() {
      var _this3 = this;

      var Node = 'div';
      var children = this.props.children;
      ...
      // Does not wrap child in a div if we don't have to.
      if (Array.isArray(children)) {
       ...
        }));
      } else if (children) {
        return _react2['default'].cloneElement(children, this.getProps());
      } 
      ...
      }
  }]);

this.props.children是react组件的一个属性,代表的是组件的所有子节点。回顾下上面的代码:

<AltContainer store={LocationStore}>
          <AllLocations />
        </AltContainer>

AltContainer是父组件,AllLocations是形成子节点的组件,所以这里的this.props.children指代的就是AllLocations.

那么上面的代码的意思就是,如果AltContainer有这个子节点,那么就调用react的cloneElement这个方法来。看名称和参数的话,这个方法的作用应该就是将从getProps方法获得返回结果克隆到对应的子组件。

我们首先看看AltContianer的getProps这个方法返回的是什么东东:

function getProps() {
      var flux = this.props.flux || this.context.flux;
      var transform = typeof this.props.transform === 'function' ? this.props.transform : id;
      return transform((0, _objectAssign2['default'])(flux ? { flux: flux } : {}, this.state));
    }

从之前的分析可以看到,这里我们alt-tutorial传进来的props只有两个,一个是this.props.store(也就是我们LocationStore),一个是this.props.children(也就是我们的AllLocations组件),所以这里并没有this.props.flux和this.props.transform,我也没有看到在哪里有定义了this.context.flux,所以最后一句应该可以在我们的情况下简化为:

function getProps() {
      return id((0, _objectAssign2['default'])(this.state));
    }

而这里的id只是将输入参数做简单的返回:

var id = function id(it) {
  return it;
};

所以最终geProps返回的结果就是AltContainer自身的this.state。

那么我们继续分析上面的cloneElement方法:

ReactElement.cloneElement = function (element, config, children) {
  var propName;

  // Original props are copied
  var props = assign({}, element.props);
  ...
      // Remaining properties override existing props
    for (propName in config) {
      if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }

这里的第一个参数element指代的就是我们的AllLocation这个子组件,第二个参数config就是AltContainer的getProps方法的返回结果,也就是AltContainer的state。

从未省略掉的代码看来的话,cloneElement的其中一个功能就是,AltContainer的state列表,然后将其拷贝到目标element(也就是我们这里的AllLocations)的props。

到此,我们就分析完了AltContainer是如何将自己的state变成子组件的props的了。

4. 小结

关于flux和alt:

  • flux是一个为了解决javascript编写web前端的数据流控制的架构,存在多种不同的实现方式。
  • alt是flux的一种实现。

AltContainer存在的主要目的:

  • 通过职责的解绑,让Component的尽可能的关注在如何进行页面渲染的逻辑上去,而不需要去管该如何获取数据,该如何监听状态是否改变是否需要重新渲染的逻辑,从而让整个组件更容易重用!
    因为AltContainer是作为Components的一个容器,一个父组件存在的。

AltContainer的主要职责:

  • 帮子组件建立对相应的Store的监听,以便Store状态state发生改变时,可以通知到该子组件进行重新渲染
  • 将Store的状态state数据取出来作为子组件的props,来让子组件知道应该去什么数据来进行界面渲染。这里因为子组件只是负责该如何渲染页面,而不需要对数据作任何修改(需要修改的话是通过发送Actions来做的),所以这里用的是子组件的props,而不是state。

AltContainer原理简述:

  • AltContainer通过将store的监听处理函数指定为自身的成员方法altSetState, 将store的state的改动,映射成AltContainer这个父组件自身的state的改动。所以一旦store的state改动,必然会触发AltContainer自身的重新渲染,从而调用自身的render方法。
  • AltContainer通过React的cloneElement方法,将自身的state在每次AltContainer需要重新render时,复制到子组件的props上面。
  • 所以,每次store的状态state有改动,就会导致AltContainer重新渲染,从而就会导致子组件的重新渲染,同时子组件可以通过自身的props获取到绑定的store的所有state。

5. 准备知识和鸣谢


如果对reactjs和flux的基本概念不清楚的,建议先查看以下文章:

<<未完待续>>

喜欢 1

这篇文章还没有评论

发表评论