四、添加 React 前端来完成 MERN

没有前端,web 应用是不完整的。它是用户与之交互的部分,对任何 web 体验都至关重要。在本章中,我们将使用 React 将交互式用户界面添加到在上一章中开始构建的 MERN skeleton 应用后端实现的基本用户和身份验证功能中。

我们将介绍以下主题,以添加工作前端并完成 MERN skeleton 应用:

  • 骨架的前端特征
  • 使用 React、React 路由和物料 UI 设置开发
  • 后端用户 API 集成
  • 身份验证集成
  • 主页、用户、注册、登录、用户配置文件、编辑和删除视图
  • 导航菜单
  • 基本服务器端渲染

骨架前端

为了全面实现第 3 章功能分解部分中讨论的框架应用功能,使用 MongoDB、Express 和 Node构建后端,我们将向基础应用添加以下用户界面组件:

  • 主页:在根 URL 处呈现的视图,用于欢迎用户访问 web 应用
  • 用户列表页面:获取并显示数据库中所有用户列表的视图,还可以链接到各个用户配置文件
  • 注册页面:带有用户注册表单的视图,允许新用户创建用户帐户,并在成功创建后将其重定向到登录页面
  • 登录页面:一个带有登录表单的视图,允许现有用户登录,以便访问受保护的视图和操作
  • 个人资料页面:一个获取和显示个人用户信息的组件,只有登录用户才能访问,还包含编辑和删除选项,只有登录用户查看自己的个人资料时才可见
  • 编辑个人资料页面:一种表单,用于获取表单中的用户信息,允许用户编辑信息,并且只有登录用户试图编辑自己的个人资料时才可访问
  • 删除用户组件:允许登录用户在确认其意图后只删除自己的个人资料的选项
  • 菜单导航栏:向用户列出所有可用和相关视图的组件,也有助于指示用户在应用中的当前位置

以下 React 组件树图显示了我们将开发的所有 React 组件,以构建此基础应用的视图:

MainRouter将是根 React 组件,其中包含应用中所有其他自定义 React 视图。HomeSignupSigninUsersProfileEditProfile将在使用 React Router 声明的各个路由上呈现,菜单组件将呈现所有这些视图,DeleteUser将是剖面视图的一部分。

The code discussed in this chapter, and for the complete skeleton, is available on GitHub in the repository at github.com/shamahoque/mern-skeleton. You can clone this code and run the application as you go through the code explanations in the rest of this chapter. 

文件夹和文件结构

以下文件夹结构显示了要添加到骨架中的新文件夹和文件,以使用 React 前端完成骨架:

| mern_skeleton/
   | -- client/
      | --- img/
         | ---- images/
      | --- auth/
         | ---- api-auth.js
         | ---- auth-helper.js
         | ---- PrivateRoute.js
         | ---- Signin.js
      | --- core/
         | ---- Home.js
         | ---- Menu.js
      | --- user/
         | ---- api-user.js
         | ---- DeleteUser.js
         | ---- EditProfile.js
         | ---- Profile.js
         | ---- Signup.js
         | ---- Users.js
      | --- App.js
      | --- main.js
      | --- MainRouter.js
  | -- server/
      | --- devBundle.js
  | -- webpack.config.client.js
  | -- webpack.config.client.production.js

客户端文件夹将包含 React 组件、帮助程序和前端资产,如图像和 CSS。除了此文件夹和用于编译和绑定客户端代码的 Webpack 配置之外,我们还将修改其他一些现有文件以集成完整的框架。

为 React 开发设置

在我们可以在现有的框架代码库中开始使用 React 进行开发之前,我们首先需要添加配置来编译和绑定前端代码,添加构建交互界面所需的 React 相关依赖项,并将其连接到 MERN 开发流程中。

配置 Babel 和 Webpack

为了在开发过程中编译和绑定客户端代码以运行它,并将其绑定到生产环境中,我们将更新 Babel 和 Webpack 的配置。

巴别塔

要编译 React,首先将 Babel React 预置模块作为开发依赖项安装:

npm install babel-preset-react --save-dev

然后,更新.babelrc以包含该模块,并根据react-hot-loader模块的需要配置react-hot-loader巴别塔插件。

mern-skeleton/.babelrc

{
    "presets": [
      "env",
      "stage-2",
      "react"
    ],
    "plugins": [
 "react-hot-loader/babel"
 ]
}

网页包

要在使用 Babel 编译后捆绑客户端代码,并启用react-hot-loader以加快开发,请安装以下模块:

npm install --save-dev webpack-dev-middleware webpack-hot-middleware file-loader
npm install --save react-hot-loader

然后,为了为前端开发配置 Webpack 并构建生产包,我们将添加一个webpack.config.client.js文件和一个webpack.config.client.production.js文件,其配置代码与第 2 章准备开发环境中描述的相同。

加载 Web 包中间件进行开发

在开发过程中,当我们运行服务器时,Express app 应根据客户端代码的配置集加载与前端相关的 Webpack 中间件,以便集成前端和后端开发工作流。为了实现这一点,我们将使用第 2 章中讨论的devBundle.js文件准备开发环境来建立一个compile方法,将 Express app 配置为使用 Webpack 中间件。server文件夹中的devBundle.js如下所示。

mern-skeleton/server/devBundle.js

import config from './../config/config'
import webpack from 'webpack'
import webpackMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import webpackConfig from './../webpack.config.client.js'

const compile = (app) => {
  if(config.env === "development"){
    const compiler = webpack(webpackConfig)
    const middleware = webpackMiddleware(compiler, {
      publicPath: webpackConfig.output.publicPath
    })
    app.use(middleware)
    app.use(webpackHotMiddleware(compiler))
  }
}

export default {
  compile
}

然后,在express.js中导入并调用此compile方法,只在开发时添加以下突出显示的行。

mern-skeleton/server/express.js

import devBundle from './devBundle'
const app = express()
devBundle.compile(app)

这两行突出显示的代码仅用于开发模式,在构建用于生产的代码时应注释掉。当 Express app 在开发模式下运行时,此代码将在启动 Webpack 编译和捆绑客户端代码之前导入中间件和 Webpack 配置。捆绑的代码将放在dist文件夹中

使用 Express 服务静态文件

为了确保 Express server 正确处理对静态文件(如 CSS 文件、图像或捆绑客户端 JS)的请求,我们将通过在express.js中添加以下配置,将其配置为服务dist文件夹中的静态文件。

mern-skeleton/server/express.js

import path from 'path'
const CURRENT_WORKING_DIR = process.cwd()
app.use('/dist', express.static(path.join(CURRENT_WORKING_DIR, 'dist')))

更新模板以加载捆绑脚本

为了在 HTML 视图中添加捆绑的前端代码,我们将更新template.js文件,将脚本文件从dist文件夹添加到<body>标记的末尾。

mern-skeleton/template.js

...
<body>
    <div id="root"></div>
    <script type="text/javascript" src="/dist/bundle.js"></script>
</body>

添加 React 依赖项

前端视图将主要使用 React 实现。此外,为了实现客户端路由,我们将使用 React Router,为了增强用户体验,我们将使用 Material UI。

反应

在本书中,我们将使用 React 16 对前端进行编码。要开始编写React组件代码,我们需要安装以下模块作为常规依赖项:

npm install --save react react-dom

反应路由

React Router 提供一组导航组件,用于在前端为 React 应用进行路由。为了利用声明式路由和可书签的 URL 路由,我们将添加以下 React 路由模块:

npm install --save react-router react-router-dom

材料界面

为了使我们的 MERN 应用中的 UI 保持光滑,而不必过多地钻研 UI 设计和实现,我们将利用Material-UI库。它提供现成的、可定制的React组件,实现谷歌的材料设计。要开始使用 Material UI 组件制作前端,我们需要安装以下模块:

npm install --save material-ui@1.0.0-beta.43 material-ui-icons

At the time of writing, the latest pre-release version of Material-UI is 1.0.0-beta.43 and it is recommended to install this exact version in order to ensure the code for the example projects do not break.

要按照 Material UI 的建议添加Roboto字体,并使用Material-UI图标,我们将在 HTML 文档的<head>部分的template.js文件中添加相关的样式链接:

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

开发配置都设置好了,必要的 React 模块添加到代码库中,我们现在可以开始实现定制的 React 组件了。

实现 React 视图

功能性前端应将 React 组件与后端 API 集成,并允许用户基于授权在应用内无缝导航。为了演示如何实现此 MERN 框架的功能性前端视图,我们将首先详细介绍如何在根路径上呈现主页组件,然后介绍后端 API 和用户身份验证集成,然后重点介绍实现其余视图组件的独特方面。

呈现主页

在根路由上实现和呈现一个工作的Home组件的过程也将公开框架中前端代码的基本结构。我们将从顶层入口组件开始,该组件包含整个 React 应用,并呈现连接应用中所有 React 组件的主路由组件。

main.js 的入口点

客户端文件夹中的client/main.js文件将是呈现完整 React 应用的入口点。在这段代码中,我们导入将包含完整前端的根或顶级 React 组件,并将其呈现给具有在template.js中的 HTML 文档中指定的 ID'root'div元素。

mern-skeleton/client/main.js

import React from 'react'
import { render } from 'react-dom'
import App from './App'

render(<App/>, document.getElementById('root'))

根反应组分

包含应用前端所有组件的顶级 React 组件在client/App.js文件中定义。在该文件中,我们配置 React 应用,以使用自定义材质 UI 主题呈现视图组件,启用前端路由,并确保 React Hot Loader 可以在开发组件时立即加载更改。

自定义材质 UI 主题

可以使用MuiThemeProvider组件,通过在createMuiTheme()中为主题变量配置自定义值,轻松定制物料 UI 主题。

mern-skeleton/client/App.js

import {MuiThemeProvider, createMuiTheme} from 'material-ui/styles'
import {indigo, pink} from 'material-ui/colors'

const theme = createMuiTheme({
  palette: {
    primary: {
    light: '#757de8',
    main: '#3f51b5',
    dark: '#002984',
    contrastText: '#fff',
  },
  secondary: {
    light: '#ff79b0',
    main: '#ff4081',
    dark: '#c60055',
    contrastText: '#000',
  },
    openTitle: indigo['400'],
    protectedTitle: pink['400'],
    type: 'light'
  }
})

对于骨架,我们只通过设置一些要在 UI 中使用的颜色值来应用最小的定制。这里生成的主题变量将传递给我们构建的所有组件,并在其中可用。

使用 MUI 主题和 BrowserRouter 包装根组件

我们为组成用户界面而创建的自定义 React 组件将通过MainRouter组件中指定的前端路由进行访问。基本上,该组件包含为应用开发的所有自定义视图。在App.js中定义根组件时,我们用MuiThemeProvider包装MainRouter组件,使其能够访问物料 UI 主题,BrowserRouter使用 React Router 启用前端路由。前面定义的自定义主题变量作为道具传递给MuiThemeProvider,使主题在所有自定义组件中可用。

mern-skeleton/client/App.js

import React from 'react'
import MainRouter from './MainRouter'
import {BrowserRouter} from 'react-router-dom'

const App = () => (
  <BrowserRouter>
    <MuiThemeProvider theme={theme}>
      <MainRouter/>
    </MuiThemeProvider>
  </BrowserRouter>
)

将根组件标记为热导出

App.js中导出App组件的最后一行代码使用react-hot-loader中的hot模块将根组件标记为hot。这将允许在开发过程中实时重新加载 React 组件。

mern-skeleton/client/App.js

import { hot } from 'react-hot-loader'
...
export default hot(module)(App)

对于我们的 MERN 应用,在这一点之后,我们不必对main.jsApp.js代码进行太多更改,我们可以通过在MainRouter组件中注入新组件继续构建 React 应用的其余部分。

向主路由添加主路由

MainRouter.js代码将有助于根据应用中的路由或位置呈现我们的自定义 React 组件。在第一个版本中,我们将只添加根路由来呈现Home组件。

mern-skeleton/client/MainRouter.js

import React, {Component} from 'react'
import {Route, Switch} from 'react-router-dom'
import Home from './core/Home'
class MainRouter extends Component {
  render() {
    return (<div>
      <Switch>
        <Route exact path="/" component={Home}/>
      </Switch>
    </div>)
  }
}
export default MainRouter

随着我们开发更多的视图组件,我们将更新MainRouter,为Switch组件中的新组件添加路由。

The Switch component in React Router renders a route exclusively. In other words, it only renders the first child that matches the requested route path. Whereas, without being nested in a Switch, every Route component renders inclusively when there is a path match. For example, a request at '/' also matches a route at '/contact'.

主分量

当用户访问根路径时,Home组件将在浏览器上呈现,我们将使用材质 UI 组件组合它。以下屏幕截图显示了Home组件和Menu组件,这两个组件将在本章后面作为单个组件实现,以提供应用的导航:

将在浏览器中呈现供用户交互的Home组件和其他视图组件将遵循一个通用代码结构,该代码结构按给定顺序包含以下部分。

进口

组件文件将根据特定组件的要求从代码中导入 React、materialui、React 路由模块、图像、CSS、API fetch 和 auth helpers。例如,对于Home.js中的Home组件代码,我们使用以下导入。

mern-skeleton/client/core/Home.js

import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {withStyles} from 'material-ui/styles'
import Card, {CardContent, CardMedia} from 'material-ui/Card'
import Typography from 'material-ui/Typography'
import seashellImg from './../iimg/seashell.jpg'

图像文件保存在client/iimg/文件夹中,并导入/添加到Home组件中。

样式声明

导入之后,我们将根据需要使用Material-UI主题变量定义 CSS 样式,以设置组件中元素的样式。对于Home.js中的Home组件,我们有以下样式。

mern-skeleton/client/core/Home.js

const styles = theme => ({
  card: {
    maxWidth: 600,
    margin: 'auto',
    marginTop: theme.spacing.unit * 5
  },
  title: {
    padding:`${theme.spacing.unit * 3}px ${theme.spacing.unit * 2.5}px 
    ${theme.spacing.unit * 2}px`,
    color: theme.palette.text.secondary
  },
  media: {
    minHeight: 330
  }
}) 

此处定义的 JSS 样式对象将被注入到组件中,并用于为组件中的元素设置样式,如下面的Home组件定义所示。

Material-UI uses JSS, which is a CSS-in-JS styling solution to add styles to the components. JSS uses JavaScript as a language to describe styles. This book will not cover CSS and styling implementations in detail. It will most rely on the default look and feel of Material-UI components. To learn more about JSS, visit http://cssinjs.org/?v=v9.8.1. For examples of how to customize the Material-UI component styles, check out the Material-UI documentation at https://material-ui-next.com/.

组件定义

在组件定义中,我们将组合组件的内容和行为。Home组件将包含一个带有标题、图像和标题的材料 UICard,所有这些都使用前面定义的类进行样式化,并作为道具传入。

mern-skeleton/client/core/Home.js

class Home extends Component {
  render() {
    const {classes} = this.props 
    return (
      <div>
        <Card className={classes.card}>
          <Typography type="headline" component="h2" className=
          {classes.title}>
            Home Page
          </Typography>
          <CardMedia className={classes.media} image={seashellImg} 
          title="Unicorn Shells"/>
          <CardContent>
            <Typography type="body1" component="p">
              Welcome to the Mern Skeleton home page
            </Typography>
          </CardContent>
        </Card>
      </div>
    )
  }
}

属性类型验证

为了验证作为组件支柱的样式声明所需的注入,我们将PropTypes需求验证器添加到定义的组件中。

mern-skeleton/client/core/Home.js

Home.propTypes = {
  classes: PropTypes.object.isRequired
}

导出组件

最后,在组件文件的最后一行代码中,我们将导出使用Material-UI中的withStyles传入的定义样式的组件。这样使用withStyles创建一个高阶组件HOC,该组件可以访问定义的样式对象作为道具。

mern-skeleton/client/core/Home.js

export default withStyles(styles)(Home)

导出的组件现在可以用于其他组件内的合成,就像我们在前面讨论的MainRouter组件中的路径中使用这个Home组件一样。

要在我们的 MERN 应用中实现的其他视图组件将遵循相同的结构。在本书的其余部分中,我们将主要关注组件定义,重点介绍所实现组件的独特方面。

捆绑图像资产

我们导入到Home组件视图中的静态图像文件也必须与其他编译的 JS 代码一起包含在包中,以便代码可以访问和加载它。为了实现这一点,我们需要更新 Webpack 配置文件,以添加一个模块规则,将图像文件加载、捆绑并发送到输出目录,该目录包含已编译的前端和后端代码。

使用babel-loader后更新webpack.config.client.jswebpack.config.server.jswebpack.config.client.production.js文件,增加以下模块规则:

[ …
    {
       test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/,
       use: 'file-loader'
    }
]

此模块规则使用 Webpack 的file-loadernpm 模块,该模块需要作为开发依赖项安装,如下所示:

npm install --save-dev file-loader

在浏览器中运行和打开

到目前为止,可以运行客户端代码在根 URL 处查看浏览器中的Home组件。要运行应用,请使用以下命令:

npm run development

然后,在浏览器中打开根 URL(http://localhost:3000,查看Home组件。

这里开发的Home组件是一个基本的视图组件,没有交互功能,不需要为用户 CRUD 或 auth 使用后端 API。但是,框架前端的其余视图组件将需要后端 API 和身份验证。

后端 API 集成

用户应该能够使用前端视图根据身份验证和授权获取和修改数据库中的用户数据。为了实现这些功能,React 组件将使用 fetchapi 访问后端公开的 API 端点。

The Fetch API is a newer standard to make network requests similar to XMLHttpRequest (XHR) but using promises instead, enabling a simpler and cleaner API. To learn more about the Fetch API, visit https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API.

获取用户 CRUD

client/user/api-user.js文件中,我们将添加访问每个用户 CRUD API 端点的方法,React 组件可以根据需要使用这些方法与服务器和数据库交换用户数据。

创建用户

create方法将从视图组件中获取用户数据,使用fetch进行POST调用,在后端创建新用户,最后将服务器的响应作为承诺返回给组件。

mern-skeleton/client/user/api-user.js

const create = (user) => {
  return fetch('/api/users/', {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(user)
    })
    .then((response) => {
      return response.json()
    }).catch((err) => console.log(err))
}

列出用户

list方法将使用 fetch 进行GET调用,以检索数据库中的所有用户,然后将服务器的响应作为承诺返回给组件。

mern-skeleton/client/user/api-user.js

const list = () => {
  return fetch('/api/users/', {
    method: 'GET',
  }).then(response => {
    return response.json()
  }).catch((err) => console.log(err))
}

读取用户配置文件

read方法将使用 fetch 进行GET调用,以按 ID 检索特定用户。由于这是受保护的路由,除了将用户 ID 作为参数传递外,请求组件还必须提供有效凭据,在这种情况下,该凭据将是成功登录后接收到的有效 JWT。

mern-skeleton/client/user/api-user.js

const read = (params, credentials) => {
  return fetch('/api/users/' + params.userId, {
    method: 'GET',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    }
  }).then((response) => {
    return response.json()
  }).catch((err) => console.log(err))
}

JWT 使用Bearer方案连接到Authorization报头中的GETfetch 调用,然后服务器的响应以承诺的形式返回给组件。

更新用户的数据

update方法将从特定用户的视图组件中获取已更改的用户数据,然后使用fetch进行PUT调用以更新后端的现有用户。这也是一个受保护的路由,需要有效的 JWT 作为凭据。

mern-skeleton/client/user/api-user.js

const update = (params, credentials, user) => {
  return fetch('/api/users/' + params.userId, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    },
    body: JSON.stringify(user)
  }).then((response) => {
    return response.json()
  }).catch((err) => {
    console.log(err)
  })
}

删除用户

remove方法将允许视图组件从数据库中删除特定用户,使用 fetch 进行DELETE调用。这也是一个受保护的路由,需要有效的 JWT 作为凭证,类似于readupdate方法。服务器对删除请求的响应将作为承诺返回给组件。

mern-skeleton/client/user/api-user.js

const remove = (params, credentials) => {
  return fetch('/api/users/' + params.userId, {
    method: 'DELETE',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    }
  }).then((response) => {
    return response.json()
  }).catch((err) => {
    console.log(err)
  }) 
}

最后,根据需要导出 React 组件要导入和使用的用户 API 帮助器方法。

mern-skeleton/client/user/api-user.js

export { create, list, read, update, remove }

获取身份验证 API

为了将来自服务器的 auth API 端点与前端 React 组件集成,我们将在client/auth/api-auth.js文件中添加获取登录和注销 API 端点的方法。

登录

signin方法将从视图组件获取用户登录数据,然后使用fetch进行POST调用,用后端验证用户。来自服务器的响应将返回到 promise 中的组件,如果登录成功,该组件可能包含 JWT。

mern-skeleton/client/user/api-auth.js

const signin = (user) => {
  return fetch('/auth/signin/', {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      credentials: 'include',
      body: JSON.stringify(user)
    })
    .then((response) => {
      return response.json()
    }).catch((err) => console.log(err))
}

注销

signout方法将使用 fetch 对服务器上的注销 API 端点进行 GET 调用。

mern-skeleton/client/user/api-auth.js

const signout = () => {
  return fetch('/auth/signout/', {
    method: 'GET',
  }).then(response => {
      return response.json()
  }).catch((err) => console.log(err))
}

api-auth.js文件的末尾,导出signinsignout方法。

mern-skeleton/client/user/api-auth.js

export { signin, signout }

使用这些 API 获取方法,React 前端可以完全访问后端中可用的端点。

在前端进行身份验证

如前一章所述,使用 JWT 实现身份验证将把管理和存储用户身份验证状态的责任移交给客户端。为此,我们需要编写代码,允许客户端在成功登录时存储从服务器接收的 JWT,在访问受保护路由时使其可用,在用户注销时删除或使令牌无效,并根据用户身份验证状态限制对前端视图和组件的访问。

使用 React Router 文档中的身份验证工作流示例,我们将编写帮助器方法来管理组件间的身份验证状态,并使用自定义PrivateRoute组件将受保护的路由添加到前端。

管理身份验证状态

client/auth/auth-helper.js中,我们将定义以下助手方法来存储和检索客户端sessionStorage中的 JWT 凭证,并在用户注销时清除sessionStorage

  • authenticate(jwt, cb):成功登录时保存凭证:
authenticate(jwt, cb) {
    if(typeof window !== "undefined")
        sessionStorage.setItem('jwt', JSON.stringify(jwt))
    cb()
}
  • isAuthenticated():如果已登录,则检索凭据:
isAuthenticated() {
    if (typeof window == "undefined")
      return false

    if (sessionStorage.getItem('jwt'))
      return JSON.parse(sessionStorage.getItem('jwt'))
    else
      return false
}
  • signout(cb):删除凭证并注销:
signout(cb) {
      if(typeof window !== "undefined")
        sessionStorage.removeItem('jwt')
      cb()
      signout().then((data) => {
          document.cookie = "t=; expires=Thu, 01 Jan 1970 00:00:00 
          UTC; path=/;"
      })
}

使用此处定义的方法,我们构建的 React 组件将能够检查和管理用户身份验证状态,以限制前端的访问,如以下自定义PrivateRoute所示。

专用路由组件

client/auth/PrivateRoute.js定义了PrivateRoute组件,如中的验证流示例所示 https://reacttraining.com/react-router/web/example/auth-workflow 在 React 路由文档中。它将允许我们为前端声明受保护的路由,以限制基于用户身份的视图访问。

mern-skeleton/client/auth/PrivateRoute.js

import React, { Component } from 'react'
import { Route, Redirect } from 'react-router-dom'
import auth from './auth-helper'

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route {...rest} render={props => (
    auth.isAuthenticated() ? (
      <Component {...props}/>
    ) : (
      <Redirect to={{
        pathname: '/signin',
        state: { from: props.location }
      }}/>
    )
  )}/>
)

export default PrivateRoute

PrivateRoute中要呈现的组件只有在用户经过身份验证后才会加载,否则用户将被重定向到Signin组件。

集成了后端 API 和组件中可以使用的身份验证管理助手方法后,我们可以开始构建其余的视图组件。

用户和身份验证组件

本节中描述的 React 组件通过允许用户查看、创建和修改数据库中存储的与身份验证限制相关的用户数据,完成了为骨架定义的交互功能。对于以下每个组件,我们将在MainRouter中介绍每个组件的独特方面,以及如何将组件添加到应用中。

用户组件

client/user/Users.js中的Users组件显示从数据库中获取的所有用户的名称,并将每个名称链接到用户配置文件。此组件可供应用的任何访问者查看,并将在路径'/users'处呈现:

在组件定义中,我们首先使用空的用户数组初始化状态。

mern-skeleton/client/user/Users.js

class Users extends Component {
  state = { users: [] }
...

接下来,在componentDidMount中,我们使用api-user.js助手方法中的list方法,从后端获取用户列表,并通过更新状态将用户数据加载到组件中。

mern-skeleton/client/user/Users.js

  componentDidMount = () => {
    list().then((data) => {
      if (data.error)
        console.log(data.error)
      else
        this.setState({users: data})
    })
  }

render功能包含Users组件的实际查看内容,由PaperListListItems等物料 UI 组件组成。元素的样式由 CSS 定义并作为道具传入。

mern-skeleton/client/user/Users.js

render() {
    const {classes} = this.props
    return (
      <Paper className={classes.root} elevation={4}>
        <Typography type="title" className={classes.title}>
          All Users
        </Typography>
        <List dense>
          {this.state.users.map(function(item, i) {
              return <Link to={"/user/" + item._id} key={i}>
                <ListItem button="button">
                  <ListItemAvatar>
                    <Avatar>
                      <Person/>
                    </Avatar>
                  </ListItemAvatar>
                  <ListItemText primary={item.name}/>
                  <ListItemSecondaryAction>
                    <IconButton>
                      <ArrowForward/>
                    </IconButton>
                  </ListItemSecondaryAction>
                </ListItem>
              </Link>
            })}
        </List>
      </Paper>
    )
  }

为了生成每个列表项,我们使用 map 函数遍历状态中的用户数组。

要将这个Users组件添加到 React 应用中,我们需要用一个Route来更新MainRouter组件,该Route将这个组件呈现在'/users'路径上。在Home路径后的Switch组件中添加Route

mern-skeleton/client/MainRouter.js

<Route path="/users" component={Users}/>

要查看浏览器中呈现的此视图,可以临时在Home组件中添加Link组件,以路由到Users组件:

<Link to="/users">Users</Link>

注册组件

client/user/Signup.js中的Signup组件向用户提供一个包含姓名、电子邮件和密码字段的表单,供用户在'/signup'路径注册:

在组件定义中,我们首先使用空的输入字段值、空的错误消息初始化状态,然后将 dialog open 变量设置为 false。

mern-skeleton/client/user/Signup.js

  constructor() {
    state = { name: '', password: '', email: '', open: false, error: '' }
  ...

我们还定义了当输入值更改或单击 submit 按钮时要调用的两个处理程序函数。handleChange函数接受输入字段中输入的新值,并将其设置为state

mern-skeleton/client/user/Signup.js

handleChange = name => event => {
    this.setState({[name]: event.target.value})
}

提交表单时调用clickSubmit函数。它从 state 获取输入值,并调用createfetch 方法向后端注册用户。然后,根据服务器的响应,显示错误消息或成功对话框。

mern-skeleton/client/user/Signup.js

  clickSubmit = () => {
    const user = {
      name: this.state.name || undefined,
      email: this.state.email || undefined,
      password: this.state.password || undefined
    } 
    create(user).then((data) => {
      if (data.error)
        this.setState({error: data.error})
      else
        this.setState({error: '', open: true})
    })
  }

render函数中,我们使用 Material UI 中的TextField等组件在注册视图中组合表单组件并设置其样式。

mern-skeleton/client/user/Signup.js

  render() {
    const {classes} = this.props
    return (<div>
      <Card className={classes.card}>
        <CardContent>
          <Typography type="headline" component="h2" 
                      className={classes.title}>
            Sign Up
          </Typography>
          <TextField id="name" label="Name" 
          className={classes.textField} 
                     value={this.state.name} 
                     onChange={this.handleChange('name')} 
                     margin="normal"/> <br/>
          <TextField id="email" type="email" label="Email" 
                     className={classes.textField} value=
                     {this.state.email} 
                     onChange={this.handleChange('email')}
                     margin="normal"/><br/>
          <TextField id="password" type="password"
          label="Password" className={classes.textField} 
                     value={this.state.password} 
                     onChange={this.handleChange('password')} 
                     margin="normal"/><br/> 
          {this.state.error && ( <Typography component="p" 
           color="error">
              <Icon color="error" 
              className={classes.error}>error</Icon>
              {this.state.error}</Typography>)}
        </CardContent>
        <CardActions>
          <Button color="primary" raised="raised"
                  onClick={this.clickSubmit} 
           className={classes.submit}>Submit</Button>
        </CardActions>
      </Card>
      <Dialog> ... </Dialog>
    </div>)
  }

呈现还包含一个错误消息块和一个Dialog组件,该组件根据服务器的注册响应有条件地呈现。Signup.js中的Dialog组件组成如下。

mern-skeleton/client/user/Signup.js

<Dialog open={this.state.open} disableBackdropClick={true}>
   <DialogTitle>New Account</DialogTitle>
   <DialogContent>
      <DialogContentText>
         New account successfully created.
      </DialogContentText>
   </DialogContent>
   <DialogActions>
      <Link to="/signin">
         <Button color="primary" autoFocus="autoFocus" variant="raised">
            Sign In
         </Button>
      </Link>
   </DialogActions>
</Dialog>

成功创建帐户后,用户将得到确认,并要求用户使用此Dialog组件登录,该组件链接到Signin组件:

要将Signup组件添加到应用中,请将以下Route添加到Switch组件中的MainRouter中。

mern-skeleton/client/MainRouter.js

<Route path="/signup" component={Signup}/>

这将在'/signup'处呈现Signup视图。

符号成分

client/auth/Signin.js中的Signin组件也是一个表单,只有用于登录的电子邮件和密码字段。该组件与Signup组件非常相似,将在'/signin'路径上呈现。关键区别在于成功登录并存储接收到的 JWT 后实现重定向:

对于重定向,我们将使用 React Router 的Redirect组件。首先,使用其他字段将状态中的redirectToReferrer值初始化为false

mern-skeleton/client/auth/Signin.js

class Signin extends Component {
  state = { email: '', password: '', error: '', redirectToReferrer: false } 
...

当用户提交表单后成功登录且接收到的 JWT 存储在sessionStorage中时,应将redirectToReferrer设置为true。为了存储 JWT 和重定向后缀,我们将调用auth-helper.js中定义的authenticate()方法。此代码将进入clickSubmit()函数,在表单提交时调用。

mern-skeleton/client/auth/Signin.js

clickSubmit = () => {
    const user = {
      email: this.state.email || undefined,
      password: this.state.password || undefined
    }
    signin(user).then((data) => {
      if (data.error) {
        this.setState({error: data.error})
      } else {
        auth.authenticate(data, () => {
 this.setState({redirectToReferrer: true})
 })
      }
    })
}

重定向将基于render函数中的Redirect组件的redirectToReferrer值有条件地发生。在返回之前在 render 函数中添加重定向代码,如下所示:

mern-skeleton/client/auth/Signin.js

render() {
    const {classes} = this.props
    const {from} = this.props.location.state || {
 from: {pathname: '/' }
 } 
 const {redirectToReferrer} = this.state
 if (redirectToReferrer)
 return (<Redirect to={from}/>)
    return (...)
  }
}

如果呈现Redirect组件,则会将应用带到最后一个位置或根位置的Home组件

返回将包含类似于Signup的表单元素,只有emailpassword字段、条件错误消息和submit按钮。

要将Signin组件添加到应用中,请将以下路由添加到Switch组件中的MainRouter

mern-skeleton/client/MainRouter.js

<Route path="/signin" component={Signin}/>

这将在"/signin"处呈现Signin组件。

轮廓组件

client/user/Profile.js中的Profile组件在'/user/:userId'路径的视图中显示单个用户的信息,userId参数表示特定用户的 ID:

只有在用户登录的情况下,才能从服务器获取此配置文件信息,为了验证这一点,组件必须向read获取调用提供 JWT,否则,应将用户重定向到登录视图。

Profile组件定义中,我们首先需要使用空用户初始化状态,并将redirectToSignin设置为false

mern-skeleton/client/user/Profile.js

class Profile extends Component {
  constructor({match}) {
    super()
    this.state = { user: '', redirectToSignin: false }
    this.match = match 
  } ...

我们还需要访问Route组件传递的 match 道具,该道具将包含:userId参数值,并且在组件安装时可以作为this.match.params.userId访问。

Profile组件应获取用户信息,并在userId参数在路由中发生变化时呈现。但是,当应用从一个纵断面图转到另一个纵断面图,并且只是路由路径中的参数更改时,React 组件不会重新装载。而是通过componentWillReceiveProps中的新道具。为了确保组件在路由参数更新时加载相关的用户信息,我们将在init()函数中放置readfetch 调用,然后在componentDidMountcomponentWillReceiveProps中都可以调用该函数。

mern-skeleton/client/user/Profile.js

init = (userId) => {
    const jwt = auth.isAuthenticated()
    read({
      userId: userId
    }, {t: jwt.token}).then((data) => {
      if (data.error)
        this.setState({redirectToSignin: true})
      else
        this.setState({user: data})
    })
}

init(userId)函数取userId值,并调用读取用户获取方法。由于此方法还需要凭据来授权登录用户,因此使用auth-helper.js中的isAuthenticated方法从sessionStorage检索 JWT。服务器响应后,状态将使用用户信息更新,或者视图将重定向到登录视图。

componentDidMountcomponentWillReceiveProps中调用此init函数,并将相关的userId值作为参数传入,以便在组件中获取并加载正确的用户信息。

mern-skeleton/client/user/Profile.js

componentDidMount = () => {
  this.init(this.match.params.userId)
}
componentWillReceiveProps = (props) => {
  this.init(props.match.params.userId)
}

render函数中,我们设置条件重定向到 Signin 视图,并返回Profile视图的内容:

mern-skeleton/client/user/Profile.js

render() {
   const {classes} = this.props
   const redirectToSignin = this.state.redirectToSignin
   if (redirectToSignin)
     return <Redirect to='/signin'/>
   return (...)
 }

如果当前登录的用户正在查看其他用户的个人资料,render函数将返回包含以下元素的Profile视图。

mern-skeleton/client/user/Profile.js

<div>
  <Paper className={classes.root} elevation={4}>
    <Typography type="title" className={classes.title}> Profile </Typography>
      <List dense>
        <ListItem>
          <ListItemAvatar>
             <Avatar>
               <Person/>
             </Avatar>
          </ListItemAvatar>
          <ListItemText primary={this.state.user.name} 
                       secondary={this.state.user.email}/>
        </ListItem>
        <Divider/>
        <ListItem>
          <ListItemText primary={"Joined: " + 
              (new Date(this.state.user.created)).toDateString()}/>
        </ListItem>
      </List>
  </Paper>
</div>

但是,如果当前登录的用户正在查看自己的个人资料,他们将能够在Profile组件中看到编辑和删除选项,如以下屏幕截图所示:

为了实现此功能,在Profile中的第一个ListItem组件中,添加一个包含Edit按钮的ListItemSecondaryAction组件和一个DeleteUser组件,该组件将根据当前用户是否正在查看自己的个人资料进行有条件的渲染。

mern-skeleton/client/user/Profile.js

{ auth.isAuthenticated().user && auth.isAuthenticated().user._id == this.state.user._id &&
    (<ListItemSecondaryAction>
       <Link to={"/user/edit/" + this.state.user._id}>
         <IconButton color="primary">
           <Edit/>
         </IconButton>
       </Link>
       <DeleteUser userId={this.state.user._id}/>
    </ListItemSecondaryAction>)}

Edit按钮将路由到EditProfile组件,这里使用的自定义DeleteUser组件将处理删除操作,并将userId作为道具传递给它。

要将Profile组件添加到应用中,请将Route添加到Switch组件中的MainRouter

mern-skeleton/client/MainRouter.js

<Route path="/user/:userId" component={Profile}/>

编辑配置文件组件

client/user/EditProfile.js中的EditProfile组件在实现上与SignupProfile组件都有相似之处。它将允许授权用户以类似于注册表格的形式编辑自己的个人资料信息:

当在'/user/edit/:userId'加载时,组件将在验证 JWT 是否为 auth 后获取 ID 为的用户信息,然后使用接收到的用户信息加载表单。表单将允许用户编辑并仅向update获取调用提交更改的信息,并且在成功更新后,将用户重定向到具有更新信息的Profile视图。

EditProfile将以与Profile组件中相同的方式加载用户信息,方法是使用this.match.params中的userId参数和auth.isAuthenticated中的凭证在componentDidMount中使用read进行获取。表单视图将具有与Signup组件相同的元素,输入值在更改状态下更新。

在表单提交时,组件将使用userId、JWT 和更新的用户数据调用update获取方法。

mern-skeleton/client/user/EditProfile.js

clickSubmit = () => {
    const jwt = auth.isAuthenticated()
    const user = {
      name: this.state.name || undefined,
      email: this.state.email || undefined,
      password: this.state.password || undefined
    }
    update({
      userId: this.match.params.userId
    }, {
      t: jwt.token
    }, user).then((data) => {
      if (data.error) {
        this.setState({error: data.error})
      } else {
        this.setState({'userId': data._id, 'redirectToProfile': true})
      }
    })
}

根据服务器的响应,用户将看到错误消息或被重定向到更新的配置文件页面,其中呈现函数中包含以下Redirect组件。

mern-skeleton/client/user/EditProfile.js

if (this.state.redirectToProfile)
   return (<Redirect to={'/user/' + this.state.userId}/>)

要将EditProfile组件添加到应用中,我们这次将使用PrivateRoute来限制在用户未登录的情况下加载组件。MainRouter中的放置顺序也很重要。

mern-skeleton/client/MainRouter.js

<Switch>
  ... <PrivateRoute path="/user/edit/:userId" component={EditProfile}/><>
  <Route path="/user/:userId" component={Profile}/>
</Switch>

路径为'/user/edit/:userId'的路由需要放在路径为'/user/:userId'的路由之前,这样,当请求该路由时,编辑路径首先在交换机组件中唯一匹配,而不会与Profile路由混淆

删除用户组件

client/user/DeleteUser.js中的DeleteUser组件基本上是一个我们将添加到纵断面图中的按钮,点击后会打开一个Dialog组件,要求用户确认delete动作:

组件首先初始化状态,Dialog组件的open设置为false,而redirect也设置为false,因此不会首先渲染。

mern-skeleton/client/user/DeleteUser.js

class DeleteUser extends Component {
  state = { redirect: false, open: false } 
...

接下来,我们需要处理程序方法来打开和关闭dialog按钮。当用户点击delete按钮时,对话框打开。

mern-skeleton/client/user/DeleteUser.js

clickButton = () => {
    this.setState({open: true})
}

当用户点击对话框cancel时,对话框关闭。

mern-skeleton/client/user/DeleteUser.js

  handleRequestClose = () => {
    this.setState({open: false})
  }

当用户在对话框中确认delete动作后,组件将有权访问Profile组件作为道具传入的userId,该道具需要与 JWT 一起调用remove获取方法。

mern-skeleton/client/user/DeleteUser.js

deleteAccount = () => {
    const jwt = auth.isAuthenticated() 
    remove({
      userId: this.props.userId
    }, {t: jwt.token}).then((data) => {
      if (data.error) {
        console.log(data.error)
      } else {
        auth.signout(() => console.log('deleted'))
 this.setState({redirect: true})
      }
    }) 
  }

确认后,deleteAccount函数调用remove获取方法,其中userId来自 props,JWT 来自isAuthenticated。在服务器中成功删除后,用户将注销并重定向到主视图。

渲染函数包含条件Redirect到主视图,并返回DeleteUser组件元素、一个DeleteIcon按钮和确认Dialog

mern-skeleton/client/user/DeleteUser.js

render() {
    const redirect = this.state.redirect
    if (redirect) {
      return <Redirect to='/'/>
    }
    return (<span>
      <IconButton aria-label="Delete" onClick={this.clickButton} 
      color="secondary">
        <DeleteIcon/>
      </IconButton>
      <Dialog open={this.state.open} onClose={this.handleRequestClose}>
        <DialogTitle>{"Delete Account"}</DialogTitle>
        <DialogContent>
          <DialogContentText>
            Confirm to delete your account.
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={this.handleRequestClose} color="primary">
            Cancel
          </Button>
          <Button onClick={this.deleteAccount} color="secondary" 
          autoFocus="autoFocus">
            Confirm
          </Button>
        </DialogActions>
      </Dialog>
    </span>)
}

DeleteUseruserId作为在delete获取调用中使用的道具,因此我们为所需道具userId添加了propType检查。

mern-skeleton/client/user/DeleteUser.js

DeleteUser.propTypes = {
  userId: PropTypes.string.isRequired
}

当我们在Profile组件中使用DeleteUser组件时,当MainRouter中添加Profile时,它会被添加到应用视图中。

菜单组件

Menu组件将通过提供指向所有可用视图的链接,在前端应用中充当导航栏,并指示应用中的当前位置。

为了实现这些导航栏功能,我们将使用 React 路由的 HOCwithRouter来访问历史对象的属性。Menu组件中的以下代码仅添加标题、Home图标链接到根路由以及Users按钮链接到'/users'路由。

mern-skeleton/client/core/Menu.js

const Menu = withRouter(({history}) => (<div>
  <AppBar position="static">
    <Toolbar>
      <Typography type="title" color="inherit">
        MERN Skeleton
      </Typography>
      <Link to="/">
        <IconButton aria-label="Home" style={isActive(history, "/")}>
          <HomeIcon/>
        </IconButton>
      </Link>
      <Link to="/users">
        <Button style={isActive(history, "/users")}>Users</Button>
      </Link>
    </Toolbar>
  </AppBar>
</div>))

为了在Menu上指示应用的当前位置,我们将通过有条件地更改颜色来突出显示与当前位置路径匹配的链接。

mern-skeleton/client/core/Menu.js

const isActive = (history, path) => {
  if (history.location.pathname == path)
    return {color: '#ff4081'}
  else
    return {color: '#ffffff'}
}

isActive功能用于给Menu中的按钮上色,如下所示:

style={isActive(history, "/users")}

其余链接,如登录、注册、我的个人资料和注销,将根据用户是否登录显示在Menu上:

例如,只有当用户未登录时,登录和登录的链接才应显示在菜单上。所以我们需要在Users按钮后面添加一个条件,将其添加到Menu组件中。

mern-skeleton/client/core/Menu.js

{!auth.isAuthenticated() && (<span>
    <Link to="/signup">
       <Button style={isActive(history, "/signup")}> Sign Up </Button>
    </Link>
    <Link to="/signin">
       <Button style={isActive(history, "/signin")}> Sign In </Button>
    </Link>
</span>)}

类似地,MY PROFILE的链接和SIGN OUT按钮只应在用户登录时显示在菜单上,并应通过此条件检查添加到Menu组件中。

mern-skeleton/client/core/Menu.js

{auth.isAuthenticated() && (<span>
   <Link to={"/user/" + auth.isAuthenticated().user._id}>
      <Button style={isActive(history, "/user/" + auth.isAuthenticated().user._id)}>
           My Profile 
      </Button>
   </Link>
   <Button color="inherit" 
           onClick={() => { auth.signout(() => history.push('/')) }}>
        Sign out
   </Button>
 </span>)}

MY PROFILE按钮使用登录用户的信息链接到用户自己的个人资料,SIGN OUT按钮在点击时调用auth.signout()方法。用户登录后,菜单将如下所示:

要使Menu导航栏出现在所有视图中,我们需要将其添加到MainRouter中,然后再添加到所有其他路线中,并添加到Switch组件之外。

mern-skeleton/client/MainRouter.js

 <Menu/>
    <Switch>
    …
    </Switch>

这将使Menu组件在路径上访问时呈现在所有其他组件之上。

框架前端包含所有必要的组件,使用户能够在后端注册、查看和修改用户数据,同时考虑身份验证和授权限制。但是,仍然无法在浏览器地址栏中直接访问前端管线,只能在从前端视图中链接时访问。要在骨架应用中启用此功能,我们需要实现基本的服务器端渲染。

基本服务器端渲染

当前,如果在浏览器地址栏中直接输入 React 路由路由或路径名,或者刷新不在根路径上的视图,则 URL 不起作用。这是因为服务器无法识别 React 路由路由。我们必须在后端实现基本的服务器端渲染,以便服务器能够在接收到前端路由请求时做出响应。

为了在服务器接收到对前端路由的请求时正确地呈现相关的 React 组件,我们需要在服务器端呈现 React 路由和 materialui 组件的 React 组件。

React 应用服务器端呈现的基本思想是使用react-dom中的renderToString方法将根 React 组件转换为标记字符串,并将其附加到服务器收到请求时呈现的模板。

express.js中,我们将用代码替换响应'/'GET请求返回template.js的代码,该代码在接收到任何传入 GET 请求时,生成相关 React 组件的服务器端呈现标记,并将该标记添加到模板中。该代码将具有以下结构:

app.get('*', (req, res) => {
     // 1\. Prepare Material-UI styles
     // 2\. Generate markup with renderToString
     // 3\. Return template with markup and CSS styles in the response
})

服务器端渲染模块

为了实现基本的服务器端渲染,我们需要将以下 React、React 路由和 materialui 特定模块导入服务器代码。在我们的代码结构中,这些模块将被导入到server/express.js中:

  • 反应模块:需要渲染反应组件并使用renderToString
import React from 'react'
import ReactDOMServer from 'react-dom/server'
  • 路由模块StaticRouter是一个无状态路由,它使用请求的 URL 匹配前端路由和MainRouter组件,后者是我们前端的根组件:
import StaticRouter from 'react-router-dom/StaticRouter'
import MainRouter from './../client/MainRouter'
  • 材质 UI 模块:以下模块将根据前端使用的材质 UI 主题,帮助生成前端组件的 CSS 样式:
import { SheetsRegistry } from 'react-jss/lib/jss'
import JssProvider from 'react-jss/lib/JssProvider'
import { MuiThemeProvider, createMuiTheme, createGenerateClassName } from 'material-ui/styles'
import { indigo, pink } from 'material-ui/colors'

使用这些模块,我们可以准备、生成和返回服务器端呈现的前端代码。

为 SSR 准备材料 UI 样式

当服务器接收到任何请求时,在使用包含 React 视图的生成标记进行响应之前,我们需要准备 CSS 样式,这些样式也应该添加到标记中,以便 UI 在初始渲染时不会中断。

mern-skeleton/server/express.js

const sheetsRegistry = new SheetsRegistry()
const theme = createMuiTheme({
    palette: {
      primary: {
      light: '#757de8',
      main: '#3f51b5',
      dark: '#002984',
      contrastText: '#fff',
    },
    secondary: {
      light: '#ff79b0',
      main: '#ff4081',
      dark: '#c60055',
      contrastText: '#000',
    },
      openTitle: indigo['400'],
      protectedTitle: pink['400'],
      type: 'light'
    },
})
const generateClassName = createGenerateClassName()

为了注入材料 UI 样式,在每个请求中,我们首先生成一个新的SheetsRegistry和 MUI 主题实例,匹配前端代码中使用的内容。

生成标记

使用renderToString的目的是生成 React 组件的 HTML 字符串版本,该版本将显示给用户,以响应请求的 URL:

mern-skeleton/server/express.js

const context = {} 
const markup = ReactDOMServer.renderToString(
      <StaticRouter location={req.url} context={context}>
        <JssProvider registry={sheetsRegistry} generateClassName=
          {generateClassName}>
          <MuiThemeProvider theme={theme} sheetsManager={new Map()}>
            <MainRouter/>
          </MuiThemeProvider>
        </JssProvider>
      </StaticRouter>
) 

客户端应用的根组件MainRouter用材质 UI 主题和 JS 包装,以提供MainRouter子组件所需的样式道具。这里使用无状态的StaticRouter代替客户端使用的BrowserRouter,包装MainRouter并提供用于实现客户端组件的路由道具。基于这些值,renderToString将返回包含相关视图的标记,例如请求的location路线和作为道具传递给包装组件的主题。

发送带有标记和 CSS 的模板

生成标记后,我们首先检查组件中是否有一个redirect呈现,以便在标记中发送。如果没有重定向,那么我们将从sheetsRegistry生成 CSS 字符串,并在响应中发送带有标记和 CSS 注入的模板

mern-skeleton/server/express.js

if (context.url) {
   return res.redirect(303, context.url)
}
const css = sheetsRegistry.toString()
res.status(200).send(Template({
   markup: markup,
   css: css
}))

组件中呈现重定向的一个示例是,尝试通过服务器端呈现访问PrivateRoute时。由于服务器端无法从客户端sessionStorage访问 auth 令牌,PrivateRoute中的重定向将呈现。在本例中,context.url将具有'/signin'路由,因此它将重定向到'/signin'路由,而不是尝试呈现PrivateRoute组件。

更新 template.js

服务器上生成的标记和 CSS 必须添加到template.jsHTML 代码中,如下所示,以便在服务器呈现模板时加载

mern-skeleton/template.js

export default ({markup, css}) => {
    return `...
           <div id="root">${markup}</div>
           <style id="jss-server-side">${css}</style> 
           ...`
}

更新主路由

一旦服务器端呈现的代码到达浏览器,前端脚本接管,我们需要在主组件挂载时删除服务器端注入的 CSS。这将返回对向客户端呈现 React 应用的完全控制:

mern-skeleton/client/MainRouter.js

componentDidMount() {
   const jssStyles = document.getElementById('jss-server-side')
   if (jssStyles && jssStyles.parentNode)
      jssStyles.parentNode.removeChild(jssStyles)
}

使用水合物代替渲染

现在 React 组件将在服务器端呈现,我们可以将main.js代码更新为使用ReactDOM.hydrate()而不是ReactDOM.render()

import React from 'react'
import { hydrate } from 'react-dom'
import App from './App'

hydrate(<App/>, document.getElementById('root'))

hydrate函数对已经有ReactDOMServer呈现的 HTML 内容的容器进行水合物化处理。这意味着服务器呈现的标记将被保留,并且当 React 在浏览器中接管时,仅附加事件处理程序,从而使初始加载性能更好。

实现了基本的服务器端渲染后,服务器现在可以正确处理从浏览器地址栏到前端路由的直接请求,从而可以将 React 前端视图添加到书签中。

这里开发的框架 MERN 应用现在是一个功能完整的 MERN web 应用,具有基本的用户功能。我们可以扩展此框架中的代码,为不同的应用添加各种功能

总结

在本章中,我们通过添加一个工作的 React 前端来完成 MERN skeleton 应用,包括前端路由和 React 视图的基本服务器端呈现。

我们首先更新了开发流程,以包含 React 视图的客户端代码绑定。我们更新了 Webpack 和 Babel 的配置以编译 React 代码,并讨论了如何从 Express app 加载已配置的 Webpack 中间件,以便在开发过程中从一个位置启动服务器端和客户端代码编译。

随着开发流程的更新,在构建前端之前,我们添加了相关的 React 依赖项以及 React Router for frontend routing 和 Material UI,以使用骨架应用用户界面中的现有组件

然后,我们实现了顶级根 React 组件,并集成了 React 路由,使我们能够添加用于导航的客户端路由。使用这些路由,我们加载了使用 Material UI 组件开发的定制 React 组件,以构成骨架应用的用户界面。

为了使这些 React 视图动态且与从后端获取的数据交互,我们使用 Fetch API 连接到后端用户 API。然后,我们在前端视图上加入了身份验证和授权,使用sessionStorage存储用户特定的详细信息和成功登录时从服务器获取的 JWT,并使用PrivateRoute组件限制对某些视图的访问

最后,我们修改了服务器代码以实现基本的服务器端呈现,允许在服务器识别到传入的请求实际上是针对 React 路由之后,使用服务器端呈现的标记直接在浏览器中加载前端路由

在下一章中,我们将使用在开发这个基本的 MERN 应用时学到的概念,并扩展框架应用代码以构建一个功能齐全的社交媒体应用。