开始之前,先来个段子,博君一笑:

虽然go成为了世界上最并发的语言,但是, 这并不妨碍php成为世界上最好的语言,也不妨碍java成为世界上最有模式的语言,更不会妨碍c++成为21 天就能学会了的语言...但是JavaScript终将统治世界,而V8将成为世界的基石!

1. node内存分配基础

在V8中所有的JavaScript对象都是通过堆来分配的。为了提高垃圾回收的效率,V8将堆分为新生代和老生代两个部分,其中新生代为存活时间较短的对象(需要经常进行垃圾回收),而老生代为存活时间较长的对象(垃圾回收的频率较低)。

Alt text

新生代和老生代的默认内存限制在启动的时候就确定了,没办法根据应用使用内存情况自动扩充,当应用分配过多内存时,就会引起OOM(Out Of Memory,内存溢出)进程错误。64位系统和32位系统的内存限制不同,分别如下:

类型 64位系统 32位系统
新生代 32MB x 2 16MB x 2
老生代 1400MB 700MB
实际可用内存 1432MB 716MB

在node启动时,通过--max-new-space-size--max-old-space-size可分别设置新生代和老生代的默认内存限制。V8为什么要对内存做如此限制呢?最终的原因还是V8的垃圾回收机制所限制的,在较大的内存上进行垃圾回收是很耗时地。下面我们就来了解一下V8的垃圾回收机制。

2. node垃圾回收原理

2.1 常用垃圾回收基本算法

垃圾回收机制有多种,但最常用的就是以下几种:

类型 方法 是否停止程序
引用计数(Reference Counting) 每个对象配置一个计数器即可,每当引用它的对象被删除时,就将其引用数减1,当其引用计数为0时,即可清除
标记-清除(Mark-Sweep) 标记阶段标记活对象,清除阶段清除未被标记的对象
停止-复制(Stop-Copy) 内存分为两块,并将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色
标记-压缩(Mark-Compact) 对所有可达对象做一次标记,将所有的存活对象压缩到内存的一端,以减少内存碎片
增量算法(Incremental Collecting) 每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

2.2 V8的分代垃圾回收

上面提到过,V8将内存分为新生代和老生代,新生代中对象存活时间较短,老生代中对象存活时间较长。为了最大程度的提升垃圾回收效率,V8使用了一种综合性的方法,其在新生代和老生代中分别使用上文提到的不同的基本垃圾回收算法。

[1] 新生代垃圾回收算法Scavenge

在新生代中,由于内存较小(64位系统为64MB)且存活对象较少,V8采取了一种以空间换时间的方案,即停止-复制算法 (Stop-Copy)。它将新生代分为两个半区域(semi-space),分别称为from空间和to空间。一次垃圾回收分为两步:

(1) 将from空间中的活对象复制到to空间
(2) 切换from和to空间

V8将新生代中的一次垃圾回收过程,称为Scavenge。

Alt text

[2] 老生代垃圾回收算法

老生代的内存空间较大且存活对象较多,因此其垃圾回收算法也就没有新生代那么简单了。为此V8使用了标记-清除算法 (Mark-Sweep)进行垃圾回收,并使用标记-压缩算法 (Mark-Compact)整理内存碎片,提高内存的利用率。老生代的垃圾回收算法步骤如下:

(1).对老生代进行第一遍扫描,标记存活的对象
(2).对老生代进行第二次扫描,清除未被标记的对象
(3).将存活对象往内存的一端移动
(4).清除掉存活对象边界外的内存

Alt text

从上面的表格可以看出,停止-复制(Stop-Copy)、标记-清除(Mark-Sweep)和标记-压缩(Mark-Compact)都需要停止应用逻辑,我们将之称为stop-the-world。但因为新生代内存较小且存活对象较少,即便stop-the-world,对应用的性能影响也不大;而老生代的内存很大,stop-the-world就不能接受了,为此V8引入了增量标记。增量标记使得应用逻辑和垃圾回收交替运行,减少了垃圾回收对应用逻辑的干扰。

2.3 分代垃圾回收的代价

在讨论新生代中的垃圾回收算法Scavenge时,我们忽略了许多细节。

真的仅仅扫描新生代的内存空间,就能确定新生代的活动对象吗?

当然不是,老生代的对象也可能引用新生代的对象啊。如果每次运行Scavenge算法时,都要扫描老生代空间的话,这种操作带来的性能损耗就完全抵消了分代式垃圾回收所带来的性能提升。为此V8使用写屏障技术解决了这个问题:

V8使用一个列表(我们称之为CrossRefList)记录所有老生代对象指向新生代的情况,当有老生代中的对象出现指向新生代对象的指针时,便记录下来这样的跨区指向。由于这种记录行为总是发生在写操作时,因此被称为写屏障。

Alt text

每个写操作都要经历这样一关,性能上必然有损失,这是分代垃圾回收的代价之一。通过使用写屏障技术,我们在对新生代进行垃圾回收时,只需要扫描新生代From空间和CrossRefList列表就可以确定活动对象了。

3.垃圾回收监控

理解了垃圾回收的基本原理以后,我们来看一看如何监控node的垃圾回收情况。查看垃圾回收方式的最方便的方法是通过在启动时使用--trace-gc参数:

node --trace-gc app.js
1254 ms: Scavenge 413.1 (460.9) -> 413.1 (460.9) MB, 0.5 
        / 0 ms (+ 3.0 ms in 14 steps since last GC)
1258 ms: Mark-sweep 413.5 (461.9) -> 412.6 (461.9) MB, 1.0 
        / 0 ms (+ 255.0 ms in 2050 steps since start of marking, biggest step 1.0 ms)
         [GC interrupt] [GC in old space requested]

从控制台日志中可以轻易的看出node的垃圾回收动作,包括新生代垃圾回收(Scavenge)和老生代垃圾回收(Mark-sweep)。

而一种更加程序化的方式是使用memwatch-next模块,该模块在node每一次进行全量垃圾(full-gc,包括标记-清除和标记-压缩)回收时触发相应的事件:

var memwatch = require('memwatch-next');
memwatch.on('stats', function(stats) { 
    console.log(stats);
});

上述代码监控每一次全量垃圾回收动作,并打印出相应垃圾回收统计信息:

{
  "num_full_gc": 8,            //目前为止进行全量GC的次数
  "num_inc_gc": 18,             //目前为止进行增量GC的次数
  "heap_compactions": 8,        //目前为止进行的内存压缩的次数
  "usage_trend": 0,             //内存增长趋势,如果一直大于0,则可能有内存泄露
  "estimated_base": 2592568,    
  "current_base": 2592568,
  "min": 2499912,
  "max": 2592568
}                 

4.内存泄露定位

使用上文提到的垃圾回收监控方法,我们可以知道程序是否有内存泄露,那么具体在什么地方有内存泄露呢?我们需要借助于新的工具。node-heapdump提供了v8的堆内存快照抓取工具。

4.1 抓取对内存镜像

我们可以在程序中直接通过它提供的函数抓取内存快照:

var heapdump = require('heapdump');
heapdump.writeSnapshot('/tmp/' + Date.now() + '.heapsnapshot');

在linux下,我们还可以通过向node进程发送信号来抓取内存快照:

kill -USR2 pid

有了内存快照后,我们就可以借助chrome的Profile工具,具体的分析内存泄露发生在什么地方了。

4.2 三次快照法

利用chrome的Profile工具分析内存泄露的经典方法是三次快照法,我们需要首选准备3个内存快照文件:

(1) 第一次获取正常情况下内存快照
(2) 第二次获取发生内存泄露时的内存快照
(3) 第三次获取继续发生内存泄露时的内存快照

三次快照要求第一次必须在没有出现内存泄露时,是为了过滤一些无用的信息,使得分析结果可读性更强。

5.常见的内存泄露case

了解了node内存的基本原理后,我们一起来看一看常见的内存泄露case。

5.1 使用对象作为缓存

使用javascript键值对作为缓存,我们几乎必然要承担内存泄露的风险,因为严格意义的缓存有完善的过期策略,而普通的javascript对象显然不具备这个功能:

var cache = {};
function getFromCache(key){
    if(cache[key]){
        return cache[key];
    }else{
        cache[key] = new CacheItem();
        return cache[key];
    }
}

上述cache里缓存的item永远得不到释放,尽管没有任何引用。那么如果我们确实需要使用缓存呢?有两种方案:

(1) 使用外部缓存服务,比如memcahe
(2) 使用es6的WeakMap作为程序内缓存数据结构

WeakMap是ES6标准中新引入的一种类似字典的数据结构,和普通字典数据结构不同的是,当WeakMap中key没有其他引用,并且value也没有除key之外的引用时,value可以被垃圾回收,这是一种程序内缓存的理想选择。

6.堆外内存

在node中,我们不可避免的需要操作大内存,而堆内内存大小的限制显然无法满足我们的要求。为此,node通过内置的全局Buffer模块提供堆外的内存使用方法。Buffer是一个C++与Javascript结合的模块,其内存分配策略,也非常值得我们研究。我们知道大部分Javascript对象所使用的内存都是很小的,如果每一次都向操作系统申请,就必须频繁地进行系统调用;为了解决这个问题,node使用C++层面申请一大块内存,然后按需分配给Javascript的策略;也就是*nix系统常用的slab内存分配策略,这是一种典型的对时间和空间的折衷算法(time/space trade-off)。

slab是一块提前申请好地固定大小的内存,一个slab有三种状态:full、partial、empty。node层面提供了一个SlowBuffer类,封装C++的api,用于申请真实的物理内存,可以简单地将一个slab理解为一个SlowBuffer对象。node中维护着一个名为pool的指针,它指向当前slab(SlowBuffer对象)。向系统申请slab的过程可用如下伪代码表示:

var pool;
function allocateSlab(){
    pool = new SlowBuffer();
    pool.used = 0;
}

6.1 小内存(<4kB)的分配

一个slab的大小为8KB(Buffer.poolSize),node中通过Buffer.poolSize定义。当我们需要创建一个长度小于4kB的Buffer对象时,会首先判断当前slab的剩余空间是否足够,如果剩余空间足够,则在当前slab上为Buffer对象分配内存,否则创建一个新的slab块,并在新的slab上为Buffer对象分配内存。

if(!pool || pool.lengh - pool.used < this.length){
    allocateSlab(); //向系统申请新的slab
    allocateBuffer(); //给buffer对象分配内存
}else{
    allocateBuffer(); //给buffer对象分配内存
}

从当前slab上为Buffer分配内存的算法也很容易理解,只需要将Buffer指向slab的某段内存,并调整pool的length和used等属性:

function allocateBuffer(){
    this.parent = pool; //buffer的parent属性指向当前slab
    this.offset = pool.used; //buffer的offset属性指向当前slab可用内存段的开始位置
    pool.used += this.length; //调整buffer的已使用空间
}

由此可见,一个slab(SlowBuffer对象)可供多个小内存的Buffer共用:

Alt text

在写本文时,node官方已经不建议使用new Buffer()创建buffer对象了,官方提供了更新的Buffer.alloc()Buffer.allocUnsafe()。其中Buffer.alloc()在创建Buffer对象时会对内存进行初始化,并且不会使用slab策略;而Buffer.allocUnsafe()则是使用slab算法分配一块未初始化的内存,因此其性能要比Buffer.alloc()高很多。因此我们应该使用Buffer.allocUnsafe()替换来的new Buffer()

6.2 大内存(>4kB)的分配

对于大于4KB的Buffer对象,其大小甚至可能超过一个slab的大小,系统就无法使用固定大小的slab分配算法了。值得注意的是,node对单个Buffer大小是有上限的(buffer.kMaxLength),在32系统上其上限接近1GB(230-1),而在64位系统上其上限则接近2GB(231-1)。

6.3 slab算法的代价

鱼与熊掌不可兼得,上文中提到slab算法一种时间和空间的折衷算法。为了提高内存的分配速度,该算法可能导致内存碎片:当一个slab上的剩余空间不足于容纳新申请的Buffer的大小,或者新申请Buffer大于等于4kb(Buffer.poolSize)时,就需要创建新的slab,原来slab上剩余的空间就浪费了。我们写个小程序证明一下我们的猜想:

function testBufferSlab(size){
    var itt = 10000;
    var store = [];
    var rss = process.memoryUsage().rss;
    var tmpMem;
    for(var i =0 ;i < itt; i++){
        store.push(new Buffer(1));
        tmp = Buffer(size);
        if(i/1000){
            global.gc();
        }
    }
    var nr = process.memoryUsage().rss
    console.log((((nr - rss) / 1024 / 1024).toFixed(2)));
}

上述程序,在一万次循环中申请了一个1字节的全局缓存,并申请了size大小的临时缓存(其引用会在循环中被垃圾回收)。我们分别给testBufferSlab传递两个特殊的参数:Buffer.poolSizeBuffer.poolSize/2-1,并观察结果时,奇怪的现象发生了(申请较大的Buffer时竟然消耗更少的内存):

testBufferSlab(Buffer.poolSize);
node --expose-gc test.js
output: 5.2
testBufferSlab(Buffer.poolSize/2-1);
node --expose-gc test.js
output: 54.78

究其原因就是:当新申请的Buffer的小于4Kb时(Buffer.poolSize/2),会使用slab算法,即便当前Buffer块已经没有引用了,只要其对应slab上还有其他Buffer指向时,整个slab内存就无法释放,这样就会造成内存碎片。


参考文章:

https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/

https://www.zhihu.com/question/20018826

https://github.com/caoxudong/oracle_jrockit_the_definitive_guide/blob/master/chap3/3.3.md

http://newhtml.net/v8-garbage-collection/

http://taobaofed.org/blog/2016/04/15/how-to-find-memory-leak/

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/WeakMap

https://github.com/promises-aplus/promises-spec/issues/179

https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/

https://nodejs.org/api/buffer.html

http://stackoverflow.com/questions/14009048/what-makes-node-js-slowbuffers-slow

https://github.com/nodejs/node-v0.x-archive/issues/4525

前端是工程能力比技术能力更重要的领域,而最近一两年,前端在构建流程、组件化、同构渲染等方面有了深入的发展,其入行门槛也在逐步提高。

前端社区的活跃程度让人惊叹,各种工具层出不穷,比如grunt、gulp、webpack、fis等,即便你没有全部用过,也该了解过它们中的大部分,这些工具极大的解放了前端的生产力,解决了前端的构建流程问题。而React的出现将前端引入新的境界,它优雅的解决了前端UI层组件化的问题,使得组件化也成了前端项目的标配。为了提高页面渲染速度,页面首屏由后端直出,也已经有了很多解决方案。这里我们探讨一下容易被大家忽略的领域:页面状态的管理。

1. 页面状态

页面上所有UI层的显示都可以用对应的状态描述,比如,比如当前的列表项、当前被选中的标签等。如下图所示:

Alt text

可以简单的将前端项目抽象为对UI的管理和对状态的管理,UI和状态之间相互作用,处理它们之间的相互关系很复杂,行业内有不同的解决方案,比如以angularJS为代表的双向绑定、以及flux提出的单向数据流。本文我们将抽象的理解facebook提出单项数据流方案flux,以及它的具体实现redux。

2. 单向数据流flux

flux是facebook提出的一种应用程序框架,其基本架构如下入所示,其核心理念是单向数据流,它完善了React对应用状态的管理。

Alt text

上图描述了页面的启动和运行原理:

1.通过dispatcher派发action,并利用store中的action处理逻辑更新状态和view

2.而view也可以触发新的action,从而进入新的步骤1

其中的action是用于描述动作的简单对象,通常通过用户对view的操作产生,包括动作类型和动作所携带的所需参数,比如描述删除列表项的action:

{
    type: types.DELETE_ITEM,
    id: id
};

dispatcher用于对action进行分发,分发的目标就是注册在store里的事件处理函数:

dispatcher.register(function (action) {
  switch(action.type) {
    case 'DELETE_ITEM':
      sotre.deleteItem(action.id); //更新状态
      store.emitItemDeleted(); //通知视图更新
      break;
    default:
      // no op
  }
})

store包含了应用的所有状态和逻辑,它有点像传统的MVC模型中的model层,但又与之有明显的区别,store包括的是一个应用特定功能的全部状态和逻辑,它代表了应用的整个逻辑层;而不是像Model一样包含的是数据库中的一些记录和与之对应的逻辑。

3. 一种对flux的实现,redux

随着前端应用的复杂性指数级的提升,前端页面需要管理的状态也越来越多,flux给出了管理状态的基本数据流,而redux对flux就是对它最好的实现之一,而且其对flux的理念进行了更进一步的扩展。

redux倡导三大原则:

1.一个对象存储整个应用的状态
2.状态对象是只读的,只能通过action触发改变
3.通过普通函数处理action的逻辑

其基本流程如下:

Alt text

其相对于flux有如下不同之处:

1. redux没有dispacher,其通过普通函数处理action逻辑,并改变应用状态
2. redux的状态对象是immutable的,每一个action都会局部地创建新的状态对象

需要强调的是,redux不一定要和react搭配,它是一种应用状态管理方案,不涉及UI层,你可以任意选择自己的UI层;正因为redux脱离UI层,提供了整个应用状态的管理,使得我们的开发流程有了颠覆性的改变。

我们可以在UI层ready之前,完成应用的逻辑设计和实现。比如我们将应用的逻辑设计如下:

Alt text

ADD_ITEM的action触发todos列表状态的改变,SET_FILTER的action触发filter状态的改变。因为每一个action都是简单对象,我们可以轻易的模拟。这也就使得我们可以在UI层ready之前,对前端全部逻辑写单独的测试用例。

然后,在UI层ready之后,将UI层或网络层的事件映射为redux的action。比如将表单提交事件映射为ADD_ITEM,将标签页切换按钮点击事件映射为SET_FILTER。UI层和逻辑层相互独立,并仅仅通过事件与action的映射来建立联系,这种方案使得复杂的前端项目有了更清晰的架构。

4.redux与react的配合

redux只负责应用的逻辑层,而通过使用react-redux模块,其可以天衣无缝的和react配合。

4.1 经典案例

我们简单了解一下,如何使用react+redux实现经典的todolist案例。案例的源代码在此。最终的界面如下:

Alt text


(1) 逻辑层设计

前端应用本质上是:通过事件触发应用状态的改变。而redux包办了应用状态(state)、事件(action)、和事件处理函数(reducer),使得我们可以抛开UI层,先设计应用的逻辑层。redux使用单一对象存储整个应用的状态,todolist应用的状态(state)树如下:

{
  filter: 'show_all'
  todos: [
    {
     id: 1,
      text: 'todo1',
      marked: true
    }
  ]
}

其中filter为列表过滤策略,用于标志底部三个按钮选中的状态,而todos作为代办事项列表。上文提到过state是immutable的,只能通过action触发对state的改变,一个编辑todo条目的action如下:


var editTodo = function (id, text) {
    return {
        type: types.EDIT_TODO,
        id: id,
        text: text
    };
}

相应的,redux提倡用使用简单函数(又称作reducer)处理action,如下为EDIT_TODO的处理逻辑:

module.todo = function(state, action) {
    state = state || [];
    switch (action.type) {

        case types.EDIT_TODO:
            return state.map(function(todo) {
                return todo.id === action.id ?
                    assign({}, todo, { text: action.text }) :
                    todo
            });

        default:
            return state;
    }
}

每一个action处理函数都是将action作用在old state上,从而产生new state。state、action、reducer太零散,通过createStore可以将它统一在store中,而store则代表了整个应用的逻辑。

var store = createStore(reducer);

(2) UI层设计

本案例采用react作为UI层的组件化方案,在这里不再详述。需要注意的是为了配合redux,在写react组件时,我们需要对组件类别进行划分,将其划分为展示型组件和容器型组件。redux只需要和容器型组件通信,而不用管理展示型组件。用一个表格可以很好的说明它们和区别:

展示型组件 容器型组件
组件设计目标 纯粹的UI(标签、样式) 管理页面逻辑,和展示型组件
对redux是否可见 不可见 可见
数据来源 从props中读取数据 通过redux的state获取数据
改变数据的方式 调用通过props获取的回调函数 派发redux的actions
组件生成方式 手动写组件 通过react redux动态生成

(3) 组合逻辑层与UI层

上文提到,redux只关注react的容器型组件,而且容器型组件可以由react-redux动态生成,以防止state注入容器型组件时的硬编码。比如我们使用如下代码将应用状态注入一个容器型组件TodoApp中:

var App = React.createClass({
    render: function() {
        return (
            <Provider store={store}>
                {function() { return <TodoApp />; }}
            </Provider>
        );
    }
});

module.exports = App;

上述代码通过Provider将store注入容器型组件TodoApp中。在TodoApp组件中,我们可以通过this.props来获取store中存储的应用状态了:

var TodoApp = React.createClass({
    render: function () {
        var todos = this.props.todos;
        var filter = this.props.filter;
        return (
            <div>
                ......
            </div>
        );
    },
});
function mapStateToProps(state) {
    return state;
}
module.exports = connect(mapStateToProps)(TodoApp);

其中connect函数用于动态的创建容器型组件。借助于Provider和容器型组件,我们就将应用的逻辑层和UI层组合在一起了。

4.2 高级特性

了解了react和redux结合的基本思路以后,让我们一起看一看redux的高级特性。

(1) 状态树分治

redux提倡用一个对象存储整个应用的状态,而复杂应用的状态对象是很大的,这样会不会有性能问题?各个容器型组件都对整个应用状态对象进行操作,会不会引起混乱?对此redux有充分的考虑。首选在逻辑层设计时,我们就应该充分的考虑到状态树的分治,比如在设计action的处理函数(reducer)时,针对状态树的不同部分,将其对应的actions处理函数存储在不同的文件中,redux通过combineReducers对此提供了支持。比如

var todos = require('../reducers/todos');
var filter = require('../reducers/filter');
combineReducers({filter: filter, todos: todos});

其次,在UI层我们也可以很方便的只将部分状态树注入某个容器型组件,redux在使用connect生成容器型组件时,接收一个函数(mapStateToProps)作为参数,该函数可以只返回整个状态树的部分状态,因此,connect生成的容器型组件也就只能感知到部分状态树。这种方式,避免了应用状态树过大的混乱,通过分治降低了复杂度。如下代码,创建了一个只关注整个状态树中state.todos的容器型组件:

var TodoApp = React.createClass({
    render: function () {
        var todos = this.props.todos;
        return (
            <div>
                ......
            </div>
        );
    },
});

function mapStateToProps(state) {
    return state.todos;
}
module.exports = connect(mapStateToProps)(TodoApp);

(2) 异步action

一般来说,异步action并不能算是高级特性,因为它太常见了。比如发送一个网络请求,这是再寻常不过的需求了。只是用redux触发异步action并不是那么直接。我们需要首先了解redux的中间件概念,它可以用于在action被触发和action到达处理函数reducer之前,对action进行处理。

Alt text

可以在创建store时,通过applyMiddleware函数提供redux的中间件:

createStore( todosApp,applyMiddleware(someMiddleWare))

一个典型的redux中间件是redux-logger,它在控制台中记录每一次action作用前后的应用状态变化,非常适合在开发阶段进行调试。

Alt text

redux官方提供了thunkMiddleware的中间件,用于处理异步action,它使得redux可以派发一个函数而不是一个普通action对象,在该函数中我们可以进行异步网络请求:

var fetchTodos = function () {
    return function (dispatch) {
        return fetch('/todos');
    }
}

我们可以使用dispach函数像派发普通action一样,派发异步函数,异步函数的返回值还可以是Promise,其返回值会透传过dispch函数。

dispach(fetchTodos)
  .then(function(json){
      //handle response
  })
  .catch(function(error){
      //handle error
  });

通过网络加载数据,并在数据到达时更新应用状态是一种比较常见的应用场景,对于这种场景,一种最优雅的方案:

1. 派发异步函数,用于进行网络请求
2. 在网络请求完成时,派发同步action用于更新应用状态

可以用如下代码表示:

var fetchTodos = function () {
    return function (dispatch) {
        return fetch('/todos')
            .then(function (json) {
                //派发同步aciton,用于更新应用状态,初始化todo列表
                dispatch(initTodos(json.data || []));
            }).catch(function () {
                //派发同步action,用于更新应用状态,设置加载失败标志
                dispatch(failLoadedTodos());
            });
    }
}

(3) 同构渲染

前后端同构,应用首屏由后端直出是近年来比较流行的性能优化方案,redux对此也有完善的支持。基本流程是:

1. 服务端初始化state
2. 将服务端state传递到应用的页面端
3. 页面端用服务端传递的状态初始化应用state

在遵从这个基本流程的情况下,服务端和页面端的使用方法开发方法基本一致,如下是服务端代码:

 store.dispatch(todoActions.loadInitTodos()).then(function () {
        var contentHtml = React.renderToString(
            <Provider store={store}>
                {function () {
                    return <TodoApp />;
                }}
            </Provider>
        );
        var initialState = JSON.stringify(store.getState());
        res.render('index.ejs', {contentHtml: contentHtml, initialState: initialState});
    }).catch(function(error){
        res.json({errMsg: 'internal error'})
    });

上述服务端代码通过派发初始化异步函数更新应用状态,该异步函数返回一个Promise,Promise对象会透传过dispach函数。在Promise处理完成后,我们得到应用的最新状态。最后我们将由React输出的HTML字符串contentHtml和初始化应用状态initialState,传递到模板文件index.ejs中,模板文件如下:

<html>
  <head>
    <title>Redux TodoMVC</title>
  </head>
  <body>
    <div class="todoapp" id="root"><%-contentHtml%></div>
  </body>
  <script>
    window.__INITIAL_STATE__ =  <%-initialState%>;
  </script>
</html>

通过浏览器的window对象,我们将服务端的初始状态传递到了页面端。

var todosApp = combineReducers({filter: filter, todos: todos});
var store = createStore(  todosApp,
                          window.__INITIAL_STATE__,
                          applyMiddleware(thunkMiddleware, reduxLogger())
                        );

本文案例源代码: https://github.com/foio/react-redux-isomorphic-todolist

参考文献: http://redux.js.org/

https://facebook.github.io/flux/docs/overview.html

https://github.com/gaearon/redux-devtools

https://github.com/foio/react-redux-isomorphic-todolist