Flux

React 标榜自己是 MVC 里面 V 的部分,那么 Flux 就相当于添加 M 和 C 的部分。

Flux 是 Facebook 使用的一套前端应用的架构模式。

一个 Flux 应用主要包含四个部分:

  • the dispatcher处理动作分发,维护 Store 之间的依赖关系
  • the stores数据和逻辑部分
  • the viewsReact 组件,这一层可以看作 controller-views,作为视图同时响应用户交互
  • the actions提供给 dispatcher 传递数据给 store

针对上面提到的 Flux 这些概念,需要写一个简单的类库来实现衔接这些功能,市面上有很多种实现,这里讨论 Facebook 官方的一个实现 Dispatcher.js

单向数据流

先来了解一下 Flux 的核心“单向数据流“怎么运作的:

Action -> Dispatcher -> Store -> View

更多时候 View 会通过用户交互触发 Action,所以一个简单完整的数据流类似这样:

flux overview

整个流程如下:

  • 首先要有 action,通过定义一些 action creator 方法根据需要创建 Action 提供给 dispatcher
  • View 层通过用户交互(比如 onClick)会触发 Action
  • Dispatcher 会分发触发的 Action 给所有注册的 Store 的回调函数
  • Store 回调函数根据接收的 Action 更新自身数据之后会触发一个 change 事件通知 View 数据更改了
  • View 会监听这个 change 事件,拿到对应的新数据并调用 setState 更新组件 UI

所有的状态都由 Store 来维护,通过 Action 传递数据,构成了如上所述的单向数据流循环,所以应用中的各部分分工就相当明确,高度解耦了。

这种单向数据流使得整个系统都是透明可预测的。

Dispatcher

一个应用只需要一个 dispatcher 作为分发中心,管理所有数据流向,分发动作给 Store,没有太多其他的逻辑(一些 action creator 方法也可以放到这里)。

Dispatcher 分发动作给 Store 注册的回调函数,这和一般的订阅/发布模式不同的地方在于:

  • 回调函数不是订阅到某一个特定的事件/频道,每个动作会分发给所有注册的回调函数
  • 回调函数可以指定在其他回调之后调用

基于 Flux 的架构思路,Dispatcher.js 提供的 API 很简单:

  • register(function callback): string 注册回调函数,返回一个 token 供在 waitFor() 使用
  • unregister(string id): void 通过 token 移除回调
  • waitFor(array ids): void 在指定的回调函数执行之后才执行当前回调。这个方法只能在分发动作的回调函数中使用
  • dispatch(object payload): void 分发动作 payload 给所有注册回调
  • isDispatching(): boolean 返回 Dispatcher 当前是否处在分发的状态

dispatcher 只是一个粘合剂,剩余的 Store、View、Action 就需要按具体需求去实现了。

接下来结合 flux-todomvc 这个简单的例子,提取其中的关键部分,看一下实际应用中如何衔接 Flux 整个流程,希望能对 Flux 各个部分有更直观深入的理解。

Action

首先要创建动作,通过定义一些 action creator 方法来创建,这些方法用来暴露给外部调用,通过 dispatch 分发对应的动作,所以 action creator 也称作 dispatcher helper methods 辅助 dipatcher 分发。 参见 actions/TodoActions.js

var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');

var TodoActions = {
  create: function(text) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_CREATE,
      text: text
    });
  },

  updateText: function(id, text) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_UPDATE_TEXT,
      id: id,
      text: text
    });
  },

  // 不带 payload 数据的动作
  toggleCompleteAll: function() {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL
    });
  }
};

AppDispatcher 直接继承自 Dispatcher.js,在这个简单的例子中没有提供什么额外的功能。TodoConstants 定义了动作的类型名称常量。

类似 createupdateText 就是 action creator,这两个动作会通过 View 上的用户交互触发(比如输入框)。 除了用户交互会创建动作,服务端接口调用也可以用来创建动作,比如通过 Ajax 请求的一些初始数据也可以创建动作提供给 dispatcher,再分发给 store 使用这些初始数据。

action creators are nothing more than a call into the dispatcher.

可以看到所谓动作就是用来封装传递数据的,动作只是一个简单的对象,包含两部分:payload(数据)和 type(类型),type 是一个字符串常量,用来标识动作。

Store

Stores 包含应用的状态和逻辑,不同的 Store 管理应用中不同部分的状态。如 stores/TodoStore.js

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var TodoConstants = require('../constants/TodoConstants');
var assign = require('object-assign');

var CHANGE_EVENT = 'change';

var _todos = {};

// 先定义一些数据处理方法
function create(text) {
  var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);
  _todos[id] = {
    id: id,
    complete: false,
    text: text
  };
}
function update(id, updates) {
  _todos[id] = assign({}, _todos[id], updates);
}
// ...

var TodoStore = assign({}, EventEmitter.prototype, {
  // Getter 方法暴露给外部获取 Store 数据
  getAll: function() {
    return _todos;
  },
  // 触发 change 事件
  emitChange: function() {
    this.emit(CHANGE_EVENT);
  },
  // 提供给外部 View 绑定 change 事件
  addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  }
});

// 注册到 dispatcher,通过动作类型过滤处理当前 Store 关心的动作
AppDispatcher.register(function(action) {
  var text;

  switch(action.actionType) {
    case TodoConstants.TODO_CREATE:
      text = action.text.trim();
      if (text !== '') {
        create(text);
      }
      TodoStore.emitChange();
      break;

    case TodoConstants.TODO_UPDATE_TEXT:
      text = action.text.trim();
      if (text !== '') {
        update(action.id, {text: text});
      }
      TodoStore.emitChange();
      break;
  }
});

在 Store 注册给 dispatcher 的回调函数中会接受到分发的 action,因为每个 action 都会分发给所有注册的回调,所以回调函数里面要判断这个 action 的 type 并调用相关的内部方法处理更新 action 带过来的数据(payload),再通知 view 数据变更。

Store 里面不会暴露直接操作数据的方法给外部,暴露给外部调用的方法都是 Getter 方法,没有 Setter 方法,唯一更新数据的手段就是通过在 dispatcher 注册的回调函数。

View

View 就是 React 组件,从 Store 获取状态(数据),绑定 change 事件处理。如components/TodoApp.react.js

var React = require('react');
var TodoStore = require('../stores/TodoStore');

function getTodoState() {
  return {
    allTodos: TodoStore.getAll(),
    areAllComplete: TodoStore.areAllComplete()
  };
}

var TodoApp = React.createClass({

  getInitialState: function() {
    return getTodoState();
  },

  componentDidMount: function() {
    TodoStore.addChangeListener(this._onChange);
  },

  componentWillUnmount: function() {
    TodoStore.removeChangeListener(this._onChange);
  },

  render: function() {
    return <div>/*...*/</div>
  },

  _onChange: function() {
    this.setState(getTodoState());
  }
});

一个 View 可能关联多个 Store 来管理不同部分的状态,得益于 React 更新 View 如此简单(setState),复杂的逻辑都被 Store 隔离了。

更多资料

Redux

Dan Abramov 在 React Europe 2015 上作了一场令人印象深刻的演示 Hot Reloading with Time Travel,之后 Redux 迅速成为最受人关注的 Flux 实现之一。

Redux 把自己标榜为一个“可预测的状态容器”,其实也是 Flux 里面“单向数据流”的思想,只是它充分利用函数式的特性,让整个实现更加优雅纯粹,使用起来也更简单。

Redux(oldState) => newState

Redux 是超越 Flux 的一次进化。

进化 Flux

我们可以先通过对比 Redux 和 Flux 的实现来感受一下 Redux 带来的惊艳。

首先是 action creators,Flux 是直接在 action 里面调用 dispatch:

export function addTodo(text) {
  AppDispatcher.dispatch({
    type: ActionTypes.ADD_TODO,
    text: text
  });
}

Redux 把它简化成了这样:

export function addTodo(text) {
  return {
    type: ActionTypes.ADD_TODO,
    text: text
  };
}

这一步把 dispatcher 和 action 解藕了,很快我们就能看到它带来的好处。

接下来是 Store,这是 Flux 里面的 Store:

let _todos = [];
const TodoStore = Object.assign(new EventEmitter(), {
  getTodos() {
    return _todos;
  }
});
AppDispatcher.register(function (action) {
  switch (action.type) {
  case ActionTypes.ADD_TODO:
    _todos = _todos.concat([action.text]);
    TodoStore.emitChange();
    break;
  }
});
export default TodoStore;

Redux 把它简化成了这样:

const initialState = { todos: [] };
export default function TodoStore(state = initialState, action) {
  switch (action.type) {
  case ActionTypes.ADD_TODO:
    return { todos: state.todos.concat([action.text]) };
  default:
    return state;
}

同样把 dispatch 从 Store 里面剥离了,Store 变成了一个 pure function(state, action) => state

什么是 pure function

如果一个函数没有任何副作用(side-effects),不会影响任何外部状态,对于任何一个相同的输入(参数),无论何时调用这个函数总是返回同样的结果,这个函数就是一个 pure function。所谓 side-effects 就是会改变外部状态的因素 ,比如 Ajax 请求就有 side-effects,因为它带来了不确定性。

所以现在 Store 不再拥有状态,而只是管理状态,所以首先要明确一个概念,Store 和 State 是有区别的,Store 并不是一个简单的数据结构,State 才是,Store 会包含一些方法来管理 State,比如获取/修改 State。

基于这样的 Store,可以做很多扩展,这也是 Redux 强大之处。

Redux 的基础概念

三个基本原则

  • 整个应用只有唯一一个可信数据源,也就是只有一个 Store
  • State 只能通过触发 Action 来更改
  • State 的更改必须写成纯函数,也就是每次更改总是返回一个新的 State,在 Redux 里这种函数称为 Reducer

Actions

Action 很简单,就是一个单纯的包含 { type, payload } 的对象,type 是一个常量用来标示动作类型,payload 是这个动作携带的数据。Action 需要通过 store.dispatch() 方法来发送。

比如一个最简单的 action:

{
  type: 'ADD_TODO',
  text: 'Build my first Redux app'
}

一般来说,会使用函数(Action Creators)来生成 action,这样会有更大的灵活性,Action Creators 是一个 pure function,它最后会返回一个 action 对象:

function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

所以现在要触发一个动作只要调用 dispatchdispatch(addTodo(text))

稍后会讲到如何拿到 store.dispatch

Reducers

Reducer 用来处理 Action 触发的对状态树的更改。

所以一个 reducer 函数会接受 oldState 和 action 两个参数,返回一个新的 state:(oldState, action) => newState。一个简单的 reducer 可能类似这样:

const initialState = {
  a: 'a',
  b: 'b'
};

function someApp(state = initialState, action) {
  switch (action.type) {
    case 'CHANGE_A':
      return { ...state, a: 'Modified a' };
    case 'CHANGE_B':
      return { ...state, b: action.payload };
    default:
      return state
  }
}

值得注意的有两点:

  • 我们用到了 object spread 语法 确保不会更改到 oldState 而是返回一个 newState
  • 对于不需要处理的 action,直接返回 oldState

Reducer 也是 pure function,这点非常重要,所以绝对不要在 reducer 里面做一些引入 side-effects 的事情,比如:

  • 直接修改 state 参数对象
  • 请求 API
  • 调用不纯的函数,比如 Data.now() Math.random()

因为 Redux 里面只有一个 Store,对应一个 State 状态,所以整个 State 对象就是由一个 reducer 函数管理,但是如果所有的状态更改逻辑都放在这一个 reducer 里面,显然会变得越来越巨大,越来越难以维护。得益于纯函数的实现,我们只需要稍微变通一下,让状态树上的每个字段都有一个 reducer 函数来管理就可以拆分成很小的 reducer 了:

function someApp(state = {}, action) {
  return {
    a: reducerA(state.a, action),
    b: reducerB(state.b, action)
  };
}

对于 reducerA 和 reducerB 来说,他们依然是形如:(oldState, action) => newState 的函数,只是这时候的 state 不是整个状态树,而是树上的特定字段,每个 reducer 只需要判断 action,管理自己关心的状态字段数据就好了。

Redux 提供了一个工具函数 combineReducers 来简化这种 reducer 合并:

import { combineReducers } from 'redux';

const someApp = combineReducers({
  a: reducerA,
  b: reducerB
});

如果 reducer 函数名字和字段名字相同,利用 ES6 的 Destructuring 可以进一步简化成:combineReducers({ a, b })

象 someApp 这种管理整个 State 的 reducer,可以称为 root reducer

Store

现在有了 Action 和 Reducer,Store 的作用就是连接这两者,Store 的作用有这么几个:

  • Hold 住整个应用的 State 状态树
  • 提供一个 getState() 方法获取 State
  • 提供一个 dispatch() 方法发送 action 更改 State
  • 提供一个 subscribe() 方法注册回调函数监听 State 的更改

创建一个 Store 很容易,将 root reducer 函数传递给 createStore 方法即可:

import { createStore } from 'redux';
import someApp from './reducers';
let store = createStore(someApp);

// 你也可以额外指定一个初始 State(initialState),这对于服务端渲染很有用
// let store = createStore(someApp, window.STATE_FROM_SERVER);

现在我们就拿到了 store.dispatch,可以用来分发 action 了:

let unsubscribe = store.subscribe(() => console.log(store.getState()));

// Dispatch
store.dispatch({ type: 'CHANGE_A' });
store.dispatch({ type: 'CHANGE_B', payload: 'Modified b' });

// Stop listening to state updates
unsubscribe();

Data Flow

以上提到的 store.dispatch(action) -> reducer(state, action) -> store.getState() 其实就构成了一个“单向数据流”,我们再来总结一下。

1. 调用 store.dispatch(action)

Action 是一个包含 { type, payload } 的对象,它描述了“发生了什么”,比如:

{ type: 'LIKE_ARTICLE', articleID: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: 'Read the Redux docs.' }

你可以在任何地方调用 store.dispatch(action),比如组件内部,Ajax 回调函数里面等等。

2. Action 会触发给 Store 指定的 root reducer

root reducer 会返回一个完整的状态树,State 对象上的各个字段值可以由各自的 reducer 函数处理并返回新的值。

  • reducer 函数接受 (state, action) 两个参数
  • reducer 函数判断 action.type 然后处理对应的 action.payload 数据来更新并返回一个新的 state

3. Store 会保存 root reducer 返回的状态树

新的 State 会替代旧的 State,然后所有 store.subscribe(listener) 注册的回调函数会被调用,在回调函数里面可以通过 store.getState() 拿到新的 State。

这就是 Redux 的运作流程,接下来看如何在 React 里面使用 Redux。

在 React 应用中使用 Redux

和 Flux 类似,Redux 也是需要注册一个回调函数 store.subscribe(listener) 来获取 State 的更新,然后我们要在 listener 里面调用 setState() 来更新 React 组件。

Redux 官方提供了 react-redux 来简化 React 和 Redux 之间的绑定,不再需要像 Flux 那样手动注册/解绑回调函数。

接下来看一下是怎么做到的,react-redux 只有两个 API

<Provider>

<Provider> 作为一个容器组件,用来接受 Store,并且让 Store 对子组件可用,用法如下:

import { render } from 'react-dom';
import { Provider } from 'react-redux';
import App from './app';

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

这时候 <Provider> 里面的子组件 <App /> 才可以使用 connect 方法关联 store。

<Provider> 的实现很简单,他利用了 React 一个(暂时)隐藏的特性 ContextsContext 用来传递一些父容器的属性对所有子孙组件可见,在某些场景下面避免了用 props 传递多层组件的繁琐,要想更详细了解 Contexts 可以参考这篇文章

Connect

connect() 这个方法略微复杂一点,主要是因为它的用法非常灵活:connect([mapStateToProps], mapDispatchToProps], [mergeProps], [options]),它最多接受4个参数,都是可选的,并且这个方法调用会返回另一个函数,这个返回的函数来接受一个组件类作为参数,最后才返回一个和 Redux store 关联起来的新组件,类似这样:

class App extends Component { ... }

export default connect()(App);

这样就可以在 App 这个组件里面通过 props 拿到 Store 的 dispatch 方法,但是注意现在的 App 没有监听 Store 的状态更改,如果要监听 Store 的状态更改,必须要指定 mapStateToProps 参数。

先来看它的参数:

  • [mapStateToProps(state, [ownProps]): stateProps]: 第一个可选参数是一个函数,只有指定了这个参数,这个关联(connected)组件才会监听 Redux Store 的更新,每次更新都会调用 mapStateToProps这个函数,返回一个字面量对象将会合并到组件的 props 属性。 ownProps 是可选的第二个参数,它是传递给组件的 props,当组件获取到新的 props 时,ownProps 都会拿到这个值并且执行 mapStateToProps 这个函数。
  • [mapDispatchProps(dispatch, [ownProps]): dispatchProps]: 这个函数用来指定如何传递 dispatch给组件,在这个函数里面直接 dispatch action creator,返回一个字面量对象将会合并到组件的 props属性,这样关联组件可以直接通过 props 调用到 action, Redux 提供了一个 bindActionCreators() 辅助函数来简化这种写法。 如果省略这个参数,默认直接把 dispatch 作为 props 传入。ownProps 作用同上。

剩下的两个参数比较少用到,更详细的说明参看官方文档,其中提供了很多简单清晰的用法示例来说明这些参数。

一个具体一点的例子

Redux 创建 Store,Action,Reducer 这部分就省略了,这里只看 react-redux 的部分。

import React, { Component } from 'react';
import someActionCreator from './actions/someAction';
import * as actionCreators from './actions/otherAction';

function mapStateToProps(state) {
  return {
    propName: state.propName
  };
}

function mapDispatchProps(dispatch) {
  return {
    someAction: (arg) => dispatch(someActionCreator(arg)),
    otherActions: bindActionCreators(actionCreators, dispatch)
  };
}

class App extends Component {
  render() {
    // `mapStateToProps` 和 `mapDispatchProps` 返回的字段都是 `props`
    const { propName, someAction, otherActions } = this.props;
    return (
      <div onClick={someAction.bind(this, 'arg')}>
        {propName}
      </div>
    );
  }
}

export default connect(mapStateToProps, mapDispatchProps)(App);

如前所述,这个 connected 的组件必须放到 <Provider> 的容器里面,当 State 更改的时候就会自动调用 mapStateToProps 和 mapDispatchProps 从而更新组件的 props。 组件内部也可以通过 props 调用到 action,如果没有省略了 mapDispatchProps,组件要触发 action 就必须手动 dispatch,类似这样:this.props.dispatch(someActionCreator('arg'))

表单

表单不同于其他 HTML 元素,因为它要响应用户的交互,显示不同的状态,所以在 React 里面会有点特殊。

状态属性

表单元素有这么几种属于状态的属性:

  • value,对应 <input> 和 <textarea> 所有
  • checked,对应类型为 checkbox 和 radio 的 <input> 所有
  • selected,对应 <option> 所有

在 HTML 中 <textarea> 的值可以由子节点(文本)赋值,但是在 React 中,要用 value 来设置。

表单元素包含以上任意一种状态属性都支持 onChange 事件监听状态值的更改。

针对这些状态属性不同的处理策略,表单元素在 React 里面有两种表现形式。

受控组件

对于设置了上面提到的对应“状态属性“值的表单元素就是受控表单组件,比如:

render: function() {
    return <input type="text" value="hello"/>;
}

一个受控的表单组件,它所有状态属性更改涉及 UI 的变更都由 React 来控制(状态属性绑定 UI)。比如上面代码里的 <input> 输入框,用户输入内容,用户输入的内容不会显示(输入框总是显示状态属性 value的值 hello),这有点颠覆我们的认知了,所以说这是受控组件,不是原来默认的表单元素了。

如果你希望输入的内容反馈到输入框,就要用 onChange 事件改变状态属性 value 的值:

getInitialState: function() {
    return {value: 'hello'};
},
handleChange: function(event) {
    this.setState({value: event.target.value});
},
render: function() {
    var value = this.state.value;
    return <input type="text" value={value} onChange={this.handleChange} />;
}

使用这种模式非常容易实现类似对用户输入的验证,或者对用户交互做额外的处理,比如截断最多输入140个字符:

handleChange: function(event) {
    this.setState({value: event.target.value.substr(0, 140)});
}

非受控组件

和受控组件相对,如果表单元素没有设置自己的“状态属性”,或者属性值设置为 null,这时候就是非受控组件。

它的表现就符合普通的表单元素,正常响应用户的操作。

同样,你也可以绑定 onChange 事件处理交互。

如果你想要给“状态属性”设置默认值,就要用 React 提供的特殊属性 defaultValue,对于 checked 会有 defaultChecked<option> 也是使用 defaultValue

为什么要有受控组件?

引入受控组件不是说它有什么好处,而是因为 React 的 UI 渲染机制,对于表单元素不得不引入这一特殊的处理方式。

在浏览器 DOM 里面是有区分 attribute 和 property 的。attribute 是在 HTML 里指定的属性,而每个 HTML 元素在 JS 对应是一个 DOM 节点对象,这个对象拥有的属性就是 property(可以在 console 里展开一个 DOM 节点对象看一下,HTML attributes 只是对应其中的一部分属性),attribute 对应的 property 会从 attribute 拿到初始值,有些会有相同的名称,但是有些名称会不一样,比如 attribute class 对应的 property 就是 className。(详细解释:.prop.prop() vs .attr()

回到 React 里的 <input> 输入框,当用户输入内容的时候,输入框的 value property 会改变,但是 value attribute 依然会是 HTML 上指定的值(attribute 要用 setAttribute 去更改)。

React 组件必须呈现这个组件的状态视图,这个视图 HTML 是由 render 生成,所以对于

render: function() {
    return <input type="text" value="hello"/>;
}

在任意时刻,这个视图总是返回一个显示 hello 的输入框。

<select>

在 HTML 中 <select> 标签指定选中项都是通过对应 <option> 的 selected 属性来做的,但是在 React 修改成统一使用 value

所以没有一个 selected 的状态属性。

<select value="B">
    <option value="A">Apple</option>
    <option value="B">Banana</option>
    <option value="C">Cranberry</option>
</select>

你可以通过传递一个数组指定多个选中项:<select multiple={true} value={['B', 'C']}>

玩转 React 服务器端渲染

React 提供了两个方法 renderToString 和 renderToStaticMarkup 用来将组件(Virtual DOM)输出成 HTML 字符串,这是 React 服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。

服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:

  • 前后端可以共享代码
  • 前后端路由可以统一处理

React 生态提供了很多选择方案,这里我们选用 Redux 和 react-router 来做说明。

Redux

Redux 提供了一套类似 Flux 的单向数据流,整个应用只维护一个 Store,以及面向函数式的特性让它对服务器端渲染支持很友好。

2 分钟了解 Redux 是如何运作的

关于 Store:

  • 整个应用只有一个唯一的 Store
  • Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
  • 状态树上的每个字段都可以进一步由不同的 reducer 函数生成
  • Store 包含了几个方法比如 dispatchgetState 来处理数据流
  • Store 的状态树只能由 dispatch(action) 来触发更改

Redux 的数据流:

  • action 是一个包含 typepayload } 的对象
  • reducer 函数通过 store.dispatch(action) 触发
  • reducer 函数接受 (state, action) 两个参数,返回一个新的 state
  • reducer 函数判断 action.type 然后处理对应的 action.payload 数据来更新状态树

所以对于整个应用来说,一个 Store 就对应一个 UI 快照,服务器端渲染就简化成了在服务器端初始化 Store,将 Store 传入应用的根组件,针对根组件调用 renderToString 就将整个应用输出成包含了初始化数据的 HTML。

react-router

react-router 通过一种声明式的方式匹配不同路由决定在页面上展示不同的组件,并且通过 props 将路由信息传递给组件使用,所以只要路由变更,props 就会变化,触发组件 re-render。

假设有一个很简单的应用,只有两个页面,一个列表页 /list 和一个详情页 /item/:id,点击列表上的条目进入详情页。

可以这样定义路由,./routes.js

import React from 'react';
import { Route } from 'react-router';
import { List, Item } from './components';

// 无状态(stateless)组件,一个简单的容器,react-router 会根据 route
// 规则匹配到的组件作为 `props.children` 传入
const Container = (props) => {
  return (
    <div>{props.children}</div>
  );
};

// route 规则:
// - `/list` 显示 `List` 组件
// - `/item/:id` 显示 `Item` 组件
const routes = (
  <Route path="/" component={Container} >
    <Route path="list" component={List} />
    <Route path="item/:id" component={Item} />
  </Route>
);

export default routes;

从这里开始,我们通过这个非常简单的应用来解释实现服务器端渲染前后端涉及的一些细节问题。

Reducer

Store 是由 reducer 产生的,所以 reducer 实际上反映了 Store 的状态树结构

./reducers/index.js

import listReducer from './list';
import itemReducer from './item';

export default function rootReducer(state = {}, action) {
  return {
    list: listReducer(state.list, action),
    item: itemReducer(state.item, action)
  };
}

rootReducer 的 state 参数就是整个 Store 的状态树,状态树下的每个字段对应也可以有自己的
reducer,所以这里引入了 listReducer 和 itemReducer,可以看到这两个 reducer
的 state 参数就只是整个状态树上对应的 list 和 item 字段。

具体到 ./reducers/list.js

const initialState = [];

export default function listReducer(state = initialState, action) {
  switch(action.type) {
  case 'FETCH_LIST_SUCCESS': return [...action.payload];
  default: return state;
  }
}

list 就是一个包含 items 的简单数组,可能类似这种结构:[{ id: 0, name: 'first item'}, {id: 1, name: 'second item'}],从 'FETCH_LIST_SUCCESS' 的 action.payload 获得。

然后是 ./reducers/item.js,处理获取到的 item 数据

const initialState = {};

export default function listReducer(state = initialState, action) {
  switch(action.type) {
  case 'FETCH_ITEM_SUCCESS': return [...action.payload];
  default: return state;
  }
}

Action

对应的应该要有两个 action 来获取 list 和 item,触发 reducer 更改 Store,这里我们定义 fetchList 和 fetchItem 两个 action。

./actions/index.js

import fetch from 'isomorphic-fetch';

export function fetchList() {
  return (dispatch) => {
    return fetch('/api/list')
        .then(res => res.json())
        .then(json => dispatch({ type: 'FETCH_LIST_SUCCESS', payload: json }));
  }
}

export function fetchItem(id) {
  return (dispatch) => {
    if (!id) return Promise.resolve();
    return fetch(`/api/item/${id}`)
        .then(res => res.json())
        .then(json => dispatch({ type: 'FETCH_ITEM_SUCCESS', payload: json }));
  }
}

isomorphic-fetch 是一个前后端通用的 Ajax 实现,前后端要共享代码这点很重要。

另外因为涉及到异步请求,这里的 action 用到了 thunk,也就是函数,redux 通过 thunk-middleware 来处理这类 action,把函数当作普通的 action dispatch 就好了,比如 dispatch(fetchList())

Store

我们用一个独立的 ./store.js,配置(比如 Apply Middleware)生成 Store

import { createStore } from 'redux';
import rootReducer from './reducers';

// Apply middleware here
// ...

export default function configureStore(initialState) {
  const store = createStore(rootReducer, initialState);
  return store;
}

react-redux

接下来实现 <List><Item> 组件,然后把 redux 和 react 组件关联起来,具体细节参见 react-redux

./app.js

import React from 'react';
import { render } from 'react-dom';
import { Router } from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';

// `__INITIAL_STATE__` 来自服务器端渲染,下一部分细说
const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState);
const Root = (props) => {
  return (
    <div>
      <Provider store={store}>
        <Router history={createBrowserHistory()}>
          {routes}
        </Router>
      </Provider>
    </div>
  );
}

render(<Root />, document.getElementById('root'));

至此,客户端部分结束。

Server Rendering

接下来的服务器端就比较简单了,获取数据可以调用 action,routes 在服务器端的处理参考 react-router server rendering,在服务器端用一个 match 方法将拿到的 request url 匹配到我们之前定义的 routes,解析成和客户端一致的 props 对象传递给组件。

./server.js

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { RoutingContext, match } from 'react-router';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';

const app = express();

function renderFullPage(html, initialState) {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
    </head>
    <body>
      <div id="root">
        <div>
          ${html}
        </div>
      </div>
      <script>
        window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
      </script>
      <script src="/static/bundle.js"></script>
    </body>
    </html>
  `;
}

app.use((req, res) => {
  match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
    if (err) {
      res.status(500).end(`Internal Server Error ${err}`);
    } else if (redirectLocation) {
      res.redirect(redirectLocation.pathname + redirectLocation.search);
    } else if (renderProps) {
      const store = configureStore();
      const state = store.getState();

      Promise.all([
        store.dispatch(fetchList()),
        store.dispatch(fetchItem(renderProps.params.id))
      ])
      .then(() => {
        const html = renderToString(
          <Provider store={store}>
            <RoutingContext {...renderProps} />
          </Provider>
        );
        res.end(renderFullPage(html, store.getState()));
      });
    } else {
      res.status(404).end('Not found');
    }
  });
});

服务器端渲染部分可以直接通过共用客户端 store.dispatch(action) 来统一获取 Store 数据。另外注意 renderFullPage生成的页面 HTML 在 React 组件 mount 的部分(<div id="root">),前后端的 HTML 结构应该是一致的。然后要把 store 的状态树写入一个全局变量(__INITIAL_STATE__),这样客户端初始化 render 的时候能够校验服务器生成的 HTML 结构,并且同步到初始化状态,然后整个页面被客户端接管。

最后关于页面内链接跳转如何处理?

react-router 提供了一个 <Link> 组件用来替代 <a> 标签,它负责管理浏览器 history,从而不是每次点击链接都去请求服务器,然后可以通过绑定 onClick 事件来作其他处理。

比如在 /list 页面,对于每一个 item 都会用 <Link> 绑定一个 route url:/item/:id,并且绑定 onClick 去触发 dispatch(fetchItem(id)) 获取数据,显示详情页内容。

更多参考

我们的未来

1、脑电波带来的第n次信息技术革命,彻底改变了人类传统的沟通方式。无言的交流。
2、重新定义人类。人类的进化,会从基因层面上,吸收其他物种的优秀基因,可以有选择地长出其他物种的器官。人类也不再千篇一律的相似,一个牛头马面的人不再罕见。满世界形形色色的人类,人类不再局限于两只手脚,一个脑袋,直立行走。也不再局限于陆地生活。那时人类将重新定义自己。那时人与人的区别,就像现在不同服装的区别。
人类可以通过基因制造出和目前各类电子设备功能相同的器官,如无线电收发器、显示器。

人类可以适应更高的温度,更低的寒冷。

数字化的人类,人类的生死,把一个人的思维意识记忆等数字化,人类的死亡也仅仅只是身体的报废。通过把数字化的意识记忆复制到新的躯体大脑中,满血复活。
一个人的消亡,是意识记忆的消亡。保存意识记忆的U盘丢失或损坏。把一个人的意识记忆的数字带到世界的另一端
起死回生。
人类指导下的有目的的进化,代替自然选择。人类设计师,生物在自然界缓慢进化。人类设计师可以快速设计出更优秀的自己。

3、交通

4、能源

5、政治

6、思想

7、外星生物

8、人类能否完全认知整个世界,把整个宇宙搞明白。
大道至简,我们的认知过程,总是从一个大的特定到一个小的特定。就如果洋葱,我们被包裹在最里层,想知道外面的宇宙。我们只有一层一层的向外认知。