react node服务器
In this article we are going to learn how to build a simple "Universal JavaScript" application (a.k.a. "Isomorphic") using React, React Router and Express.
在本文中,我们将学习如何使用React , React Router和Express构建一个简单的“ Universal JavaScript ”应用程序(又名“ Isomorphic”)。
Warm up your fingers and get your keyboard ready... it's going to be fun!
加热手指并准备好键盘……这将很有趣!
Hi, I am Luciano and I am the co-author of Node.js Design Patterns Second Edition (Packt), a book that will take you on a journey across various ideas and components, and the challenges you would commonly encounter while designing and developing software using the Node.js platform. In this book you will discover the "Node.js way" of dealing with design and coding decisions. It also features an entire chapter dedicated to Universal JavaScript. If this is the first time you read this term, keep reading, you are going to love this article!
嗨,我是Luciano,并且是Node.js Design Patterns Second Edition(Packt)的合著者,这本书将带您探索各种概念和组件,以及在设计和开发过程中通常会遇到的挑战使用Node.js平台的软件。 在本书中,您将发现处理设计和编码决策的“ Node.js方法”。 它还包含一整章专门介绍Universal JavaScript的章节。 如果这是您第一次阅读本术语,请继续阅读,您将爱上这篇文章!
One of the advantages of having Node.js as runtime for the backend of a web application is that we have to deal only with JavaScript as a single language across the web stack. With this capability it is totally legit to be willing to share some code between the frontend and the backend to reduce the code duplication between the browser and the server to the bare minimun. The art of creating JavaScript code that is "environment agnostic" is today being recognized as "Universal JavaScript", term that — after a very long debate — seems to have won a war against the original name "Isomorphic JavaScript".
将Node.js作为Web应用程序后端的运行时的优点之一是,我们只需要在整个Web堆栈中将JavaScript作为一种语言来处理。 有了此功能,愿意在前端和后端之间共享一些代码以将浏览器和服务器之间的代码重复减少到最低限度是完全合法的。 创建JavaScript代码的艺术是“环境无关的”今天是被认定为“通用JavaScript”,术语, -经过一个很 长的 辩论 -似乎已经赢得了对原来的名字“同构JavaScript”的战争。
The main concerns that we generally have to face when building an Universal JavaScript application are:
在构建通用JavaScript应用程序时,我们通常要面对的主要问题是:
Universal JavaScript is still a pretty fresh field and there is no framework or approach that emerged as a "de-facto" standard with ready-made solutions for all these problems yet. Although, there are already a miriad of stable and well known libraries and tools that can be combined to successfully build a Universal JavaScript web application.
通用JavaScript仍然是一个非常新鲜的领域,尚无框架或方法作为“事实上的”标准出现,并且没有针对所有这些问题的现成解决方案。 虽然,已经有大量稳定且众所周知的库和工具可以组合使用,以成功构建Universal JavaScript Web应用程序。
In this article we are going to use React (with its companion library React Router) and Express to build a simple application focused on showcasing universal rendering and routing. We will also use Babel to take advantage of the lovely EcmaScript 2015 syntax and Webpack to build our code for the browser.
在本文中,我们将使用React(及其配套库React Router)和Express来构建一个专注于展示通用渲染和路由的简单应用程序。 我们还将使用Babel来利用可爱的EcmaScript 2015语法和Webpack来为浏览器构建代码。
I am a Judo fan and so the app we are going to build today is "Judo Heroes", a web app that showcases some the most famous Judo athletes and their collection of medals from the Olympic Games and other prestigious international tournaments.
我是柔道迷 ,所以我们今天要构建的应用程序是“柔道英雄”,这是一个网络应用程序,其中展示了一些最著名的柔道运动员及其从奥运会和其他享有盛誉的国际比赛中获得的奖牌。
This app has essentially two views:
此应用实质上具有两个视图:
An index page where you can select the athletes:
一个索引页面,您可以在其中选择运动员:
And an athlete page that showcases their medals and some other details:
还有一个运动员页面,展示了他们的奖牌和其他一些细节:
To understand better how it works you can have a look at the demo app and navigate across the views.
为了更好地了解其工作原理,您可以查看演示应用程序并在视图之间导航。
What's the matter with it anyway, you are probably asking yourself! Yes, it looks like a very simple app, with some data and a couple of views...
无论如何,你可能在问自己! 是的,它看起来像一个非常简单的应用,带有一些数据和一些视图...
Well there's something very peculiar that happens behind the scenes that will be hardly noticed by a regular user but it makes developement super interesting: this app is using universal rendering and routing!
好吧,在幕后发生了一些非常特殊的事情,普通用户几乎不会注意到,但是这使开发变得非常有趣:此应用程序正在使用通用渲染和路由!
We can prove this using the developers tools of the browser. When we initially load a page in the browser (any page, not necessarily the home page, try for example this one) the server provides the full HTML code of the view and the browser only needs to download linked resources (images, stylesheets and scripts):
我们可以使用浏览器的开发人员工具来证明这一点。 当我们最初在浏览器中加载页面(任何页面,不一定是首页,例如尝试此页面)时,服务器将提供视图的完整HTML代码,浏览器仅需要下载链接的资源(图像,样式表和脚本) ):
Then, from there, when we switch to another view, everything happens only on the browser: no HTML code is loaded from the server and only the new resources (3 new images in the following example) are loaded by the browser:
然后,从那里,当我们切换到另一个视图时,一切都只发生在浏览器上:服务器未加载HTML代码,浏览器仅加载了新资源(以下示例中为3个新图像):
We can do another quick test (if you are still not convinced) from the command line using curl:
我们可以使用curl从命令行进行另一个快速测试(如果您仍然不满意):
curl -sS "https://judo-heroes.herokuapp.com/athlete/teddy-riner"
You will see the full HTML page (including the code rendered by React) being generated directly from the server:
您将看到完整HTML页面(包括React渲染的代码)直接从服务器生成:
I bet you are now convinced enough and eager to get your hands dirty, so let's start coding!
我敢打赌,您现在已经足够确信并渴望弄脏您的手,所以让我们开始编码!
At the end of this tutorial our project structure will look like in the following tree:
在本教程的最后,我们的项目结构如下图所示:
├── 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
In the main level we have our package.json
(to describe the project and define the dependencies) and webpack.config.js
(Webpack configuration file).
在主级别,我们有package.json
(用于描述项目并定义依赖项)和webpack.config.js
(Webpack配置文件)。
All the rest of the code will be stored inside the folder src
, which contains the main files needed for routing (routes.js
) and rendering (app-client.js
and server.js
). It also contains 4 subfolders:
所有其余代码将存储在src
文件夹中,该文件夹包含路由( routes.js
)和渲染( app-client.js
和server.js
)所需的主文件。 它还包含4个子文件夹:
components
: contains all the React components components
:包含所有React组件 data
: contains our data "module" data
:包含我们的数据“模块” static
: contains all the static files needed for our application (css, js, images, etc.) and an index.html
that we will use initially to test our app. static
:包含应用程序所需的所有静态文件(css,js,图像等)和一个index.html
,我们将首先使用它们来测试我们的应用程序。 views
: contains the template that we will use from the server to render the HTML content from the server. views
:包含我们将从服务器使用的模板,以从服务器呈现HTML内容。 The only requisite here is to have Node.js (version 6 is preferred) and NPM installed in your machine.
这里唯一的要求是在计算机上安装Node.js (首选6版)和NPM 。
Let's create a new folder called judo-heroes
somewhere in the disk and point the terminal there, then launch:
让我们在磁盘上的某个地方创建一个名为judo-heroes
的新文件夹,并将终端指向该文件夹,然后启动:
npm init
This will bootstrap our Node.js project allowing us to add all the needed dependencies.
这将引导我们的Node.js项目,允许我们添加所有需要的依赖项。
We will need to have babel, ejs, express, react and react-router installed. To do so you can run the following command:
我们将需要安装babel , ejs , express , react和react-router 。 为此,您可以运行以下命令:
npm install --save [email protected] [email protected] \
[email protected] [email protected] [email protected] \
[email protected] [email protected] [email protected] [email protected]
We will also need to install Webpack(with its Babel loader extension) and http-server as a development dependencies:
我们还需要安装Webpack (带有其Babel loader扩展名)和http-server作为开发依赖项:
npm install --save-dev [email protected] [email protected] [email protected]
From now on, I am assuming you have a basic knowledge of React and JSX and its component based approach. If not you can read an excellent article on React components or have a look at all the other React related articles on Scotch.io.
从现在开始,我假设您具有React和JSX及其基于组件的方法的基本知识。 如果不是这样,您可以阅读有关React组件的出色文章,或者在Scotch.io上查看所有其他与React相关的文章 。
Initially we will focus only on creating a functional "Single Page Application" (with only client side rendering). Later we will see how to improve it by adding universal rendering and routing.
最初,我们将只专注于创建功能强大的“单页应用程序”(仅客户端渲染)。 稍后,我们将看到如何通过添加通用渲染和路由来对其进行改进。
So the first thing we need is an HTML boilerplate to "host" our app that we will store in src/static/index.html
:
因此,我们需要做的第一件事是HTML样板来“托管”我们将存储在src/static/index.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 Reacttitle>
<link rel="stylesheet" href="/css/style.css">
head>
<body>
<div id="main">div>
<script src="/js/bundle.js">script>
body>
html>
Nothing special here. Only two main things to underline:
这里没什么特别的。 要强调的只有两件事:
src/static/css/
. 我们正在使用一个简单的“手工制作”样式表,您可能需要下载并将其保存在src/static/css/
。 /js/bundle.js
file that contains all our JavaScript frontend code. We will see later in the article how to generate it using Webpack and Babel, so you don't need to worry about it now. 我们还引用了/js/bundle.js
文件,其中包含我们所有JavaScript前端代码。 我们将在本文后面看到如何使用Webpack和Babel生成它,因此您现在不必担心它。 In a real world application we would probably use an API to obtain the data necessary for our application.
在实际的应用程序中,我们可能会使用API来获取应用程序所需的数据。
In this case we have a very small dataset with only 5 athletes and some related information, so we can keep things simple and embed the data into a JavaScript module. This way we can easily import the data into any other component or module synchronously, avoiding the added complexity and the pitfalls of managing asynchronous APIs in an Universal JavaScript project, which is not the goal of the article.
在这种情况下,我们有一个非常小的数据集,仅包含5名运动员和一些相关信息,因此我们可以简化事情,并将数据嵌入到JavaScript模块中。 这样,我们可以轻松地将数据同步导入任何其他组件或模块,从而避免了通用JavaScript项目中增加的复杂性和管理异步API的陷阱,这不是本文的目标。
Let's see how the module looks like:
让我们看一下模块的外观:
// 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;
For brevity the file here has been truncated, and we are displaying just the data of one of the five athletes. If you want to see the full code check it out on the official repository. You can download the file into src/data/athletes.js
.
为简洁起见,此处的文件已被截断,我们仅显示五名运动员之一的数据。 如果您想查看完整的代码, 请在官方存储库中查看 。 您可以将文件下载到src/data/athletes.js
。
Also notice that I am not reporting 'use strict';
here though it should be present in every JavaScript file we are going to create through the course of this tutorial.
另请注意,我没有报告'use strict';
尽管在本教程中我们将要创建的每个JavaScript文件中都应该包含它。
As you can see, the file contains an array of objects where every object represents an athlete containing some generic information like id
, name
and country
and another array of objects representing the medals
won by that athlete.
如您所见,该文件包含一个对象数组,每个对象代表一个运动员,其中包含一些通用信息(例如id
, name
和country
,另一个对象数组代表该medals
获得的medals
。
You might also want to grab all the image files from the repository and copy them under: src/static/img/
.
您可能还想从存储库中获取所有图像文件 ,并将它们复制到src/static/img/
。
We are going to organize the views of our application into several components:
我们将把应用程序的视图组织成几个组件:
AthletePreview
, Flag
, Medal
and AthletesMenu
. 一组用于构建视图的小型UI组件: AthletePreview
, Flag
, Medal
和AthletesMenu
。 Layout
component that is used as master component to define the generic appearence of the application (header, content and footer blocks). 用作主组件的Layout
组件,用于定义应用程序的一般外观(页眉,内容和页脚块)。 IndexPage
and AthletePage
. 代表主要部分两个主要组件: IndexPage
和AthletePage
。 NotFoundPage
我们将用作404页面的额外“页面”组件: NotFoundPage
AppRoutes
component that uses React Router to manage the routing between views. 使用React Router管理视图之间的路由的AppRoutes
组件。 The first component that we are going to build allows us to display a nice flag and, optionally, the name of the country that it represents:
我们将要构建的第一个组件使我们可以显示一个漂亮的标志,并可以选择显示它代表的国家的名称:
// 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>
);
}
}
As you might have noticed this component uses a small array of countries as data source. Again this makes sense only because we need a very small data set which, for the sake of this demo app, is not going to change. In a real application with a larger and more complex data set you might want to use an API or a different mechanism to connect the data to the component.
您可能已经注意到,该组件使用一小部分国家/地区作为数据源。 同样,这仅是有意义的,因为我们需要一个非常小的数据集,就此演示应用而言,该数据集将不会改变。 在具有更大和更复杂数据集的真实应用程序中,您可能需要使用API或其他机制将数据连接到组件。
In this component it's also important to notice that we are using two different props, code
and showName
. The first one is mandatory and must be passed to the component to select which flag will be shown among the ones supported. The showName
props is instead optional and if set to a truthy value the component will also display the name of the country just after the flag.
在此组件中,还要注意我们正在使用两个不同的道具code
和showName
,这一点也很重要。 第一个是强制性的,必须传递给组件以选择将在支持的标志中显示哪个标志。 相反, showName
道具是可选的,如果设置为真实值,该组件还将在标志后显示国家名称。
If you want to build a more refined reusable component for a real world app you might also want to add to it props validation and defaults, but we are going to skip this step here as this is not the goal for the app we want to build.
如果您想为现实世界的应用程序构建更精致的可重用组件,则可能还需要向其添加道具验证和默认值 ,但是我们将在此处跳过此步骤,因为这不是我们要构建的应用程序的目标。
The Medal
component is similar to the Flag
component. It receives some props that represent the data related to a medal: the type
(G
for gold, S
for silver and B
for bronze), the year
when it was won, the name of the event
and the city
where the tournament was hosted and the category
where the athlete who won the medal competed.
Medal
组件类似于Flag
组件。 它会收到一些代表奖牌数据的道具: type
( G
代表金牌, S
代表银牌, B
代表铜牌),获胜year
, event
名称以及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>
);
}
}
As for the previous component here we also use a small object to map the codes of the medal types to descriptive names.
至于前面的组件,我们还使用一个小对象将奖牌类型的代码映射到描述性名称。
In this section we are going to build the menu that is displayed on top of every athlete page to allow the user to easily switch to another athlete without going back to the index:
在本部分中,我们将构建显示在每个运动员页面顶部的菜单,以使用户可以轻松切换到其他运动员,而无需返回索引:
// src/components/AthletesMenu.js
import React from 'react';
import { Link } from 'react-router';
export default class AthletesMenu extends React.Component {
render() {
return (
<nav className="athletes-menu">
{this.props.athletes.map(menuAthlete => {
return <Link key={menuAthlete.id} to={`/athlete/${menuAthlete.id}`} activeClassName="active">
{menuAthlete.name}
Link>;
})}
nav>
);
}
}
The component is very simple, but there are some key points to underline:
该组件非常简单,但是需要强调一些要点:
athletes
prop. So from the outside, when we use the component in our layout, we will need to propagate the list of athletes available in the app directly into the component. 我们希望数据通过athletes
道具传递到组件中。 因此,从外部来看,当我们在布局中使用组件时,我们需要将应用中可用的运动员列表直接传播到组件中。 map
method to iterate over all the athletes and generate for every one of them a Link
. 我们使用map
方法遍历所有运动员,并为每个运动员生成一个Link
。 Link
is a special component provided by React Router to create links between views. Link
是React Router提供的一个特殊组件,用于在视图之间创建链接。 activeClassName
to use the class active
when the current route matches the path of the link. 最后,当当前路由与链接的路径匹配时,我们使用activeClassName
来使用active
类。 The AthletePreview
component is used in the index to display the pictures and the names of the athletes. Let's see its code:
索引中使用了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>
);
}
}
The code is quite simple. We expect to receive a number of props that describe the attributes of the athlete we want to display like id
, image
, name
and medals
. Note that again we are using the Link
component to create a link to the athlete page.
代码很简单。 我们期望收到许多道具,这些道具描述了我们想要显示的运动员的属性,例如id
, image
, name
和medals
。 请注意,我们再次使用Link
组件来创建指向运动员页面的链接。
Now that we built all our basic components let's move to creating those that give the visual structure to the application. The first one is the Layout
component, which has the only purpose of providing a display template to the whole application defining an header, a space for the main content and a footer:
现在,我们已经构建了所有基本组件,让我们开始创建那些为应用程序提供视觉结构的组件。 第一个是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>Reactstrong> and <strong>Expressstrong>.
p>
footer>
div>
);
}
}
The component is pretty simple and we should understand how it works just by looking at the code. There's though a very interesting prop that we are using here, the children
prop. This is a special property that React provides to every component and allows to nest components one inside another.
该组件非常简单,我们仅需看一下代码就应该了解它是如何工作的。 尽管我们在这里使用了一个非常有趣的道具,即children
道具。 这是React提供给每个组件的一种特殊属性,并允许将组件嵌套在另一个组件中。
We are going to see in the routing section how the React Router will make sure to nest the components into the Layout
component.
我们将在路由部分中看到React Router如何确保将组件嵌套到Layout
组件中。
This component constitutes the full index page and it contains some of the components we previously defined:
此组件构成完整的索引页,并且包含我们先前定义的一些组件:
// 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>
);
}
}
Note that in this component we are using the AthletePreview
component we created previously. Basically we are iterating over all the available athletes from our data module and creating an AthletePreview
component for each of them. The AthletePreview
component is data agnostic, so we need to pass all the information about the current athlete as props using the JSX spread operator ({...object}
).
请注意,在此组件中,我们使用的是先前创建的AthletePreview
组件。 基本上,我们从数据模块中迭代所有可用的运动员,并为每个运动员创建一个AthletePreview
组件。 AthletePreview
组件与数据无关,因此我们需要使用JSX传播运算符 ( {...object}
)将有关当前运动员的所有信息作为道具传递。
In a similar fashion we can define the AthletePage
component:
我们可以类似的方式定义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 athletes={athletes}/>
<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">Wikipediaa>).
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 indexLink>
div>
div>
);
}
}
By now, you must be able to understand most of the code shown here and how the other components are used to build this view. What might be important to underline is that this page component accepts from the outside only the id of the athlete, so we include the data module to be able to retrieve the related information. We do this at the beginning of the render
method using the function filter
on the data set. We are also considering the case where the received id does not exist in our data module, in this case we render NotFoundPage
, a component that we are going to create in the next section.
到目前为止,您必须能够理解此处显示的大多数代码,以及如何使用其他组件来构建此视图。 要强调的重要一点是,此页面组件仅从外部接受运动员的ID,因此我们包括数据模块以能够检索相关信息。 我们在render
方法的开始使用数据集上的函数filter
进行此操作。 我们还考虑了数据模块中不存在接收到的ID的情况,在这种情况下,我们渲染NotFoundPage
,这是我们将在下一部分中创建的组件。
One last important detail is that here we are accessing the id with this.props.params.id
(instead of simply this.props.id
): params
is a special object created by React Router when using a component from a Route
and it allows to propagate routing parameters into components. It will be easier to understand this concept when we will see how to setup the routing part of the application.
最后一个重要的细节是,这里我们使用this.props.params.id
(而不是this.props.id
)访问id: params
是React Router在使用Route
的组件时创建的特殊对象,它允许将路由参数传播到组件中。 当我们看到如何设置应用程序的路由部分时,更容易理解这个概念。
Now let's see the NotFoundPage
component, which acts as a template to generate the code of our 404 pages:
现在,让我们看一下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>404h1>
<h2>Page not found!h2>
<p>
<Link to="/">Go back to the main pageLink>
p>
div>
);
}
}
The last component we need to create is the AppRoutes
component which is the master component that renders all the other views using internally the React Router. This component will use the routes
module, so let's have a quick look at it first:
我们需要创建的最后一个组件是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;
In this file we are basically using the React Router Route
component to map a set of routes to the page components we defined before. Note how the routes are nested inside a main Route
component. Let's explain how this works:
在这个文件中,我们基本上是使用React Router Route
组件将一组路由映射到我们之前定义的页面组件。 注意路线如何嵌套在主要Route
组件内。 让我们解释一下它是如何工作的:
/
to the Layout
component. This allows us to use our custom layout in every section of our application. The components defined into the nested routes will be rendered inside the Layout
component in place of the this.props.children
property that we discussed before. 根路径将路径/
映射到Layout
组件。 这使我们可以在应用程序的每个部分中使用自定义布局。 定义到嵌套路由中的组件this.props.children
现在Layout
组件中,以代替我们之前讨论的this.props.children
属性。 IndexRoute
which is a special route used to define the component that will be rendered when we are viewing the index page of the parent route (/
in this case). We use our IndexPage
component as index route. 第一个子路径是IndexRoute
,它是一种特殊的路径,用于定义当我们查看父路径(在本例中为/
)的索引页面时将呈现的组件。 我们使用IndexPage
组件作为索引路由。 athlete/:id
is mapped to the AthletePage
. Note here that we are using a named parameter :id
. So this route will match all the paths with the prefix /athlete/
, the remaining part will be associated to the params id
and will be available inside the component in this.props.params.id
. athlete/:id
路径映射到AthletePage
。 请注意,这里我们使用的是命名参数:id
。 因此,此路由将使所有路径都带有前缀/athlete/
匹配,其余部分将与params id
关联,并将在this.props.params.id
的组件内部可用。 *
maps every other path to the NotFoundPage
component. This route must be defined as the last one. 最终, 所有匹配路由*
将所有其他路径映射到NotFoundPage
组件。 此路由必须定义为最后一条。 Let's see now how to use these routes with the React Router inside our AppRoutes
component:
现在让我们看看如何在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)}/>
);
}
}
Basically we only need to import the Router
component and add it inside our render
function. The router receives our routes mapping in the router
prop. We also configure the history
prop to specify that we want to use the HTML5 browser history for the routing (as an alternative you could also use hashHistory).
基本上,我们只需要导入Router
组件并将其添加到我们的render
函数中。 路由器在router
属性中接收我们的路由映射。 我们还配置了history
道具,以指定我们要使用HTML5浏览器历史记录进行路由(或者,您也可以使用hashHistory )。
Finally we also added an onUpdate
callback to reset the scrolling of the window to the top everytime a link is clicked.
最后,我们还添加了onUpdate
回调,以在每次单击链接时将窗口的滚动重置为顶部。
The last bit of code to complete our first version of the application is to define the JavaScript logic that initializes the whole app in the browser:
完成我们的第一个应用程序版本的最后代码是定义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'));
};
The only thing we do here is to import our master AppRoutes
component and render it using the ReactDOM.render
method. The React app will be living inside our #main
DOM element.
我们在这里所做的唯一一件事就是导入我们的主AppRoutes
组件,并使用ReactDOM.render
方法对其进行呈现。 React应用程序将存在于我们的#main
DOM元素中。
Before we are able to run our application we need to generate the bundle.js
file containing all our React components with Webpack. This file will be executed by the browser so Webpack will make sure to convert all the modules into code that can be executed in the most common browser environments. Webpack will convert ES2015 and React JSX syntax to equivalent ES5 syntax (using Babel), which can be executed practically by every browser. Furthermore we can use Webpack to apply a number of optimizations to the resulting code like combining all the scripts files into one file and minifying the resulting bundle.
在我们能够运行我们的应用程序之前,我们需要生成bundle.js
文件,其中包含bundle.js
的所有React组件。 该文件将由浏览器执行,因此Webpack将确保将所有模块转换为可以在最常见的浏览器环境中执行的代码。 Webpack会将ES2015和React JSX语法转换为等效的ES5语法 (使用Babel),每个浏览器实际上都可以执行。 此外,我们可以使用Webpack对生成的代码进行多种优化,例如将所有脚本文件组合到一个文件中并最小化生成的包。
Let's write our webpack configuration file:
让我们编写我们的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
})
]
};
In the first part of the configuration file we define what is the entry point and the output file. The entry point is the main JavaScript file that initializes the application. Webpack will recursively resolve all the included/imported resources to determine which files will go into the final bundle.
在配置文件的第一部分,我们定义什么是入口点和输出文件。 入口点是用于初始化应用程序的主JavaScript文件。 Webpack将递归解析所有包含/导入的资源,以确定哪些文件将进入最终捆绑包。
The module.loaders
section allows to specify transformations on specific files. Here we want to use Babel with the react
and es2015
presets to convert all the included JavaScript files to ES5 code.
module.loaders
部分允许在特定文件上指定转换。 在这里,我们想将Babel与react
和es2015
预设一起使用,以将所有包含JavaScript文件转换为ES5代码。
In the final section we use plugins
to declare and configure all the optimizations plugins we want to use:
在最后一节中,我们使用plugins
来声明和配置我们要使用的所有优化插件:
DefinePlugin
allows us to define the NODE_ENV
variable as a global variable in the bundling process as if it was defined in one of the scripts. Some modules (e.g. React) relies on it to enable or disable specific features for the current environment (production or development). DefinePlugin
允许我们在捆绑过程中将NODE_ENV
变量定义为全局变量,就像在脚本之一中定义它一样。 一些模块(例如React)依靠它来启用或禁用当前环境( 生产或开发 )的特定功能。 DedupePlugin
removes all the duplicated files (modules imported in more than one module). DedupePlugin
删除所有重复的文件(在多个模块中导入的模块)。 OccurenceOrderPlugin
helps in reducing the file size of the resulting bundle. OccurenceOrderPlugin
有助于减小结果包的文件大小。 UglifyJsPlugin
minifies and obfuscates the resulting bundle using UglifyJs. UglifyJsPlugin
使用UglifyJs UglifyJsPlugin
化和混淆生成的包。 Now we are ready to generate our bundle file, you just need to run:
现在我们准备生成我们的捆绑文件,您只需要运行:
NODE_ENV=production node_modules/.bin/webpack -p
(if you are on Windows you can use PowerShell and run set NODE_ENV=production | node_modules/.bin/webpack -p
. Thanks Miles Rausch for the suggestion)
(如果您在Windows上,则可以使用PowerShell并运行set NODE_ENV=production | node_modules/.bin/webpack -p
。感谢Miles Rausch的建议)
The NODE_ENV
environment variable and the -p
option are used to generate the bundle in production mode, which will apply a number of additional optimizations, for example removing all the debug code from the React library.
NODE_ENV
环境变量和-p
选项用于在生产模式下生成捆绑包,这将应用许多其他优化,例如从React库中删除所有调试代码。
If everything went fine you will now have your bundle file in src/static/js/bundle.js
.
如果一切顺利,您现在将在src/static/js/bundle.js
拥有捆绑文件。
We are finally ready to play with the first version of our app!
我们终于可以使用我们的应用程序的第一个版本了!
We don't have a Node.js web server yet, so for now we can just use the module http-server
(previously installed as development dependency) to run a simple static file web server:
我们还没有Node.js Web服务器,所以现在我们可以只使用模块http-server
(以前安装为开发依赖项)来运行一个简单的静态文件Web服务器:
node_modules/.bin/http-server src/static
And your app will be magically available on http://localhost:8080.
您的应用程序将在http:// localhost:8080上神奇地可用。
Ok, now take some time to play with it, click on all the links and explore all the sections.
好的,现在花一些时间来玩它,单击所有链接并浏览所有部分。
Does everything seem to work allright? Well, almost! There's just a little caveat... If you refresh the page in a section different from the index you will get a 404 error from the server.
一切似乎都正常吗? 好吧,差不多! 只是有一点警告...如果在不同于索引的部分刷新页面,则会从服务器收到404错误。
There are a number of ways to address this problem. In our case it will be solved as soon as we implement our universal routing and rendering solution, so let's move on to the next section.
有很多方法可以解决此问题。 在我们的情况下,将在实现通用的路由和渲染解决方案后立即解决该问题,因此让我们继续下一部分。
Ok, we are now ready to evolve our application to the next level and build the missing server side part.
好的,我们现在可以将应用程序升级到一个新的级别,并构建缺少的服务器端部分。
In order to have server side routing and rendering we will use Express with a relatively small server script that we will see in a moment.
为了进行服务器端路由和渲染,我们将Express与相对较小的服务器脚本结合使用,稍后我们将看到该脚本。
The rendering part will use an ejs template as replacement for our index.html
file that we will save in src/views/index.ejs
:
呈现部分将使用ejs模板替代我们将保存在src/views/index.ejs
index.html
文件:
Judo Heroes - A Universal JavaScript demo application with React
<%- markup -%>
The only difference with the original HTML file is that we are using the template variable <%- markup -%>
inside the #main
div in order to include the React markup into the server-generated HTML code.
与原始HTML文件的唯一区别是,我们在#main
div中使用模板变量<%- markup -%>
,以便将React标记包含在服务器生成HTML代码中。
Now we are ready to write our server application:
现在我们准备编写服务器应用程序:
// 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}]`);
});
The code is commented, so it shouldn't be hard to get a general understanding of what is going on here.
该代码带有注释,因此对这里发生的事情有一个大致的了解并不难。
The important part of the code here is the Express route defined with app.get('*', (req, res) => {...})
. This is an Express catch-all route that will intercept all the GET requests to every URL in the server. Inside this route, we take care of delegating the routing logic to the React Router match
function.
此处代码的重要部分是使用app.get('*', (req, res) => {...})
定义的Express路由。 这是Express 包罗万象的路由,它将拦截对服务器中每个URL的所有GET请求。 在此路由中,我们负责将路由逻辑委托给React Router match
功能。
ReactRouter.match
accepts two parameters: the first one is a configuration object and the second is a callback function. The configuration object must have two keys:
ReactRouter.match
接受两个参数:第一个是配置对象 ,第二个是回调函数 。 配置对象必须具有两个键:
routes
: used to pass the React Router routes configuration. Here, we are passing the exact same configuration that we used for the client-side rendering. routes
:用于传递React Router路由配置。 在这里,我们传递的是与客户端渲染所使用的配置完全相同的配置。 location
: This is used to specify the currently requested URL. location
:用于指定当前请求的URL。 The callback function is called at the end of the matching. It will receive three arguments, error
, redirectLocation
, and renderProps
, that we can use to determine what exactly the result of the match operation was.
匹配结束时将调用回调函数。 它将接收三个参数error
, redirectLocation
和renderProps
,我们可以使用它们来确定匹配操作的结果是什么。
We can have four different cases that we need to handle:
我们可以处理四种不同的情况:
renderProps
is an object that contains the data we need to use to render the component. The component we are rendering is RouterContext
(contained in the React Router module), which is responsible for rendering the full component tree using the values in renderProps
. 第三种情况是当我们匹配路线并且必须渲染关联的组件时。 在这种情况下,参数renderProps
是一个对象,其中包含我们需要用来渲染组件的数据。 我们正在渲染的组件是RouterContext
(包含在React Router模块中),它负责使用renderProps
的值渲染完整的组件树。 This is the core of our server- side routing mechanism and we use the ReactDOM.renderToString
function to be able to render the HTML code that represents the component associated to the currently matched route.
这是我们服务器端路由机制的核心,我们使用ReactDOM.renderToString
函数能够呈现表示与当前匹配的路由关联的组件HTML代码。
Finally, we inject the resulting HTML into the index.ejs
template we defined before to obtain the full HTML page that we send to the browser.
最后,我们将结果HTML注入到我们之前定义的index.ejs
模板中,以获得发送到浏览器的完整HTML页面。
Now we are ready to run our server.js
script, but because it's using the JSX syntax we cannot simply run it with the node
interpreter. We need to use babel-node
and the full command (from the root folder of our project) looks like this:
现在我们可以运行server.js
脚本了,但是因为它使用的是JSX语法,所以我们不能简单地使用node
解释器来运行它。 我们需要使用babel-node
,完整的命令(来自项目的根文件夹)如下所示:
NODE_ENV=production node_modules/.bin/babel-node --presets react,es2015 src/server.js
At this stage your app is available at http://localhost:3000 and, for the sake of this tutorial, it can be considered complete.
在此阶段,您的应用程序可在http:// localhost:3000上获得,就本教程而言,可以认为它是完整的。
Again feel free to check it out and try all the sections and links. You will notice that this time we can refresh every page and the server will be able to identify the current route and render the right page.
再次随意检查并尝试所有部分和链接。 您会注意到,这次我们可以刷新每个页面,服务器将能够识别当前路线并呈现正确的页面。
Small advice: don't forget to check out the 404 page by entering a random non-existing URL!
小建议:不要忘记输入随机不存在的URL来签出404页面!
HOORAY! This completes our tutorial! I'm really happy to know you got to the end, but you can make me even more happier if you post here in the comments some example Universal JavaScript apps that you built using this (or a similar) approach.
哇! 这样就完成了我们的教程! 我很高兴知道您已经走到了尽头,但是如果您在评论中发布一些使用此(或类似方法)构建的示例Universal JavaScript应用示例,则可以让我更加开心。
If you want to know more about Universal Javascript and improve your application even more (e.g. by adding Universal Data Retrival using REST APIs) I definitely recommend to read the chapter Universal JavaScript for Web Applications on my book Node.js Design Patterns:
如果您想进一步了解Universal Javascript并进一步改善您的应用程序(例如,通过使用REST API添加Universal Data Retrival ),我绝对建议您阅读Node.js Design Patterns一书中的Web应用程序通用JavaScript一章:
Until next time!
直到下一次!
PS: Huge thanks to Mario Casciaro for reviewing this article and to Marwen Trabelsi for the support on improving the code! Also thanks to @CriticalMAS for finding a typo :P
PS: 非常感谢 Mario Casciaro审阅了本文,并感谢 Marwen Trabelsi对改进代码的支持! 也感谢@CriticalMAS查找拼写错误:P
翻译自: https://scotch.io/tutorials/react-on-the-server-for-beginners-build-a-universal-react-and-node-app
react node服务器