一直想用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的,真棒!拿首页的编码举个例子
路由我这里用的本身的路由组织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); };
这样咱们又作到了接口的复用!
来看看咱们打包的入口
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结构。
再来看看路由,不熟悉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;
都是些基本的路由配置
再来看下入口组件
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> ); } });
这里就是业务代码了
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对象!
程序是为业务服务的!
就算这样,我仍是想还成为那个吃桃子的人!