原文:Build a universal React and Node Appjavascript
演示:https://judo-heroes.herokuapp.com/css
译者:nzbinhtml
译者的话:这是一篇很是优秀的 React 教程,该文对 React 组件、React Router 以及 Node 作了很好的梳理。我是 9 月份读的该文章,当时跟着教程作了一遍,收获很大。可是因为时间缘由,直到如今才与你们分享,幸亏赶在年末以前完成了译文,不然必定会成为 2016 年的小遗憾。翻译仓促,其中还有个别不通顺的地方,望见谅。前端
将 Node.js 做为运行 web 程序的后端系统的一个优点就是咱们只需使用 JavaScript 这一种语言。因为这个缘由,先后端能够共享一些代码,这能够将浏览器及服务器中重复的代码减小到最小。建立 JavaScript 代码的艺术是 "环境未知的",现在被看作 "通用的 JavaScript",这条术语在通过 很 长时间 争论 以后,彷佛取代了原始的名称 "同构的 JavaScript"。java
咱们在建立一个通用的 JavaScript 应用程序时,主要考虑的是:node
通用的 JavaScript 仍然是一个很是新的领域,尚未框架或者方法能够成为解决全部这些问题的 "事实上" 的标准。尽管,已经有无数稳定的以及众所周知的库和工具能够成功地构建一个通用的 JavaScript 的 Web 应用程序。react
在这篇文章中,咱们将使用 React (包括 React Router 库) 和 Express 来构建一个展现通用渲染和路由的简单的应用程序。咱们也将经过 Babel 来享受使人愉快的 EcmaScript 2015 语法以及使用 Webpack 构建浏览器端的代码。webpack
我是一个 柔道迷 ,因此咱们今天要建立的应用叫作 "柔道英雄"。 这个 web 应用展现了最有名的柔道运动员以及他们在奥运会及著名国际赛事中得到的奖牌状况。git
这个 app 有两个主要的视图:es6
一个是首页,你能够选择运动员:
另外一个是运动员页面,展现了他们的奖牌及其余信息:
为了更好的理解工做原理,你能够看看这个应用的 demo 而且浏览一下整个视图。
不管如何,你可能会问本身! 是的,它看起来像一个很是简单的应用,有一些数据及视图...
其实应用的幕后有一些普通用户不会注意的特殊的事情,但却使开发很是有趣: 这个应用使用了通用渲染及路由!
咱们可使用浏览器的开发者工具证实这一点。 当咱们在浏览器中首次载入一个页面(任意页面, 不须要是首页, 试试 这一个) ,服务器提供了视图的全部 HTML 代码而且浏览器只需下载连接的资源(图像, 样式表及脚本):
而后当咱们切换视图的时候,一切都在浏览器中发生:没有从服务器加载的 HTML 代码, 只有被浏览器加载的新资源 (以下示例中的 3 张新图片) :
咱们能够在命令行使用 curl 命令作另外一个快速测试 (若是你仍然不相信):
curl -sS "https://judo-heroes.herokuapp.com/athlete/teddy-riner"
你将看到整个从服务器端生成的 HTML 页面(包括被 React 渲染的代码):
我保证你如今已经信心满满地想要跃跃欲试,因此让咱们开始编码吧!
在教程的最后,咱们的文件结构会像下面的文件树同样:
├── package.json ├── webpack.config.js ├── src │ ├── app-client.js │ ├── routes.js │ ├── server.js │ ├── components │ │ ├── AppRoutes.js │ │ ├── AthletePage.js │ │ ├── AthletePreview.js │ │ ├── AthletesMenu.js │ │ ├── Flag.js │ │ ├── IndexPage.js │ │ ├── Layout.js │ │ ├── Medal.js │ │ └── NotFoundPage.js │ ├── data │ │ └── athletes.js │ ├── static │ │ ├── index.html │ │ ├── css │ │ ├── favicon.ico │ │ ├── img │ │ └── js │ └── views ` └── index.ejs
主文件夹中有 package.json
(描述项目而且定义依赖) 和 webpack.config.js
(Webpack 配置文件)。
余下的代码都保存在 src
文件夹中, 其中包含路由 (routes.js
) 和渲染 (app-client.js
和 server.js
) 所需的主要文件。它包含四个子文件夹:
components
: 包含全部的 React 组件data
: 包含数据 "模块"static
: 包含应用所需的全部静态文件 (css, js, images, etc.) 和一个测试应用的 index.html。
views
: 包含渲染服务器端的 HTML 内容的模板。须要在你的电脑上安装 Node.js (最好是版本 6) 和 NPM。
在硬盘上的任意地方建立一个名为 judo-heroes
的文件夹而且在给目录下打开终端,而后输入:
npm init
这将会启动 Node.js 项目并容许咱们添加全部须要的依赖。
咱们须要安装 babel, ejs, express, react 和 react-router 。 你能够输入如下命令:
npm install --save babel-cli@6.11.x babel-core@6.13.x \ babel-preset-es2015@6.13.x babel-preset-react@6.11.x ejs@2.5.x \ express@4.14.x react@15.3.x react-dom@15.3.x react-router@2.6.x
咱们也须要安装 Webpack (以及它的 Babel loader 扩展) 和 http-server 做为开发依赖:
npm install --save-dev webpack@1.13.x babel-loader@6.2.x http-server@0.9.x
如今, 我建设你已经具有了 React 和 JSX 以及基于组件方法的基础知识。 若是没有,你能够读一下 excellent article on React components 或者 React related articles on Scotch.io。
首先咱们只专一于建立一个实用的 "单页应用" (只有客户端渲染). 稍后咱们将看到如何经过添加通用的渲染和路由来改进它。
所以咱们须要一个 HTML 模板做为应用的主入口,将其保存在 src/static/index.html
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Judo Heroes - A Universal JavaScript demo application with React</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div id="main"></div> <script src="/js/bundle.js"></script> </body> </html>
这里没有什么特别的。只需强调两件事:
src/static/css/
。/js/bundle.js
文件。 以后的文章会介绍如何使用 Webpack 和 Babel 生成该文件, 因此你如今不用担忧。在一个真实的应用中,咱们可能会使用 API 来获取应用所需的数据。
在这个案例中只有 5 个运动员及其相关信息的不多的数据, 因此能够简单点,把数据保存在 JavaScript 模块中。这种方法能够很简单的在组件或模块中同步导入数据, 避免增长复杂度以及在通用 JavaScript 项目中管理异步 API 的陷阱, 这也不是这篇文章的目的。
让咱们看一下这模块:
// src/data/athletes.js const athletes = [ { 'id': 'driulis-gonzalez', 'name': 'Driulis González', 'country': 'cu', 'birth': '1973', 'image': 'driulis-gonzalez.jpg', 'cover': 'driulis-gonzalez-cover.jpg', 'link': 'https://en.wikipedia.org/wiki/Driulis_González', 'medals': [ { 'year': '1992', 'type': 'B', 'city': 'Barcelona', 'event': 'Olympic Games', 'category': '-57kg' }, { 'year': '1993', 'type': 'B', 'city': 'Hamilton', 'event': 'World Championships', 'category': '-57kg' }, { 'year': '1995', 'type': 'G', 'city': 'Chiba', 'event': 'World Championships', 'category': '-57kg' }, { 'year': '1995', 'type': 'G', 'city': 'Mar del Plata', 'event': 'Pan American Games', 'category': '-57kg' }, { 'year': '1996', 'type': 'G', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-57kg' }, { 'year': '1997', 'type': 'S', 'city': 'Osaka', 'event': 'World Championships', 'category': '-57kg' }, { 'year': '1999', 'type': 'G', 'city': 'Birmingham', 'event': 'World Championships', 'category': '-57kg' }, { 'year': '2000', 'type': 'S', 'city': 'Sydney', 'event': 'Olympic Games', 'category': '-57kg' }, { 'year': '2003', 'type': 'G', 'city': 'S Domingo', 'event': 'Pan American Games', 'category': '-63kg' }, { 'year': '2003', 'type': 'S', 'city': 'Osaka', 'event': 'World Championships', 'category': '-63kg' }, { 'year': '2004', 'type': 'B', 'city': 'Athens', 'event': 'Olympic Games', 'category': '-63kg' }, { 'year': '2005', 'type': 'B', 'city': 'Cairo', 'event': 'World Championships', 'category': '-63kg' }, { 'year': '2006', 'type': 'G', 'city': 'Cartagena', 'event': 'Central American and Caribbean Games', 'category': '-63kg' }, { 'year': '2006', 'type': 'G', 'city': 'Cartagena', 'event': 'Central American and Caribbean Games', 'category': 'Tema' }, { 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'Pan American Games', 'category': '-63kg' }, { 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'World Championships', 'category': '-63kg' }, ], }, { // ... } ]; export default athletes;
为简洁起见这里的文件已被截断,咱们只是展现一个运动员的数据。若是你想看所有的代码, 在官方仓库中查看。你能够把文件下载到 src/data/athletes.js
。
如你所见,这个文件包含了一个对象数组。数组中的每一个对象表明一个运动员,包含一些通用的信息好比 id
, name
和 country
,另一个对象数组表明运动员得到的奖牌。
你能够在仓库中下载 全部的图片文件 ,复制到: src/static/img/
。
咱们将把应用的视图分红若干个组件:
AthletePreview
, Flag
, Medal
和 AthletesMenu
Layout
组件,做为主组件用来定义应用的通用样式(header, content 和 footer)IndexPage
和 AthletePage
NotFoundPage
AppRoutes
组件咱们将要建立的第一个组件会展现一个漂亮的国旗以及它所表明的国家名:
// src/components/Flag.js import React from 'react'; const data = { 'cu': { 'name': 'Cuba', 'icon': 'flag-cu.png', }, 'fr': { 'name': 'France', 'icon': 'flag-fr.png', }, 'jp': { 'name': 'Japan', 'icon': 'flag-jp.png', }, 'nl': { 'name': 'Netherlands', 'icon': 'flag-nl.png', }, 'uz': { 'name': 'Uzbekistan', 'icon': 'flag-uz.png', } }; export default class Flag extends React.Component { render() { const name = data[this.props.code].name; const icon = data[this.props.code].icon; return ( <span className="flag"> <img className="icon" title={name} src={`/img/${icon}`}/> {this.props.showName && <span className="name"> {name}</span>} </span> ); } }
你可能注意到这个组件使用了一个国家的数组做为数据源。 这样作是有道理的,由于咱们只须要很小的数据。因为是演示应用,因此数据不会变。在真实的拥有巨大以及复杂数据的应用中,你可能会使用 API 或者不一样的机制将数据链接到组件。
在这个组件中一样须要注意的是咱们使用了两个不一样的 props, code
和 showName
。第一个是强制性的, 必须传递给组件以显示对应的国旗。 showName
props 是可选的,若是设置为 true ,组件将会在国旗的后面显示国家名。
若是你想在真实的 app 中建立可重用的组件,你须要添加 props 的验证及默认值, 但咱们省略这一步,由于这不是咱们要构建的应用程序的目标。
Medal
组件与 Flag
组件相似。它接受一些 props,这些属性表明与奖牌相关的数据: type
(G
表示金牌, S
表示银牌以及 B
表示铜牌), year
(哪一年赢得), event
(赛事名称), city
(举办比赛的城市)以及 category
(运动员赢得比赛的级别)。
// src/components/Medal.js import React from 'react'; const typeMap = { 'G': 'Gold', 'S': 'Silver', 'B': 'Bronze' }; export default class Medal extends React.Component { render() { return ( <li className="medal"> <span className={`symbol symbol-${this.props.type}`} title={typeMap[this.props.type]}>{this.props.type}</span> <span className="year">{this.props.year}</span> <span className="city"> {this.props.city}</span> <span className="event"> ({this.props.event})</span> <span className="category"> {this.props.category}</span> </li> ); } }
做为前面的组件,咱们也使用一个小对象将奖牌类型的代码映射成描述性名称。
这一步咱们将要建立在每一个运动员页面的顶端显示的菜单,这样用户不须要返回首页就能够很方便的切换运动员:
// src/components/AthletesMenu.js import React from 'react'; import { Link } from 'react-router'; import athletes from '../data/athletes'; export default class AthletesMenu extends React.Component { render() { return ( <nav className="atheletes-menu"> {athletes.map(menuAthlete => { return <Link key={menuAthlete.id} to={`/athlete/${menuAthlete.id}`} activeClassName="active"> {menuAthlete.name} </Link>; })} </nav> ); } }
这个组件很是简单, 可是有几个须要注意的地方:
map
方法遍历全部的运动员,给每一个人生成一个 Link
。Link
是 React Router 为了在视图间生成连接所提供的特殊组件。activeClassName
属性,当当前路由与连接路径匹配时会添加 active
的类。 AthletePreview
组件用在首页显示运动员的图片及名称。来看一下它的代码:
// src/components/AthletePreview.js import React from 'react'; import { Link } from 'react-router'; export default class AthletePreview extends React.Component { render() { return ( <Link to={`/athlete/${this.props.id}`}> <div className="athlete-preview"> <img src={`img/${this.props.image}`}/> <h2 className="name">{this.props.name}</h2> <span className="medals-count"><img src="/img/medal.png"/> {this.props.medals.length}</span> </div> </Link> ); } }
代码很是简单。咱们打算接受许多 props 来描述运动员的特征,好比 id
, image
, name
以及 medals
。再次注意咱们使用 Link
组件在运动员页面建立了一个连接。
既然咱们已经建立了全部的基本组件,如今咱们开始建立那些给应用程序提供视觉结构的组件。 第一个是 Layout
组件, 它的惟一用途就是给整个应用提供展现模板,包括页头区、 主内容区以及页脚区:
// src/components/Layout.js import React from 'react'; import { Link } from 'react-router'; export default class Layout extends React.Component { render() { return ( <div className="app-container"> <header> <Link to="/"> <img className="logo" src="/img/logo-judo-heroes.png"/> </Link> </header> <div className="app-content">{this.props.children}</div> <footer> <p> This is a demo app to showcase universal rendering and routing with <strong>React</strong> and <strong>Express</strong>. </p> </footer> </div> ); } }
组件很是简单,只需看代码就能了解它是如何工做的。 咱们在这里使用了一个有趣的 props, children
属性. 这是 React 提供给每一个组件的特殊属性,容许在一个组件中嵌套组件。
咱们将在路由的部分看到 React Router 如何在 Layout
组件中嵌套另外一个组件。
这个组件构成了整个首页,它包含了以前定义的一些组件:
// src/components/IndexPage.js import React from 'react'; import AthletePreview from './AthletePreview'; import athletes from '../data/athletes'; export default class IndexPage extends React.Component { render() { return ( <div className="home"> <div className="athletes-selector"> {athletes.map(athleteData => <AthletePreview key={athleteData.id} {...athleteData} />)} </div> </div> ); } }
在这个组件中咱们须要注意,咱们使用了以前定义的 AthletePreview
组件。基本上咱们在数据模块中遍历全部的运动员, 给每一个人建立一个 AthletePreview
组件。由于 AthletePreview
组件的数据是未知的,因此咱们须要使用 JSX 扩展操做符 ({...object}
) 来传递当前运动员的全部信息。
咱们用一样的方式建立 AthletePage
组件:
// src/components/AthletePage.js import React from 'react'; import { Link } from 'react-router'; import NotFoundPage from './NotFoundPage'; import AthletesMenu from './AthletesMenu'; import Medal from './Medal'; import Flag from './Flag'; import athletes from '../data/athletes'; export default class AthletePage extends React.Component { render() { const id = this.props.params.id; const athlete = athletes.filter((athlete) => athlete.id === id)[0]; if (!athlete) { return <NotFoundPage/>; } const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` }; return ( <div className="athlete-full"> <AthletesMenu/> <div className="athlete"> <header style={headerStyle}/> <div className="picture-container"> <img src={`/img/${athlete.image}`}/> <h2 className="name">{athlete.name}</h2> </div> <section className="description"> Olympic medalist from <strong><Flag code={athlete.country} showName="true"/></strong>, born in {athlete.birth} (Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>). </section> <section className="medals"> <p>Winner of <strong>{athlete.medals.length}</strong> medals:</p> <ul>{ athlete.medals.map((medal, i) => <Medal key={i} {...medal}/>) }</ul> </section> </div> <div className="navigateBack"> <Link to="/">« Back to the index</Link> </div> </div> ); } }
如今, 你必定能够理解上面的大部分代码以及如何用其它的组件建立这个视图。须要强调的是这个页面组件只能从外部接受运动员的 id, 因此咱们引入数据模块来检索运动员的相关信息。咱们在 render
方法开始以前对数据采用了 filter
函数。咱们也考虑了接受的 id 在数据模块中不存在的状况。这种状况下会渲染 NotFoundPage
组件,咱们会在后面的部分建立这个组件。
最后一个重要的细节是咱们经过 this.props.params.id
(而不是简单的 this.props.id
)来访问 id:当在 Route
中使用组件时, React Router 会建立一个特殊的对象 params
,而且它容许给组件传递路由参数。当咱们知道如何设置应用的路由部分时,这个概念更容易理解。
如今让来看看 NotFoundPage
组件, 它是生成 404 页面代码的模板:
// src/components/NotFoundPage.js import React from 'react'; import { Link } from 'react-router'; export default class NotFoundPage extends React.Component { render() { return ( <div className="not-found"> <h1>404</h1> <h2>Page not found!</h2> <p> <Link to="/">Go back to the main page</Link> </p> </div> ); } }
咱们建立的最后一个组件是 AppRoutes
组件,它是使用 React Router 渲染全部视图的主要组件。这个组件将使用 routes
模块,让咱们先睹为快:
// src/routes.js import React from 'react' import { Route, IndexRoute } from 'react-router' import Layout from './components/Layout'; import IndexPage from './components/IndexPage'; import AthletePage from './components/AthletePage'; import NotFoundPage from './components/NotFoundPage'; const routes = ( <Route path="/" component={Layout}> <IndexRoute component={IndexPage}/> <Route path="athlete/:id" component={AthletePage}/> <Route path="*" component={NotFoundPage}/> </Route> ); export default routes;
在这个文件中咱们使用 React Router 的 Route
组件将路由映射到以前定义的组件中。注意如何在一个主 Route
组件中嵌套路由。我解释一下它的原理:
/
路径映射到 Layout
组件。这容许咱们在应用程序的每一个部分使用自定义的 layout 。在嵌套路由中定义的组件将会代替 this.props.children
属性在 Layout
组件中被渲染,咱们在以前已经讨论过。IndexRoute
,这个特殊的路由所定义的组件会在咱们浏览父路由(/)的索引页时被渲染。咱们将 IndexPage
组件做为索引路由。athlete/:id
被映射为 AthletePage
。注意咱们使用了命名参数 :id
。因此这个路由会匹配全部前缀是 /athlete/
的路径, 余下的部分将关联参数 id
并对应组件中的 this.props.params.id
。*
会将其它路径映射到 NotFoundPage
组件。这个路由必须被定义为最后一条 。如今看一下如何在 AppRoutes
组件中经过 React Router 使用路由:
// src/components/AppRoutes.js import React from 'react'; import { Router, browserHistory } from 'react-router'; import routes from '../routes'; export default class AppRoutes extends React.Component { render() { return ( <Router history={browserHistory} routes={routes} onUpdate={() => window.scrollTo(0, 0)}/> ); } }
基本上咱们只需导入 Router
组件,而后把它添加到 render
函数中。router 组件会在 router
属性中接收路由的映射。咱们也定义了 history
属性来指定要使用 HTML5 的浏览历史记录(as an alternative you could also use hashHistory).
最后咱们也添加了 onUpdate
回调函数,它的做用是每当链接被点击后窗口都会滚动到顶部。
完成咱们的应用程序的首个版本的最后一部分代码就是编写在浏览器中启动 app 的 JavaScript 逻辑代码:
// src/app-client.js import React from 'react'; import ReactDOM from 'react-dom'; import AppRoutes from './components/AppRoutes'; window.onload = () => { ReactDOM.render(<AppRoutes/>, document.getElementById('main')); };
咱们在这里惟一要作的就是导入 AppRoutes
组件,而后使用 ReactDOM.render
方法渲染。React app 将会在 #main
DOM 元素中生成。
在运行应用以前,咱们须要使用 Webpack 生成包含全部 React 组件的 bundle.js
组件。这个文件将会被浏览器执行,所以 Webpack 要确保将全部模块转换成能够在大多数浏览器环境执行的代码。 Webpack 会把 ES2015 和 React JSX 语法转换成相等的 ES5 语法(使用 Babel), 这样就能够在每一个浏览器中执行。此外, 咱们可使用 Webpack 来优化最终生成的代码,好比将全部的脚本压缩合并成一个文件。
来写一下 webpack 的配置文件:
// webpack.config.js const webpack = require('webpack'); const path = require('path'); module.exports = { entry: path.join(__dirname, 'src', 'app-client.js'), output: { path: path.join(__dirname, 'src', 'static', 'js'), filename: 'bundle.js' }, module: { loaders: [{ test: path.join(__dirname, 'src'), loader: ['babel-loader'], query: { cacheDirectory: 'babel_cache', presets: ['react', 'es2015'] } }] }, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) }), new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }, mangle: true, sourcemap: false, beautify: false, dead_code: true }) ] };
在配置文件的第一部分,咱们定义了文件入口以及输出路径。 文件入口是启动应用的 JavaScript 文件。Webpack 会使用递归方法将打包进 bundle 文件的那些包含或导入的资源进行筛选。
module.loaders
部分会对特定文件进行转化。在这里咱们想使用 Babel 的 react
和 es2015
设置将全部引入的 JavaScript 文件转化成 ES5 代码。
最后一部分咱们使用 plugins
声明及配置咱们想要使用的全部优化插件:
DefinePlugin
容许咱们在打包的过程当中将 NODE_ENV
变量定义为全局变量,和在脚本中定义的同样。 有些模块 (好比 React) 会依赖于它启用或禁用当前环境(产品或开发)的特定功能。DedupePlugin
删除全部重复的文件 (模块导入多个模块).OccurenceOrderPlugin
能够减小打包后文件的体积。UglifyJsPlugin
使用 UglifyJs 压缩和混淆打包的文件。如今咱们已经准备好生成 bundle 文件,只需运行:
NODE_ENV=production node_modules/.bin/webpack -p
NODE_ENV
环境变量和 -p
选项用于在产品模式下生成 bundle 文件,这会应用一些额外的优化,好比在 React 库中删除全部的调试代码。
若是一切运行正常,你将会在 src/static/js/bundle.js
目录中看到 bundle 文件。
咱们已经准备好玩一玩应用程序的第一个版本了!
咱们尚未 Node.js 的 web 服务器,所以如今咱们可使用 http-server
模块(以前安装的开发依赖) 运行一个简单的静态文件服务器:
node_modules/.bin/http-server src/static
如今你的应用已经能够在 http://localhost:8080 上运行。
好了,如今花些时间玩一玩,点击全部的连接,浏览全部的部分。
一切彷佛工做正常? 嗯,是的! 只是有一些错误警告... 若是你在首页以外的部分刷新页面, 服务器会返回 404 错误。
解决这个问题的方法有不少。咱们会使用通用路由及渲染方案解决这个问题,因此让咱们开始下一部分吧!
咱们如今准备将应用程序升级到下一个版本,并编写缺乏的服务器端部分。
为了具备服务端路由及渲染, 稍后咱们将使用 Express 编写一个相对较小的服务端脚本。
渲染部分将使用 ejs 模板替换 index.html
文件,并保存在 src/views/index.ejs
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Judo Heroes - A Universal JavaScript demo application with React</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div id="main"><%- markup -%></div> <script src="/js/bundle.js"></script> </body> </html>
与原始 HTML 文件仅有的不一样就是咱们在 #main
div 元素中使用了模板变量 <%- markup -%>
,为了在服务端生成的 HTML 代码中包含 React markup 。
如今咱们准备写服务端应用:
// src/server.js import path from 'path'; import { Server } from 'http'; import Express from 'express'; import React from 'react'; import { renderToString } from 'react-dom/server'; import { match, RouterContext } from 'react-router'; import routes from './routes'; import NotFoundPage from './components/NotFoundPage'; // initialize the server and configure support for ejs templates const app = new Express(); const server = new Server(app); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // define the folder that will be used for static assets app.use(Express.static(path.join(__dirname, 'static'))); // universal routing and rendering app.get('*', (req, res) => { match( { routes, location: req.url }, (err, redirectLocation, renderProps) => { // in case of error display the error message if (err) { return res.status(500).send(err.message); } // in case of redirect propagate the redirect to the browser if (redirectLocation) { return res.redirect(302, redirectLocation.pathname + redirectLocation.search); } // generate the React markup for the current route let markup; if (renderProps) { // if the current route matched we have renderProps markup = renderToString(<RouterContext {...renderProps}/>); } else { // otherwise we can render a 404 page markup = renderToString(<NotFoundPage/>); res.status(404); } // render the index template with the embedded React markup return res.render('index', { markup }); } ); }); // start the server const port = process.env.PORT || 3000; const env = process.env.NODE_ENV || 'production'; server.listen(port, err => { if (err) { return console.error(err); } console.info(`Server running on http://localhost:${port} [${env}]`); });
代码添加了注释, 因此不难理解其中原理。
其中重要的代码就是使用 app.get('*', (req, res) => {...})
定义的 Express 路由。 这是一个 Express catch-all 路由,它会在服务端将全部的 GET 请求编译成 URL 。 在这个路由中, 咱们使用 React Router match
函数来受权路由逻辑。
ReactRouter.match
接收两个参数:第一个参数是配置对象,第二个是回调函数。配置对象须要有两个键值:
routes
: 用于传递 React Router 的路由配置。在这里,咱们传递用于服务端渲染的相同配置。location
: 这是用来指定当前请求的 URL 。回调函数在匹配结束时调用。它接收三个参数, error
, redirectLocation
以及 renderProps
, 咱们能够经过这些参数肯定匹配的结果。
咱们可能有四种须要处理的状况:
renderProps
对象参数包含了咱们须要渲染组件的数据。咱们须要渲染的组件是 RouterContext
(包含在 React Router 模块中),这就是使用 renderProps
中的值渲染整个组件树的缘由。这是服务器端路由机制的核心,咱们使用 ReactDOM.renderToString
函数渲染与当前路由匹配的组件的 HTML 代码。
最后,咱们将产生的 HTML 代码注入到咱们以前编写的 index.ejs
模板中,这样就能够获得发送到浏览器的 HTML 页面。
如今咱们准备好运行 server.js
脚本,可是由于它使用 JSX 语法,因此咱们不能简单的使用 node
编译器运行。咱们须要使用 babel-node
以及以下的完整的命令 (从项目的根文件夹) :
NODE_ENV=production node_modules/.bin/babel-node --presets 'react,es2015' src/server.js
如今你的应用已经能够在 http://localhost:3000 上运行,由于是教程,项目到此就算完成了。
再次任意地检查应用,并尝试全部的部分和连接。你会注意到这一次咱们能够刷新每一页而且服务器可以识别当前路由并呈现正确的页面。
小建议: 不要忘了输入一个随意的不存在的 URL 来检查 404 页面!