这是一篇非常优秀的 React 教程,这篇文章对 React 组件、React Router 以及 Node 做了很好的梳理。我是 9 月份读的该文章,当时跟着教程做了一遍,收获很大。但是由于时间原因,直到现在才与大家分享,幸好赶在年底之前完成了译文,否则一定会成为 2016 年的小遗憾。翻译仓促,其中还有个别不通顺的地方,望见谅。
将 Node.js 作为运行 web 程序的后端系统的一个优势就是我们只需使用 JavaScript 这一种语言。由于这个原因,前后端可以共享一些代码,这可以将浏览器及服务器中重复的代码减少到最小。创建 JavaScript 代码的艺术是 "环境未知的",如今被看做 "通用的 JavaScript",这条术语在经过 之后,似乎取代了原始的名称 "同构的 JavaScript"。
我们在创建一个通用的 JavaScript 应用程序时,主要考虑的是:
通用的 JavaScript 仍然是一个非常新的领域,还没有框架或者方法可以成为解决所有这些问题的 "事实上" 的标准。尽管,已经有无数稳定的以及众所周知的库和工具可以成功地构建一个通用的 JavaScript 的 Web 应用程序。
在这篇文章中,我们将使用 React (包括 React Router 库) 和 Express 来构建一个展示通用渲染和路由的简单的应用程序。我们也将通过 Babel 来享受令人愉快的 EcmaScript 2015 语法以及使用 Webpack 构建浏览器端的代码。
我是一个 ,所以我们今天要创建的应用叫做 "柔道英雄"。 这个 web 应用展示了最有名的柔道运动员以及他们在奥运会及著名国际赛事中获得的奖牌情况。
这个 app 有两个主要的视图:
一个是首页,你可以选择运动员:
另一个是运动员页面,展示了他们的奖牌及其他信息:
为了更好的理解工作原理,你可以看看这个应用的 并且浏览一下整个视图。
无论如何, 你可能会问自己! 是的,它看起来像一个非常简单的应用,有一些数据及视图...
其实应用的幕后有一些普通用户不会注意的特殊的事情,但却使开发非常有趣: 这个应用使用了通用渲染及路由!
我们可以使用浏览器的开发者工具证明这一点。 当我们在浏览器中首次载入一个页面 (任意页面, 不需要是首页, 试试) ,服务器提供了视图的所有 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 配置文件)。
- webpack.config.js
余下的代码都保存在
文件夹中, 其中包含路由 (
- src
) 和渲染 (
- routes.js
和
- app-client.js
) 所需的主要文件。它包含四个子文件夹:
- server.js
: 包含所有的 React 组件
- components
: 包含数据 "模块"
- data
: 包含应用所需的所有静态文件 (css, js, images, etc.) 和一个测试应用的
- static
- index.html。
: 包含渲染服务器端的 HTML 内容的模板。
- views
需要在你的电脑上安装 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 扩展) 和 作为开发依赖:
- npm install --save-dev webpack@1.13.x babel-loader@6.2.x http-server@0.9.x
现在, 我建设你已经具备了 React 和 JSX 以及基于组件方法的基础知识。 如果没有,你可以读一下 或者 。
首先我们只专注于创建一个实用的 "单页应用" (只有客户端渲染). 稍后我们将看到如何通过添加通用的渲染和路由来改进它。
因此我们需要一个 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/
文件。 之后的文章会介绍如何使用 Webpack 和 Babel 生成该文件, 所以你现在不用担心。
- /js/bundle.js
在一个真实的应用中,我们可能会使用 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
组件,作为主组件用来定义应用的通用样式 (header, content 和 footer)
- Layout
和
- 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 (
- {
- this.props.showName && {
- name
- }
- }
- );
- }
- }
你可能注意到这个组件使用了一个国家的数组作为数据源。 这样做是有道理的,因为我们只需要很小的数据。由于是演示应用,所以数据不会变。在真实的拥有巨大以及复杂数据的应用中,你可能会使用 API 或者不同的机制将数据连接到组件。
在这个组件中同样需要注意的是我们使用了两个不同的 props,
和
- code
。第一个是强制性的, 必须传递给组件以显示对应的国旗。
- showName
props 是可选的,如果设置为 true ,组件将会在国旗的后面显示国家名。
- showName
如果你想在真实的 app 中创建可重用的组件,你需要添加 , 但我们省略这一步,因为这不是我们要构建的应用程序的目标。
组件与
- Medal
组件类似。它接受一些 props,这些属性代表与奖牌相关的数据:
- Flag
(
- 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 (
- this.props.type
- }`
- }
- title = {
- typeMap[this.props.type]
- } > {
- this.props.type
- } {
- this.props.year
- } {
- this.props.city
- } ({
- this.props.event
- }) {
- this.props.category
- }
- );
- }
- }
作为前面的组件,我们也使用一个小对象将奖牌类型的代码映射成描述性名称。
这一步我们将要创建在每个运动员页面的顶端显示的菜单,这样用户不需要返回首页就可以很方便的切换运动员:
- // 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 (
- {
- athletes.map(menuAthlete = >{
- return {
- menuAthlete.name
- };
- })
- }
- );
- }
- }
这个组件非常简单, 但是有几个需要注意的地方:
方法遍历所有的运动员,给每个人生成一个
- map
。
- Link
是 React Router 为了在视图间生成链接所提供的特殊组件。
- Link
属性,当当前路由与链接路径匹配时会添加
- 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 (this.props.id
- }`
- } >
- {
- this.props.name
- } {
- this.props.medals.length
- }
- );
- }
- }
代码非常简单。我们打算接受许多 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 (
- {this.props.children}
- This is a demo app to showcase universal rendering and routing with React and Express.
- );
- }
- }
组件非常简单,只需看代码就能了解它是如何工作的。 我们在这里使用了一个有趣的 props,
属性. 这是 React 提供给每个组件的特殊属性,允许在一个组件中嵌套组件。
- children
我们将在路由的部分看到 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 (
- {
- athletes.map(athleteData = >)
- }
- );
- }
- }
在这个组件中我们需要注意,我们使用了之前定义的
组件。基本上我们在数据模块中遍历所有的运动员, 给每个人创建一个
- AthletePreview
组件。因为
- AthletePreview
组件的数据是未知的,所以我们需要使用 (
- AthletePreview
) 来传递当前运动员的所有信息。
- {...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;
- }
- const headerStyle = {
- backgroundImage: `url(/img/$ {
- athlete.cover
- })`
- };
- return (
- {
- athlete.name
- }
- Olympic medalist from, born in {
- athlete.birth
- } (Find out more on).
- Winner of {
- athlete.medals.length
- }
- medals: {
- athlete.medals.map((medal, i) = >)
- }
- «Back to the index
- );
- }
- }
现在, 你一定可以理解上面的大部分代码以及如何用其它的组件创建这个视图。需要强调的是这个页面组件只能从外部接受运动员的 id, 所以我们引入数据模块来检索运动员的相关信息。我们在
方法开始之前对数据采用了
- render
函数。我们也考虑了接受的 id 在数据模块中不存在的情况。这种情况下会渲染
- filter
组件,我们会在后面的部分创建这个组件。
- NotFoundPage
最后一个重要的细节是我们通过
(而不是简单的
- this.props.params.id
) 来访问 id:当在
- this.props.id
中使用组件时, React Router 会创建一个特殊的对象
- Route
,并且它允许给组件传递路由参数。当我们知道如何设置应用的路由部分时,这个概念更容易理解。
- params
现在让来看看
组件, 它是生成 404 页面代码的模板:
- NotFoundPage
- // src/components/NotFoundPage.js
- import React from 'react';
- import {
- Link
- }
- from 'react-router';
- export
- default class NotFoundPage extends React.Component {
- render() {
- return (
- 404 Page not found ! Go back to the main page
- );
- }
- }
我们创建的最后一个组件是
组件,它是使用 React Router 渲染所有视图的主要组件。这个组件将使用
- AppRoutes
模块,让我们先睹为快:
- 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 = (
- );
- 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
现在看一下如何在
组件中通过 React Router 使用路由:
- AppRoutes
- // 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 (window.scrollTo(0, 0)
- }
- />
- );
- }
- }/
基本上我们只需导入
组件,然后把它添加到
- Router
函数中。router 组件会在
- render
属性中接收路由的映射。我们也定义了
- router
属性来指定要使用 HTML5 的浏览历史记录 (as an alternative you could also use).
- history
最后我们也添加了
回调函数,它的作用是每当连接被点击后窗口都会滚动到顶部。
- 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(, document.getElementById('main'));
- };
我们在这里唯一要做的就是导入
组件,然后使用
- AppRoutes
方法渲染。React app 将会在
- ReactDOM.render
DOM 元素中生成。
- #main
在运行应用之前,我们需要使用 Webpack 生成包含所有 React 组件的
组件。这个文件将会被浏览器执行,因此 Webpack 要确保将所有模块转换成可以在大多数浏览器环境执行的代码。 Webpack 会把 ES2015 和 React JSX 语法转换成相等的 (使用 Babel), 这样就可以在每个浏览器中执行。此外, 我们可以使用 Webpack 来优化最终生成的代码,比如将所有的脚本压缩合并成一个文件。
- bundle.js
来写一下 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 文件的那些包含或导入的资源进行筛选。
部分会对特定文件进行转化。在这里我们想使用 Babel 的
- module.loaders
和
- react
设置将所有引入的 JavaScript 文件转化成 ES5 代码。
- es2015
最后一部分我们使用
声明及配置我们想要使用的所有优化插件:
- plugins
允许我们在打包的过程中将
- DefinePlugin
变量定义为全局变量,和在脚本中定义的一样。 有些模块 (比如 React) 会依赖于它启用或禁用当前环境(产品或开发)的特定功能。
- NODE_ENV
删除所有重复的文件 (模块导入多个模块).
- DedupePlugin
可以减少打包后文件的体积。
- OccurenceOrderPlugin
使用 UglifyJs 压缩和混淆打包的文件。
- UglifyJsPlugin
现在我们已经准备好生成 bundle 文件,只需运行:
- NODE_ENV=production node_modules/.bin/webpack -p
环境变量和
- NODE_ENV
选项用于在产品模式下生成 bundle 文件,这会应用一些额外的优化,比如在 React 库中删除所有的调试代码。
- -p
如果一切运行正常,你将会在
目录中看到 bundle 文件。
- src/static/js/bundle.js
我们已经准备好玩一玩应用程序的第一个版本了!
我们还没有 Node.js 的 web 服务器,因此现在我们可以使用
模块 (之前安装的开发依赖) 运行一个简单的静态文件服务器:
- http-server
- node_modules/.bin/http-server src/static
现在你的应用已经可以在 上运行。
好了,现在花些时间玩一玩,点击所有的链接,浏览所有的部分。
一切似乎工作正常? 嗯,是的! 只是有一些错误警告... 如果你在首页之外的部分刷新页面, 服务器会返回 404 错误。
解决这个问题的方法有很多。我们会使用通用路由及渲染方案解决这个问题,所以让我们开始下一部分吧!
我们现在准备将应用程序升级到下一个版本,并编写缺少的服务器端部分。
为了具有服务端路由及渲染, 稍后我们将使用 Express 编写一个相对较小的服务端脚本。
渲染部分将使用 模板替换
文件,并保存在
- 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 文件仅有的不同就是我们在
div 元素中使用了模板变量
- #main
,为了在服务端生成的 HTML 代码中包含 React markup 。
- <%- 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();
- } else {
- // otherwise we can render a 404 page
- markup = renderToString();
- 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}]`);
- });
代码添加了注释, 所以不难理解其中原理。
其中重要的代码就是使用
定义的 Express 路由。 这是一个 Express 路由,它会在服务端将所有的 GET 请求编译成 URL 。 在这个路由中, 我们使用 React Router
- app.get('*', (req, res) => {...})
函数来授权路由逻辑。
- match
接收两个参数:第一个参数是配置对象,第二个是回调函数。配置对象需要有两个键值:
- ReactRouter.match
: 用于传递 React Router 的路由配置。在这里,我们传递用于服务端渲染的相同配置。
- routes
: 这是用来指定当前请求的 URL 。
- location
回调函数在匹配结束时调用。它接收三个参数,
,
- error
以及
- redirectLocation
, 我们可以通过这些参数确定匹配的结果。
- renderProps
我们可能有四种需要处理的情况:
对象参数包含了我们需要渲染组件的数据。我们需要渲染的组件是
- renderProps
(包含在 React Router 模块中),这就是使用
- RouterContext
中的值渲染整个组件树的原因。
- renderProps
这是服务器端路由机制的核心,我们使用
函数渲染与当前路由匹配的组件的 HTML 代码。
- ReactDOM.renderToString
最后,我们将产生的 HTML 代码注入到我们之前编写的
模板中,这样就可以得到发送到浏览器的 HTML 页面。
- index.ejs
现在我们准备好运行
脚本,但是因为它使用 JSX 语法,所以我们不能简单的使用
- server.js
编译器运行。我们需要使用
- node
以及如下的完整的命令 (从项目的根文件夹) :
- babel-node
- NODE_ENV=production node_modules/.bin/babel-node --presets 'react,es2015' src/server.js
现在你的应用已经可以在 上运行,因为是教程,项目到此就算完成了。
再次任意地检查应用,并尝试所有的部分和链接。你会注意到这一次我们可以刷新每一页并且服务器能够识别当前路由并呈现正确的页面。
小建议: 不要忘了输入一个随意的不存在的 URL 来检查 404 页面!
来源: