美文网首页
从零开始React服务器渲染

从零开始React服务器渲染

作者: 茶艺瑶 | 来源:发表于2019-01-29 12:14 被阅读50次

    如果你的项目是刚刚开始的话,我是建议你用Next.js直接写的,如果你的公司的项目在可把控的时间和财力的话,我还是建议你迁移一下到Next.js上。
    如果你的项目已经是用SPA的话,那今天就可能帮到你。

    一.前言
    当我们选择使用Node+React的技术栈开发Web时,React提供了一种优雅的方式实现服务器渲染。使用React实现服务器渲染有以下好处:
    1.利于SEO:React服务器渲染的方案使你的页面在一开始就有一个HTML DOM结构,方便Google等搜索引擎的爬虫能爬到网页的内容。
    2.提高首屏渲染的速度:服务器直接返回一个填满数据的HTML,而不是在请求了HTML后还需要异步请求首屏数据。
    3.前后端都可以使用js

    二.神奇的renderToString和renderToStaticMarkup

    有两个神奇的React API都可以实现React服务器渲染:renderToString和renderToStaticMarkup。renderToString和renderToStaticMarkup的主要作用都是将React Component转化为HTML的字符串。这两个函数都属于react-dom(react-dom/server)包,都接受一个React Component参数,返回一个String。

    也许你会奇怪为什么会有两个用于服务器渲染的函数,其实这两个函数是有区别的:

    1.renderToString:将React Component转化为HTML字符串,生成的HTML的DOM会带有额外属性:各个DOM会有data-react-id属性,第一个DOM会有data-checksum属性。

    2.renderToStaticMarkup:同样是将React Component转化为HTML字符串,但是生成HTML的DOM不会有额外属性,从而节省HTML字符串的大小。

    上菜真香警告

    npm -S install express react react-dom
    

    server.js:

    var express = require('express');
    var app = express();
     
    var React = require('react'),
        ReactDOMServer = require('react-dom/server');
     
    var App = React.createFactory(require('./App'));
     
    app.get('/', function(req, res) {
        var html = ReactDOMServer.renderToStaticMarkup(
            React.DOM.body(
                null,
                React.DOM.div({id: 'root',
                    dangerouslySetInnerHTML: {
                        __html: ReactDOMServer.renderToStaticMarkup(App())
                    }
                })
            )
        );
     
        res.end(html);
    });
     
    app.listen(3000, function() {
        console.log('running on port ' + 3000);
    });
    

    App.js:

    var React = require('react'),
        DOM = React.DOM, div = DOM.div, button = DOM.button, ul = DOM.ul, li = DOM.li
     
    module.exports = React.createClass({
      getInitialState: function() {
       return {
         isSayBye: false
       }
      },
      handleClick: function() {
       this.setState({
         isSayBye: !this.state.isSayBye
       })
      },
      render: function() {
        var content = this.state.isSayBye ? 'Bye' : 'Hello World';
        return div(null,
          div(null, content),
          button({onClick: this.handleClick}, 'switch')
        );
      }
    })
    
    node server.js
    
    image.png

    三.动态的React组件
    上例的页面中,点击“switch”按钮是没有反应的,这是因为这个页面只是一个静态的HTML页面,没有在客户端渲染React组件并初始化React实例。只有在初始化React实例后,才能更新组件的state和props,初始化React的事件系统,执行虚拟DOM的重新渲染机制,让React组件真正“动”起来。
    或许你会奇怪,服务器端已经渲染了一次React组件,如果在客户端中再渲染一次React组件,会不会渲染两次React组件。答案是不会的。秘诀在于data-react-checksum属性:
    上文有说过,如果使用renderToString渲染组件,会在组件的第一个DOM带有data-react-checksum属性,这个属性是通过adler32算法算出来:如果两个组件有相同的props和DOM结构时,adler32算法算出的checksum值会一样,有点类似于哈希算法。
    当客户端渲染React组件时,首先计算出组件的checksum值,然后检索HTML DOM看看是否存在数值相同的data-react-checksum属性,如果存在,则组件只会渲染一次,如果不存在,则会抛出一个warning异常。也就是说,当服务器端和客户端渲染具有相同的props和相同DOM结构的组件时,该React组件只会渲染一次。
    在服务器端使用renderToStaticMarkup渲染的组件不会带有data-react-checksum属性,此时客户端会重新渲染组件,覆盖掉服务器端的组件。因此,当页面不是渲染一个静态的页面时,最好还是使用renderToString方法。

    image.png

    四.一个完整的例子
    下面使用React服务器渲染实现一个简单的计数器。为了简单,本例中不使用redux、react-router框架,尽量排除各种没必要的东西。

    image.png
    npm install -S express react react-dom jsx-loader
    

    webpack.config.js:webpack配置文件,作用是在客户端中可以使用代码模块化和jsx形式的组件编写方式:

    var path = require('path');
     
    var assetsPath = path.join(__dirname, "public", "assets");
    var serverPath = path.join(__dirname, "server");
     
    module.exports = [
        {
            name: "browser",
            entry: './app/entry.js',
            output: {
                path: assetsPath,
                filename: 'entry.generator.js'
            },
            module: {
                loaders: [ 
                    { test: /\.js/, loader: "jsx-loader" }
                ]
            }
     
        },
        {
            name: "server-side rending",
            entry: './server/page.js',
            output: {
                path: serverPath,
                filename: "page.generator.js",
                // 使用page.generator.js的是nodejs,所以需要将
                // webpack模块转化为CMD模块
                library: 'page',
                libraryTarget: 'commonjs' 
            },
            module: {
                loaders: [
                    { test: /\.js$/, loader: 'jsx-loader' }
                ]
            }
        }
    ]
    

    app/App.js:根组件 (一个简单的计数器组件),在客户端和服务器端都需要引入使用

    var React = require('react');
     
    var App = React.createClass({
        getInitialState: function() {
            return {
                count: this.props.initialCount
            };
        },
     
        _increment: function() {
            this.setState({ count: this.state.count + 1 });
        },
     
        render: function() {
            return (
                <div>
                    <span>the count is: </span>
                    <span onClick={this._increment}>{this.state.count}</span>
                </div>
            )
        }
    })
     
    module.exports = App;
    

    server/index.js:服务器入口文件:

    var express = require('express');
    var path = require('path');
     
    var page = require("./page.generator.js").page;
     
    var app = express();
    var port = 8082;
     
    app.use(express.static(path.join(__dirname, '..', 'public')));
     
    app.get('/', function(req, res) {
        var props = {
            initialCount: 9
        };
        var html = page(props);
        res.end(html);
    });
     
    app.listen(port, function() {
        console.log('Listening on port %d', port);
    });
    
    

    server/page.js:暴露一个根组件转化为字符串的方法

    var React = require('react');
    var ReactDOMServer = require("react-dom/server");
     
    var App = require('../app/App');
     
    var ReactDOM = require('react-dom');
     
     
    module.exports = function(props) {
        
        var content = ReactDOMServer.renderToString(
            <App initialCount={props.initialCount}></App>
        );
     
        var propsScript = 'var APP_PROPS = ' + JSON.stringify(props);
     
        var html = ReactDOMServer.renderToStaticMarkup(
            <html>
                <head>
                </head>
                <body>
                    <div id="root" dangerouslySetInnerHTML={
                        {__html: content}
                    } />
                    <script dangerouslySetInnerHTML={
                        {__html: propsScript}
                    }></script>
                    <script src={"assets/entry.generator.js"}></script>
                </body>
            </html>
        );
     
        return html;
    }
    

    为了让服务器端和客户端的props一致,将一个服务器生成的首屏props赋给客户端的全局变量APP_PROPS,在客户端初始化根组件时使用这个APP_PROPS根组件的props。
    app/entry.js:客户端入口文件,用于在客户端渲染根组件,别忘了使用在服务器端写入的APP_PROPS初始化根组件的props

    var React = require('react'),
        ReactDOM = require('react-dom'),
        App = require('./App');
     
    var APP_PROPS = window.APP_PROPS || {};
     
    ReactDOM.render(
        <App initialCount={APP_PROPS.initialCount}/>,
        document.getElementById('root')
    );
    

    最后还是还是直接使用Next.js 用别人的东西就是香。 React Server Side Rendering 并没有为你省下多少开发成本。。。

    相关文章

      网友评论

          本文标题:从零开始React服务器渲染

          本文链接:https://www.haomeiwen.com/subject/vuqxsqtx.html