React同构思想

React比较吸引我的地方在于其客户端-服务端同构特性,服务端-客户端可复用组件,本文来简单介绍下这一架构思想。

出于篇幅原因,本文不会介绍React基础,所以,如果你还不清楚React的state/props/生存周期等基本概念,建议先学习相关文档

客户端React

先来回顾一下React如何写一个组件。比如要做一个下面的表格:

可以这样写: 先创建一个表格类。 Table.js

var React = require('react');

var DOM = React.DOM;
var table = DOM.table, tr = DOM.tr, td = DOM.td;

module.exports = React.createClass({
    render: function () {
        return table({
                children: this.props.datas.map(function (data) {
                    return tr(null,
                        td(null, data.name),
                        td(null, data.age),
                        td(null, data.gender)
                    );
                })
            });
    }
});

假设已经有了我们要的表格的结构化数据。 datas.js

// 三行数据,分别包括名字、年龄、性别
module.exports = [
    {
        'name': 'foo',
        'age': 23,
        'gender': 'male'
    },
    {
        'name': 'bar',
        'age': 25,
        'gender': 'female'
    },
    {
        'name': 'alice',
        'age': 34,
        'gender': 'male'
    }
];

有了表格类和相应的数据之后,就可以调用并渲染这个表格了。 render-client.js

var React = require('react');
var ReactDOM = require('react-dom');

// table类
var Table = require('./Table');
// table实例
var table = React.createFactory(Table);
// 数据源
var datas = require('./datas');

// render方法把react实例渲染到页面中 https://facebook.github.io/react/docs/top-level-api.html#reactdom
ReactDOM.render(
    table({datas: datas}),
    document.body
);

我们把React基础库Table.jsdatas.jsrender-client.js等打包成pack.js,引用到页面中:

<!doctype html>
<html>
    <head>
        <title>react</title>
    </head>
    <body>
    </body>
    <script src="pack.js"></script>
</html>'

这样页面便可按数据结构渲染出一个表格来

这里 pack.js 的具体打包工具可以是grunt/gulp/webpack/browerify等,打包方法不在这里赘述

这个例子的关键点是使用props来传递单向数据流。例如,通过遍历从``props传来的数据datas```生成表格的每一行数据:

this.props.datas.map...

组件的每一次变更(比如有新增数据),都会调用组件内部的render方法,更改其DOM结构。上面这个例子中,当给datas push新数据时,react会自动为页面中的表格新增数据行。

服务端React

上面的例子中创建的Table组件,出于性能、SEO等因素考虑,我们会考虑在服务端直接生成HTML结构,这样就可以在浏览器端直接渲染DOM了。

这时候,我们的Table组件,就可以同时在客户端和服务端使用了。

只不过与浏览器端使用ReactDOM.render指定组件的渲染目标不同,在服务器中渲染,使用的是ReactDOMServer这个模块,它有两个生成HTML字符串的方法:

关于这两个方法的区别,我想放到后面再来解释,因为跟后面介绍的内容很有关系。

有了这两个方法,我们来创建一个在服务端nodejs环境运行的文件,使之可以直接在服务端生成表格的HTML结构。

render-server.js:

var React = require('react');

// 与客户端require('react-dom')略有不同
var React = require('react');

// 与客户端require('react-dom')略有不同
var ReactDOMServer = require('react-dom/server');

// table类
var Table = require('./Table');
// table实例
var table = React.createFactory(Table);

module.exports = function () {
    return ReactDOMServer.renderToString(table(datas));
};

上面这段代码复用了同一个Table组件,生成浏览器可以直接渲染的HTML结构,下面我们通过改改nodejs的官方Hello World来做一个真实的页面。

server.js :

var makeTable = require('./render-server');

var http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});

  var table = makeTable();
  var html = '<!doctype html>\n\
              <html>\
                <head>\
                    <title>react server render</title>\
                </head>\
                <body>' +
                    table +
                '</body>\
              </html>';

  res.end(html);
}).listen(1337, "127.0.0.1");

console.log('Server running at http://127.0.0.1:1337/');

这时候运行node server.js就能看到,不实用js,达到了同样的表格效果,这里我使用了同一个Table.js,完成客户端及服务端的同构,一份代码,两处使用。

这里我们通过查看页面的HTML源码,发现表格的DOM中带了一些数据:

data-reactid / data-react-checksum 都是些啥?这里同样先留点悬念,后面再解释。

服务端 + 客户端渲染

上面的这个例子,通过在服务端调用同一个React组件,达到了同样的界面效果,但是有人可能会不开心了:貌似有点弱啊!

上面的例子有两个明显的问题:

  • datas.js 数据源是写死的,不符合大部分真实生产环境
  • 服务端生成HTML结构有时候并不完善,有时候不借助js是不行的。比如当我们的表格需要轮询服务器的数据接口,实现表格数据与服务器同步的时候,怎么实现一个组件两端使用。

为了解决这个问题,我们的Table组件需要变得更复杂。

数据源

假设我们的表格数据每过一段时间要和服务端同步,在浏览器端,我们必须借助ajax,React官方给我们指明了这类需求的方向,通过componentDidMount这一生存周期方法来拉取数据。

componentDidMount 方法,我个人把它比喻成一个“善后”的方法,就是在React把基本的HTML结构挂载到DOM中后,再通过它来做一些善后的事情,例如拉取数据更新DOM等等。

于是我们改一下我们的``Table组件,去掉假数据datas.js,在componentDidMount```中调用我们封装好的抓取数据方法,每三秒去服务器抓取一次数据并更新到页面中。

Table.js

var React = require('react');
var ReactDOM = require('react-dom');

var DOM = React.DOM;
var table = DOM.table, tr = DOM.tr, td = DOM.td;

var Data = require('./data');

module.exports = React.createClass({
    render: function () {
        return table({
                children: this.props.datas.map(function (data) {
                    return tr(null,
                        td(null, data.name),
                        td(null, data.age),
                        td(null, data.gender)
                    );
                })
            });
    },
    componentDidMount: function () {
        setInterval(function () {
            Data.fetch('http://datas.url.com').then(function (datas) {
                this.setProps({
                    datas: datas
                });
            });
        }, 3000)
    }
});

这里假设我们已经封装了一个拉取数据的Data.fetch方法,例如Data.fetch = jQuery.ajax

到这一步,我们实现了客户端的每3秒自动更新表格数据。那么上面这个Table组件是不是可以直接复用到服务端,实现数据拉取呢,不好意思,答案是“不”

React的奇葩之一,就是其组件有生存周期这一说法,在组件的生命的不同时期,例如异步数据更新,DOM销毁等等过程,都会调用不同的生命周期方法。

然而服务端情况不同,对服务端来说,它要做的事情便是:去数据库拉取数据 -> 根据数据生成HTML -> 吐给客户端。这是一个固定的过程,拉取数据和生成HTML过程是不可打乱顺序的,不存在先把内容吐给客户端,再拉取数据这样的异步过程。

所以,componentDidMount这样的“善后”方法,React在服务器渲染组件的时候,就不适用了。

而且我还要告诉你,componentDidMount这个方法,在服务端确实永远都不会执行!

看到这里,你可能要想,这步坑爹吗!搞了半天,这个东西只能在客户端用,说好的同构呢!

别急,拉取数据,我们需要另外的方法。

React中可以通过statics定义“静态方法”,学过面向对象编程的同学,自然懂statics方法的意思,没学过的,拉出去打三十大板。

我们再来改一下Table组件,把拉取数据的Data.fetch逻辑放到这里来。

Table.js:

var React = require('react');

var DOM = React.DOM;
var table = DOM.table, tr = DOM.tr, td = DOM.td;

var Data = require('./data');

module.exports = React.createClass({
    statics: {
        fetchData: function (callback) {
            Data.fetch().then(function (datas) {
                callback.call(null, datas);
            });
        }
    },
    render: function () {
        return table({
                children: this.props.datas.map(function (data) {
                    return tr(null,
                        td(null, data.name),
                        td(null, data.age),
                        td(null, data.gender)
                    );
                })
            });
    },
    componentDidMount: function () {
        setInterval(function () {

            // 组件内部调用statics方法时,使用this.constructor.xxx...
            this.constructor.fetchData(function (datas) {
                this.setProps({
                    datas: datas
                });
            });
        }, 3000);
    }
});

非常重要:Table组件能在客户端和服务端复用fetchData方法拉取数据的关键在于,Data.fetch必须在客户端和服务端有不同的实现!例如在客户端调用Data.fetch时,是发起ajax请求,而在服务端调用Data.fetch时,有可能是通过UDP协议从其他数据服务器获取数据、查询数据库等实现

由于服务端React不会调用componentDidMount,需要改一下服务端渲染的文件,同样不再通过datas.js获取数据,而是调用Table的静态方法fetchData,获取数据后,再传递给服务端渲染方法renderToString,获取数据在实际生产环境中是个异步过程,所以我们的代码也需要是异步的:

render-server.js:

var React = require('react');
var ReactDOMServer = require('react-dom/server');

// table类
var Table = require('./Table');
// table实例
var table = React.createFactory(Table);

module.exports = function (callback) {
    Table.fetchData(function (datas) {
        var html = ReactDOMServer.renderToString(table({datas: datas}));
        callback.call(null, html);
    });
};

这时候,我们的Table组件已经实现了每3秒更新一次数据,所以,我们既需要在服务端调用React初始html数据,还需要在客户端调用React实时更新,所以需要在页面中引入我们打包后的js。

server.js

var makeTable = require('./render-server');

var http = require('http');

http.createServer(function (req, res) {
    if (req.url === '/') {
        res.writeHead(200, {'Content-Type': 'text/html'});

        makeTable(function (table) {
            var html = '<!doctype html>\n\
                      <html>\
                        <head>\
                            <title>react server render</title>\
                        </head>\
                        <body>' +
                            table +
                            '<script src="pack.js"></script>\
                        </body>\
                      </html>';

            res.end(html);
        });
    } else {
        res.statusCode = 404;
        res.end();
    }

}).listen(1337, "127.0.0.1");

console.log('Server running at http://127.0.0.1:1337/');

成果

通过上面的改动,我们在服务端获取表格数据,生成HTML供浏览器直接渲染;页面渲染后,Table组件每隔3秒会通过ajax获取新的表格数据,有数据更新的话,会直接更新到页面DOM中。

checksum的作用

还记得前面的问题么?

ReactDOMServer.renderToStringReactDOMServer.renderToStaticMarkup 有什么不同?服务端生成的data-react-checksum是干嘛使的?

我们想一想,就算服务端没有初始化HTML数据,仅仅依靠客户端的React也完全可以实现渲染我们的表格,那服务端生成了HTML数据,会不会在客户端React执行的时候被重新渲染呢?我们服务端辛辛苦苦生成的东西,被客户端无情地覆盖了?

当然不会!React在服务端渲染的时候,会为组件生成相应的校验和(checksum),这样客户端React在处理同一个组件的时候,会复用服务端已生成的初始DOM,增量更新,这就是data-react-checksum的作用。

ReactDOMServer.renderToStringReactDOMServer.renderToStaticMarkup 的区别在这个时候就很好解释了,前者会为组件生成checksum,而后者不会,后者仅仅生成HTML结构数据。

所以,只有你不想在客户端-服务端同时操作同一个组件的时候,方可使用renderToStaticMarkup

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序猿DD

Spring Cloud Finchley版中Consul多实例注册的问题处理

问题:该问题可能在开发阶段不一定会发现,但是在线上部署多实例的时候,将会发现Consul中只有一个实例。

1214
来自专栏IMWeb前端团队

Grunt插件快速开发笔记

最近写了两个Grunt插件(grunt-htmlstamp 和 grunt-file-modify),已经用在自己的项目中,用得很开心。本文便是记录了Grunt...

1927
来自专栏企鹅号快讯

学好webpack,一名前端开发工程师的自我修养

前言 webpack 前端工程中扮演的角色越来越重要,它也是前端工程化很重要的一环。本文将和大家一起按照项目流程学习使用 wwbpack,由浅入深的学习,妈妈再...

22110
来自专栏大史住在大前端

大前端的自动化工厂(1)——Yeoman

Yeoman是现代化前端项目的脚手架工具,用于生成包含指定框架结构的工程化目录结构。它是整个前端自动化工厂的第一站。

634
来自专栏前端架构

phpcms v9站http升级到https加http2遇到到坑

nginx配置方面到坑,已经写了一篇:网站迁移云服务器血泪记—phpcms小站迁移 

894
来自专栏NetCore

Win7+Win8双系统安装教程!零风险超简单2步搞定

Win7 Win8双系统安装(一)解压   Win7和Win8双系统安装教程有木有?Win7和Win8双系统安装有多简单?Win7和Win8双系统安装2步搞定?...

1816
来自专栏Wordpress专用主机|主题模板|必备插件

如何使用Elementor插件制作Wordrpess首页?

之前推荐过一款广受好评的wordpress页面构建编辑插件:Elementor,由于其功能过于强大,编辑出的页面也很美观。很多wp爱好者不禁要问一下:可以使用E...

1.4K8
来自专栏云计算

用 Cricket 在 Java 环境里构建极简的内容管理服务器

原文地址:https://dzone.com/articles/minimalistic-cms-microservice-for-java

1495
来自专栏IMWeb前端团队

chrome插件 DIY

本文作者:IMWeb coolriver 原文出处:IMWeb社区 未经同意,禁止转载 1 前言 对于一个web前端开发者,chrome浏览器是一个工...

2096
来自专栏静晴轩

Gulp探究折腾之路(I)

前言: gulp是前端开发过程中对代码进行构建的工具,是自动化项目的构建利器;她不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完...

3538

扫码关注云+社区