使用React的static方法实现同构以及同构的常见问题

代码地址请在github查看,如果有新内容,我会定时更新,也欢迎您star,issue,共同进步

1.我们服务端渲染数据从何而来

1.1 如何写出同构的组件

服务端生成HTML结构有时候并不完善,有时候不借助js是不行的。比如当我们的组件需要轮询服务器的数据接口,实现数据与服务器同步的时候就显得很重要。其实这个获取数据的过程可以是数据库获取,也可以是从其他的反向代理服务器来获取。对于客户端来说,我们可以通过ajax请求来完成,只要将ajax请求放到componentDidMount方法中来完成就可以。而之所以放在该方法中有两个原因,第一个是为了保证此时DOM已经挂载到页面中;另一个原因是在该方法中调用setState会导致组件重新渲染(具体你可以查看这个文章)。而对于服务端来说,
一方面它要做的事情便是:去数据库或者反向代理服务器拉取数据 -> 根据数据生成HTML -> 吐给客户端。这是一个固定的过程,拉取数据和生成HTML过程是不可打乱顺序的,不存在先把内容吐给客户端,再拉取数据这样的异步过程。所以,componentDidMount在服务器渲染组件的时候,就不适用了(因为render方法已经调用,但是componentDidMount还没有执行,所以渲染得到的是没有数据的组件。原因在于生命周期方法componentDidMount在render之后才会调用)。

另一方面,componentDidMount这个方法,在服务端确实永远都不会执行!因此我们要采用和客户端渲染完全不一致的方法来解决渲染之前数据不存在问题。关于服务端渲染和客户端渲染的区别你可以查看Node直出理论与实践总结

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
            // 客户端在componentDidMount中获取数据,并调用setState修改状态要求
            // 组件重新渲染
            this.constructor.fetchData(function (datas) {
                this.setProps({
                    datas: datas
                });
            });
        }, 3000);
    }
});

其中服务器端的处理逻辑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) {
    //在客户端调用Data.fetch时,是发起ajax请求,而在服务端调用Data.fetch时,
    //有可能是通过UDP协议从其他数据服务器获取数据、查询数据库等实现
    Table.fetchData(function (datas) {
        var html = ReactDOMServer.renderToString(table({datas: datas}));
        callback.call(null, html);
    });
};

下面是服务器的逻辑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'});
        //先访问数据库或者反代理服务器来获取到数据,并注册回调,将含有数据的html结构返回给客户端,此处只是渲染一个组件,否则需要renderProps.components.forEach来遍历所有的组件获取数据
        //http://www.toutiao.com/i6284121573897011714/
        makeTable(function (table) {
            var html = '\n\
                      \
                        \
                            react server render\
                        \
                        ' +
                            table +
                            //这里是客户端的代码,实现每隔一定事件更新数据,至于如何添加下面的script标签内容,可以参考这里https://github.com/liangklfangl/react-universal-bucket
                            '\
                        \
                      ';
            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/');

注意:因为我们的react服务端渲染只是一次性的,不会随着调用setState而重新reRender,所以我们需要在返回给客户端的html中加入客户端的代码,真正的每隔一定时间更新组件的逻辑是客户端通过ajax来完成的。

1.2 如何避免服务端渲染后客户端再次渲染

服务端生成的data-react-checksum是干嘛使的?我们想一想,就算服务端没有初始化HTML数据,仅仅依靠客户端的React也完全可以实现渲染我们的组件,那服务端生成了HTML数据,会不会在客户端React执行的时候被重新渲染呢?我们服务端辛辛苦苦生成的东西,被客户端无情地覆盖了?当然不会!React在服务端渲染的时候,会为组件生成相应的校验和(在redux的情况下其实应该是一个组件树,为整个组件树生成校验和,因为这整个组件树就是我们首页要显示的内容)(checksum),这样客户端React在处理同一个组件的时候,会复用服务端已生成的初始DOM,增量更新(也就是说当客户端和服务端的checksum不一致的情况下才会进行dom diff,进行增量更新),这就是data-react-checksum的作用。可以通过下面的几句话来总结下:

 如果data-react-checksum相同则不重新render,省略创建DOM和挂载DOM的过程,接着触发 componentDidMount 等事件来处理服务端上的未尽事宜(事件绑定等),从而加快了交互时间;不同时,组件在客户端上被重新挂载 render。

ReactDOMServer.renderToString 和 ReactDOMServer.renderToStaticMarkup 的区别在这个时候就很好解释了,前者会为组件生成checksum,而后者不会,后者仅仅生成HTML结构数据。所以,只有你不想在客户端-服务端同时操作同一个组件的时候,方可使用renderToStaticMarkup。注意:上面使用了statics块,该写法只在createClass中可用,你可以使用下面的写法:

//组件内的写法
class Component extends React.Component {
    static propTypes = {
    ...
    }
    static someMethod(){
    }
}

在组件外面你可以按照如下写法:

class Component extends React.Component {
   ....
}
Component.propTypes = {...}
Component.someMethod = function(){....}

具体你可以查看这里。关于服务端渲染经常会出现下面的warning,大多数情况下是因为在返回 HTML 的时候没有将服务端上的数据一同返回,或者是返回的数据格式不对导致

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generatted on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Insted, figure out why the markup being generated is different on the client and server

2.如何区分客户端与服务端代码

2.1 添加客户端代码到服务端渲染的html字符串

通过这个例子我们知道,将webpack-isomorphic-tools这个插件添加到webpack的plugin中:

module.exports = {
    entry:{
        'main': [
          'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr',
        // "bootstrap-webpack!./src/theme/bootstrap.config.js",
        "bootstrap-loader",
        //确保安装bootstrap3,bootstrap4不支持less
          './src/client.js'
        ]
    },
   output: {
      path: assetsPath,
      filename: '[name]-[hash].js',
      chunkFilename: '[name]-[chunkhash].js',
      publicPath: 'http://' + host + ':' + port + '/dist/'
      //表示要访问我们客户端打包好的资源必须在前面加上的前缀,也就是虚拟路径
    },
    plugins:[
        new webpack.DefinePlugin({
          __CLIENT__: true,
          __SERVER__: false,
          __DEVELOPMENT__: true,
          __DEVTOOLS__: true //,
        }),
     webpackIsomorphicToolsPlugin.development()
     //在webpack的development模式下一定更要调用它支持asset hold reloading!
     //https://github.com/liangklfang/webpack-isomorphic-tools
    ]
}

此时我们client.js会被打包到相应的文件路径下,然后在我们的模版中,只要将这个打包好的script文件添加到html返回给客户端就可以了。下面是遍历我们的webpack-assets.json来获取到我们所有的产生的资源,然后添加到html模板中返回的逻辑:

export default class Html extends Component {
  static propTypes = {
    assets: PropTypes.object,
    component: PropTypes.node,
    store: PropTypes.object
  };
  render() {
    const {assets, component, store} = this.props;
    const content = component ? renderToString(component) : '';
    //如果有组件component传递过来,那么我们直接调用renderToString
    const head = Helmet.rewind();
    return (
      <html lang="en-us">
        <head>
          {head.base.toComponent()}
          {head.title.toComponent()}
          {head.meta.toComponent()}
          {head.link.toComponent()}
          {head.script.toComponent()}
          <link rel="shortcut icon" href="/favicon.ico" />
         <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"/>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Work+Sans:400,500"/>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/violet/0.0.1/violet.min.css"/>
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          {/* styles (will be present only in production with webpack extract text plugin)
             styles属性只有在生产模式下才会存在,此时通过link来添加。便于缓存
           */}
          {Object.keys(assets.styles).map((style, key) =>
            <link href={assets.styles[style]} key={key} media="screen, projection"
                  rel="stylesheet" type="text/css" charSet="UTF-8"/>
          )}
         {/*
            assets.styles如果开发模式下,那么肯定是空,那么我们直接采用内联的方式来插入即可。此时我们的css没有单独抽取出来,也就是没有ExtractTextWebpackPlugin,打包到js中从而内联进来
        */}
          {/* (will be present only in development mode) */}
          {/* outputs a <style/> tag with all bootstrap styles + App.scss + it could be CurrentPage.scss. */}
          {/* can smoothen the initial style flash (flicker) on page load in development mode. */}
          {/* ideally one could also include here the style for the current page (Home.scss, About.scss, etc) */}
        head>
        <body>
          <div id="content" dangerouslySetInnerHTML={{__html: content}}/>
           {/*将组件renderToString后放在id为content的div内部*/}
          <script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/>
          {/*将store.getState序列化后放在window.__data上,让客户端代码可以拿到*/}
          
                    
                    

你可能感兴趣的:(react,webpack,redux,React全家桶)