React Server Render

概述

一直想用React作些東西,苦於沒有實際項目練手,因此一直都是本身在搞些小玩意兒,作過用React Router構建的內部訂餐系統,是個SPA,也在社區分享過。因爲一我的作全棧開發,數據庫(mongodb)全靠本身設,需求全靠本身編,頁面全靠本身扯,心好累,感受不會在愛了!
SPA用來構建內部的系統徹底沒問題,可是用來作門戶、作電商網站就不行了,爲啥?由於SEO,不少的MVVM,MV*框架不能用、不敢用都是基於這個緣由(固然也可能由於我不會用)。
最近拿CNode的API作了個React服務器端渲染的例子,這裏跟你們分享下這個項目的構建過程和代碼組織,未必好,主要提供一個思路。css

搭建

圖片描述
總體項目目錄如上,這裏做個說明,附上代碼地址,上面有說明怎麼使用。html

  • component 咱們的組件目錄,這裏放置了view、ui等組件前端

  • lib 後端代碼,如過濾器等node

  • node_modules 依賴包react

  • public 靜態資源jquery

  • routes 路由webpack

瀏覽器端和服務器端的代碼咱們不必徹底獨立,實際上有時候代碼是能夠複用的。舉個例子
表單異步提交的時候,後端返回一個state狀態告知是否成功,相信大部分的人的第一反應都是抽出常量
constants.jsgit

module.exports = {
    state: {
        SUCCESS: 10000
    }     
};

固然了,瀏覽器端也是要判斷這個state的,爲了提升代碼的複用性,這裏一樣抽出
constants.jsgithub

module.exports = {
    state: {
        SUCCESS: 10000
    }     
};

雖然內容相同,實際上這是兩個不一樣的js,分處不一樣的目錄,oh shit。個人開發理念通常是這樣的web

相同的代碼堅定不寫第二遍,特殊狀況除外!

採用React後端渲染,我用了webpack打包,實際上就避免了這個問題,寫一份constants.js,打包到瀏覽器端去,NICE!

編碼

既然是後端渲染,首先得選擇一個模板引擎,這裏我採用的| 90ee772881e409df0a8a3bb9717d59483 |,具體配置和使用能夠參考文檔,這裏我就不贅述了。既然是構建SPA必不可少得要個路由管理,這裏我選擇的react-router,react-engine也是兼容react-router的,真棒!拿首頁的編碼舉個例子

route

路由我這裏用的本身的路由組織express-mapping,看首頁的代碼
routes/index.js

var constants = require('../lib/constants');
var request = require('superagent');
var queryString = require('query-string');

module.exports = {
    get: {
        '/': function (req, res) {
            request
                .get('http://cnodejs.org/api/v1/topics?' + queryString.stringify(req.query))
                .end(function (err, response) {
                    if (err) {
                        throw err;
                    }
                    res.render(req.url, {
                        state: constants.state.SUCCESS,
                        data: response.body.data,
                        title: 'CNode:Node.js專業中文社區'
                    });

                });
        }
    }
};

實際上,res.render方法被我重寫了,根據發的請求是否是ajax返回不一樣的內容
lib/filter.js

/**
 * 區分ajax請求與普通請求
 */
req.isXmlHttpRequest = (function () {
    var xRequestedWith = req.headers['x-requested-with'];
    return xRequestedWith && xRequestedWith.toLowerCase() === 'xmlhttprequest';
})();

/**
 * 重寫res.render方法
 */
var render = res.render;

res.render = function (view, data) {
    var response = _.extend({session: req.session}, data);
    req.isXmlHttpRequest ? res.json(response) : render.call(res, view, response);
};

這樣咱們又作到了接口的複用!

組件

來看看咱們打包的入口

component/index.js

var React = require('react');
var Router = require('react-router');
var $ = require('jquery');
var Routes = require('./routes.jsx');

var CLIENT_VARIABLENAME = '__REACT_ENGINE__';

var _window;
var _document;
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
    _window = window;
    _document = document;
}

document.addEventListener('DOMContentLoaded', function onLoad() {
    Router.run(Routes, Router.HistoryLocation, function onRouterRun(Root, state) {
        var props = _window[CLIENT_VARIABLENAME];
        if (props) {
            var componentInstance = React.createElement(Root, props);
            React.render(componentInstance, _document);
            _window[CLIENT_VARIABLENAME] = null;
        } else {
            $.get(state.path).then(function (data) {
                var componentInstance = React.createElement(Root, data);
                React.render(componentInstance, _document);
            });
        }

    });
});

後端渲染的原理是這樣的,當咱們第一訪問的時候,node端返回React渲染好的HTML結構,並經過script標籤將數據傳遞到前端,而後在瀏覽器端獲取到傳遞的數據再渲染一次,總共渲染了兩次。當咱們在瀏覽器端進行切換切換的時候,頁面是不刷新的,經過ajax請求獲取到數據,從新渲染DOM結構。

component/routes.jsx

再來看看路由,不熟悉React Router的最好熟悉下,會用到

var React = require('react');
var Router = require('react-router');

var Route = Router.Route;
var DefaultRoute = Router.DefaultRoute;

var App = require('./app.jsx');
var Index = require('./views/index.jsx');

var TopicDetail = require('./views/topic/detail.jsx');
var UserDetail = require('./views/user/detail.jsx');

var routes = (
    <Route handler={App} path="/">
        <DefaultRoute name="index" handler={Index}/>
        <Route name="topic-detail" path="topic/:topicId" handler={TopicDetail}/>
        <Route name="user-detail" path="user/:loginname" handler={UserDetail}/>
    </Route>
);

module.exports = routes;

都是些基本的路由配置

component/app.jsx

再來看下入口組件

var React = require('react');
var Router = require('react-router');

var Layout = require('./views/layouts/default.jsx');

var RouteHandler = Router.RouteHandler;


module.exports = React.createClass({
    render: function () {
        var data = this.props.data;
        return (
            <Layout title={this.props.title}>
                <RouteHandler data={data}/>
            </Layout>
        )
    }
});

Layout就是咱們的佈局了,相同的代碼總要抽出來的。

var React = require('react');
var constants=require('../../../lib/constants');

var Footer=require('../partials/footer.jsx');

module.exports = React.createClass({
    render: function render() {
        return (
            <html>
            <head>
                <title>{this.props.title}</title>
                <meta charSet='utf-8'/>
                <meta name="keywords" content={constants.promotion.keywords}/>
                <meta name="description" content={constants.promotion.description}/>
                <link rel="icon" href="//dn-cnodestatic.qbox.me/public/images/cnode_icon_32.png" type="image/x-icon"/>
                <link rel="stylesheet" href="/css/font-awesome.min.css"/>
                <link rel="stylesheet" href="/css/bootstrap.css"/>
                <link rel="stylesheet" href="/css/style.css"/>
            </head>
            <body>
            {this.props.children}
            <Footer />
            <script src="/build/vendor.js"></script>
            <script src="/build/bundle.js"></script>
            </body>
            </html>
        );
    }
});

component/views/index.jsx

這裏就是業務代碼了

var React = require('react');
var Router = require('react-router');
var $ = require('jquery');
var Navbar = require('./partials/navbar.jsx');
var queryString = require('query-string');
var utils=require('../component/utils');

var Link = Router.Link;


var Label = React.createClass({
    render: function () {
        var tab = this.props.tab;
        var data = this.props.data;

        if (data.top) {
            return <label className="label label-success">置頂</label>;
        }

        if (data.good) {
            return <label className="label label-success">精華</label>;
        }

        if (!tab || tab === 'all') {
            if (data.tab === 'share') {
                return <label className="label label-default">分享</label>;
            }

            if (data.tab === 'ask') {
                return <label className="label label-default">問答</label>;
            }

            if (data.tab === 'job') {
                return <label className="label label-default">招聘</label>;
            }
        }

        return null;
    }
});

module.exports = React.createClass({
    getInitialState: function () {
        return {
            data: this.props.data || [],
            page: 1
        }
    },
    componentWillReceiveProps: function (nextProps) {
        this.setState({
            data: nextProps.data,
            page: 1
        });
    },
    componentDidMount: function () {
        var loading = false;
        $(window).on('scroll', function () {
            var fromBottom = $(document).height() - $(window).height() - $(window).scrollTop();

            if (fromBottom <= 10 && !loading) {
                loading = true;
                var query = queryString.parse(location.search);
                query.page = this.state.page + 1;
                $.get(location.pathname + '?' + queryString.stringify(query), function (response) {
                    this.setState({
                        data: this.state.data.concat(response.data),
                        page: this.state.page + 1
                    }, function () {
                        loading = false;
                    });
                }.bind(this));
            }
        }.bind(this));
    },
    render: function () {
        var tab = this.props.query.tab;
        return (
            <div className="index">
                <Navbar />

                <div className="container">
                    <ul className="nav nav-tabs">
                        <li className={!tab || tab==='all'?'active':''}>
                            <Link to="index" query={{tab:'all'}}>所有</Link>
                        </li>
                        <li className={tab==='good'?'active':''}>
                            <Link to="index" query={{tab:'good'}}>精華</Link>
                        </li>
                        <li className={tab==='share'?'active':''}>
                            <Link to="index" query={{tab:'share'}}>分享</Link>
                        </li>
                        <li className={tab==='ask'?'active':''}>
                            <Link to="index" query={{tab:'ask'}}>問答</Link>
                        </li>
                        <li className={tab==='job'?'active':''}>
                            <Link to="index" query={{tab:'job'}}>招聘</Link>
                        </li>
                    </ul>

                    {this.state.data.map(function (item) {
                        return (
                            <div className="media">
                                <div className="media-left">
                                    <Link to="user-detail" params={{loginname:item.author.loginname}}>
                                        <img className="media-object" src={item.author.avatar_url} width="40"
                                             heigth="40" title={item.author.loginname}/>
                                    </Link>
                                </div>
                                <div className="media-body">
                                    <h4 className="media-heading">
                                        <Label tab={tab} data={item}/>
                                        <Link to="topic-detail" params={{topicId:item.id}}>{item.title}</Link>
                                    </h4>

                                    <p className="media-count">
                                        <i className="fa fa-hand-pointer-o"></i>{item.visit_count}
                                        <i className="fa fa-comment mg-l-5"></i>{item.reply_count}
                                        <i className="fa fa-calendar mg-l-5"></i>發表於{utils.getPubDate(item.create_at)}
                                    </p>
                                </div>
                            </div>
                        )
                    }.bind(this))}

                </div>
            </div>
        )
    }
});

看個效果
圖片描述

小結

整體來講開發流程仍是比較順利,固然了由於這裏沒有涉及到登陸問題。若是想在實際開發中使用React,有幾個問題不得不面對

  1. 對開發者的要求高,至少要熟悉React,React Router,特別是組件的構建,如何提升複用率?這些都是要在前期思考的。多人開發協做下,這個問題尤爲尖銳,一個很差就是一鍋粥!

  2. React的第三方組件不夠成熟,若是是後端渲染,不少組件不能用,覺得它們在代碼裏直接使用的window、document對象!

  3. 程序是爲業務服務的!

就算這樣,我仍是想還成爲那個吃桃子的人!

相關文章
相關標籤/搜索