概述
一直想用React做些东西,苦于没有实际项目练手,所以一直都是自己在搞些小玩意儿,做过用React Router构建的内部订餐系统,是个SPA,也在社区分享过。由于一个人做全栈开发,数据库(mongodb)全靠自己设,需求全靠自己编,页面全靠自己扯,心好累,感觉不会在爱了! SPA用来构建内部的系统完全没问题,但是用来做门户、做电商网站就不行了,为啥?因为SEO,很多的MVVM,MV*框架不能用、不敢用都是基于这个原因(当然也可能因为我不会用)。 最近拿CNode的API做了个React服务器端渲染的例子,这里跟大家分享下这个项目的构建过程和代码组织,未必好,主要提供一个思路。
搭建
整体项目目录如上,这里作个说明,附上代码地址,上面有说明怎么使用。
- component 我们的组件目录,这里放置了view、ui等组件
- lib 后端代码,如过滤器等
- node_modules 依赖包
- public 静态资源
- routes 路由
浏览器端和服务器端的代码我们没必要完全独立,实际上有时候代码是可以复用的。举个例子 表单异步提交的时候,后端返回一个state状态告知是否成功,相信大部分的人的第一反应都是抽出常量 constants.js
module.exports = {
state: {
SUCCESS: 10000
}
};
当然了,浏览器端也是要判断这个state的,为了提高代码的复用性,这里同样抽出 constants.js
module.exports = {
state: {
SUCCESS: 10000
}
};
虽然内容相同,实际上这是两个不同的js,分处不同的目录,oh shit。我的开发理念一般是这样的
相同的代码坚决不写第二遍,特殊情况除外!
采用React后端渲染,我用了webpack打包,实际上就避免了这个问题,写一份constants.js,打包到浏览器端去,NICE!
编码
既然是后端渲染,首先得选择一个模板引擎,这里我采用的react-engine,具体配置和使用可以参考文档,这里我就不赘述了。既然是构建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,有几个问题不得不面对
- 对开发者的要求高,至少要熟悉React,React Router,特别是组件的构建,如何提高复用率?这些都是要在前期思考的。多人开发协作下,这个问题尤其尖锐,一个不好就是一锅粥!
- React的第三方组件不够成熟,如果是后端渲染,很多组件不能用,以为它们在代码里直接使用的window、document对象!
- 程序是为业务服务的!
就算这样,我还是想还成为那个吃桃子的人!