Skip to content

Latest commit

 

History

History
617 lines (470 loc) · 23.8 KB

File metadata and controls

617 lines (470 loc) · 23.8 KB

六、在服务器端应用中使用StaticRouter

**服务器端渲染SSR是一种仅在服务器端渲染客户端单页应用SPA)并发送完全渲染的页面作为对用户请求的响应的技术。在客户端 SPA 中,JavaScript 包作为脚本标记包含,并且最初,页面中不呈现任何内容。首先下载包,然后通过执行包中的代码填充 DOM 节点。在连接不良的情况下,这有两个缺点,下载捆绑包可能需要更多的时间,并且不执行 JavaScript 的爬虫程序将无法看到任何内容,从而影响页面的 SEO。**

**SSR 通过加载 HTML、CSS 和 JavaScript 响应用户的请求来解决这些问题;内容在服务器上呈现,最终的 HTML 被提供给爬虫程序。React 应用可以使用 Node.js 在服务器上呈现,React Router 中可用的组件可以用于定义应用中的路由。

在本章中,我们将了解如何在服务器端呈现的 React 应用中使用 React 路由组件:

  • 使用 Node.js 和 Express.js 执行 React 应用的 SSR
  • 添加<StaticRouter>组件并创建路由
  • 理解<StaticRouter>道具
  • 通过在服务器上呈现第一个页面,然后允许客户端代码接管后续页面的呈现,创建同构的 React 应用

使用 Node.js 和 Express.js 执行 React 应用的 SSR

在本例中,我们将使用 Node.js 和 Express.js 创建一个服务器端应用,该应用将在服务器上呈现 React 应用。js 是服务器和应用的跨平台 JavaScript 运行时环境。它基于 Google 的 V8 JavaScript 引擎构建,并使用事件驱动、非阻塞 I/O 模型,这使得它高效且轻量级。Express.js 是 Node.js 环境中使用的最流行的路由和中间件 web 框架模块之一。它允许您创建中间件,帮助处理来自客户端的 HTTP 请求。

安装依赖项

让我们首先使用npm init命令创建一个服务器端应用:

npm init -y

这将创建一个文件package.json,其中包含各个字段的默认值。下一步是添加依赖项:

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

前面的命令将把所有必要的库添加到 package.json 文件的dependencies列表中。请注意,我们不是使用create-react-appCLI 创建 React 应用;相反,我们将添加所需的依赖项,并编写用于构建应用的配置文件

要构建应用,将以下开发依赖项添加到devDependencies列表中:

npm install --save-dev webpack webpack-cli nodemon-webpack-plugin webpack-node-externals babel-core babel-loader babel-preset-env babel-preset-react 

前面的命令将为package.json文件中的devDependencies列表添加构建应用所需的库。

下一步是编写构建配置,以便构建服务器端应用。

网页包生成配置

这来自 Webpack 的文档:

At its core, W****ebPack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

Webpack 已经成为为 JavaScript 应用创建捆绑包的事实标准。create-react-appCLI 包括内部使用webpack为开发和生产环境创建捆绑包的脚本。

创建一个名为webpack-server.config.babel.js的文件,包括以下配置:

import path from 'path';
import webpack from 'webpack';
import nodemonPlugin from 'nodemon-webpack-plugin';
import nodeExternals from 'webpack-node-externals';

export default {
    entry: './src/server/index.js',
    target: 'node',
    externals: [nodeExternals()],
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'server.js',
        publicPath: '/'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: 'babel-loader'
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            __isBrowser__: false
        }),
        new nodemonPlugin()
    ]
}

从前面的配置中,提到文件index.js(在./src/server路径处)作为入口点,并且生成的输出文件server.js被复制到dist目录。webpack插件babel-loader用于使用BabelWebpack在应用中传输 JavaScript 文件。nodemon-webpack-plugin用于运行nodemon实用程序,该实用程序将监视应用中 JavaScript 文件的更改,并在webpack以监视模式运行时重新加载和构建应用。

下一步是创建一个.babelrc文件,该文件将列出构建应用所需的预设:

{
 "presets": ["env","react"]
}

babel-preset-envbabel-preset-react插件用于传输 ES6,并将代码向下转换为 ES5。最后一步,在package.json文件中添加脚本命令,使用webpack-server.config.babel.js文件中提到的配置启动应用:

"scripts": {
    "start": "webpack --config webpack-server.config.babel.js --watch --mode development"
}

命令npm start将构建应用,并将侦听应用中 JavaScript 文件中的更改,并在检测到更改时重建。

服务器端应用

webpack配置中所述,应用的入口点位于/src/server/index.js。让我们在此路径创建index.js文件,并包含以下代码,该代码在给定端口启动服务器应用:

import express from 'express';

const PORT = process.env.PORT || 3001;

const app = express();

app.get('*', (req, res) => {
    res.send(`
        <!DOCTYPE HTML>
 <html>
 <head>
 <title>React SSR example</title>
 </head>
 <body>
 <main id='app'>Rendered on the server side</main>
 </body>
 </html>
 `);
});

app.listen(PORT, () => {
    console.log(`SSR React Router app running at ${PORT}`);
});

当您运行npm start命令并访问 URLhttp://localhost:3001处的应用时,将呈现前面的 HTML 内容。这确保了webpack配置构建应用并在端口3001处运行前面的服务器端代码,nodemon监视文件中的更改。

使用 ReactDOMServer.renderToString 呈现 React 应用

要在服务器端呈现 React 应用,首先创建 React 组件文件-shared/App.js

import React, { Component } from 'react';

export class App extends Component {
    render() {
        return (
            <div>Inside React App (rendered with SSR)</div>
        );
    }
}

然后,在server/index.js文件中呈现前面的组件:

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { App } from '../shared/App';

app.get('*', (req, res) => {

    const reactMarkup = ReactDOMServer.renderToString(<App />);

    res.send(`
        <!DOCTYPE HTML>
        <html>
        ...
            <main id='app'>${reactMarkup}</main>   
        ...
        </html>
    `);
});

ReactDOMServer类包括在服务器端 Node.js 应用中呈现 React 组件的各种方法。ReactDOMServer类中的renderToString方法在服务器端呈现 React 组件并返回生成的标记。然后,可以将生成的标记字符串包含在发送给用户的响应中。

当您访问http://localhost:3001页面时,您会注意到 React 应用内的消息(用 SSR 呈现)显示出来

要确认内容确实在服务器端呈现,可以右键单击页面并从上下文菜单中选择查看页面源选项。页面源显示在新选项卡中,其中包括以下内容:

<main id='app'>
 <div data-reactroot="">
        Inside React App (rendered with SSR) </div>
</main>

当爬虫程序访问应用时,前面的内容很有用。通过在服务器端呈现 React 组件,将填充标记并将其作为来自服务器的响应包含。这些内容随后被搜索引擎的爬虫编入索引,帮助应用的 SEO 方面。

添加并创建路由

<StaticRouter>组件是react-router-dom包的一部分(使用react-router中的<StaticRouter>定义),用于呈现服务器端的 React 路由组件。<StaticRouter>组件与其他路由组件类似,因为它只接受一个子组件 React 应用的根组件(<App />。此组件应在无状态应用中使用,在该应用中,用户不会四处单击以导航到页面的不同部分。

让我们通过包装应用的根组件来包含<StaticRouter>组件:

import { StaticRouter } from 'react-router-dom';

app.get('*', (req, res) => {
    const context = {};
    const reactMarkup = ReactDOMServer.renderToString(
        <StaticRouter context={context} location={req.url}>
            <App />
        </StaticRouter>
    );

    res.send(`
        ...
        <main id='app'>${reactMarkup}</main>
        ...
    `);
});

请注意,<StaticRouter>组件接受两个支柱—contextlocationcontext对象是空对象,当<App />中的<Route>组件之一由于浏览器的位置匹配而呈现时,会填充属性。

location对象通常是请求的 URL,中间件功能可以使用此信息。请求对象(req包含指定请求 URL 的url属性。

让我们在App.js中包含两个<Route>组件:

export class App extends Component { 
    render() {
        return (
            <div>
                Inside React App (rendered with SSR)
                <Route
                    exact
 path='/'
                    render={() => <div>Inside Route at path '/'</div>}
                />
 <Route
                    path='/home'
                    render={() => 
                       <div>Inside Home Route at path '/home'</div>
                    }
 />
            </div>
        );
    }
}

<Route>组件与<StaticRouter>组件的location属性中指定的请求 URL 匹配并呈现。

使用和 staticContext 进行服务器端重定向

在前面的示例中,让我们使用<Redirect>组件将用户从/路径重定向到/home路径:

<Route
    path="/"
    render={() => <Redirect to="/home" />}
    exact
/>

当您尝试访问 URLhttp://localhost:3001/时,您会注意到重定向没有发生,浏览器的 URL 也没有更新。在客户端环境中,前面的重定向已经足够了。但是,在服务器端环境中,服务器负责处理重定向。在这种情况下,<StaticRouter>组件中提到的context对象填充了必要的细节:

{
    "action": "REPLACE",
    "location": {
        "pathname": "/home",
        "search": "",
        "hash": "",
        "state": undefined
    },
    "url": "/home"
}

context对象包含组件渲染的结果。当组件仅呈现内容时,它通常是一个空对象。但是,当渲染组件重定向到其他路径时,它将填充前面的详细信息。请注意,url属性指定了用户应该重定向到'/home'路径的路径。

可以添加检查,查看context对象中是否存在url属性,然后使用response对象上的redirect方法重定向用户:

...
const reactMarkup = ReactDOMServer.renderToString(
    <StaticRouter context={context} location={req.url}>
        <App />
    </StaticRouter>
);

if (context.url) {
    res.redirect(301, 'http://' + req.headers.host + context.url);
} else {
    res.send(`
        <!DOCTYPE HTML>
        <html>
            ...
        </html>
    `);
}

response对象中的redirect方法用于执行服务器端重定向,并提及要重定向到的状态代码和 URL。

通过在渲染组件中使用staticContext属性,也可以使用更多属性填充context对象:

<Route
    path="/"
    exact
    render={({ staticContext, }) => {
        if (staticContext) {
            staticContext.status = 301;
        }
        return (
            <Redirect to="/home" />
        )
    }}
/>

这里,staticContext道具在渲染组件中可用,并且在使用<Redirect>组件重定向用户之前,将status属性添加到该道具中。然后,status属性在context对象中可用:

res.redirect(context.status, 'http://' + req.headers.host + context.url);

这里,context对象中的status属性用于设置使用redirect方法重定向用户时的 HTTP 状态。

请求与匹配路径匹配的 URL

在服务器端呈现 React 应用时,了解请求的 URL 是否与应用中的任何现有路由匹配也很有帮助。只有当路由可用时,才应在服务器端呈现相应的组件。然而,如果路由不可用,则应向用户显示未找到页面(404)。react-router包中的matchPath功能允许您将请求的 URL 与包含路由匹配属性的对象进行匹配,例如pathexactstrictsensitive

import { matchPath } from 'react-router'

app.use('*', (req, res) => {
    const isRouteAvailable = matchPath(req.url, {
path: '/dashboard/',
 strict: true
 });
    ...

});

matchPath函数类似于库如何根据请求的 URL 路径匹配<Route>组件。传递给matchPath函数的第一个参数是请求的 URL,第二个参数是请求的 URL 应与之匹配的对象。当路由匹配时,matchPath函数返回一个对象,详细说明请求的 URL 如何匹配该对象。

例如,如果请求的 URL 为/dashboard/,则matchPath函数返回以下对象:

{
    path: '/dashboard/',
    url: '/dashboard/',
    isExact: true,
    params: {}
}

这里,path属性提到用于匹配请求 URL 的路径模式,url属性提到 URL 的匹配部分,isExact布尔属性设置为true,如果请求的 URL 和路径完全匹配,params属性列出与提供的路径名匹配的参数。请考虑下面的示例,其中提到路径中的参数:

const matchedObject = matchPath(req.url, '/github/:githubID');

这里,不是将对象指定为第二个参数,而是指定一个路径字符串。如果要将路径与请求的 URL 相匹配,并使用exactstrictsensitive属性的默认值,则此简短符号非常有用。匹配的对象将返回以下内容:

{
    path: '/github/:githubID',
    url: '/github/sagar.ganatra',
    isExact: true,
    params: { githubID: 'sagar.ganatra' } 
}

请注意,params属性现在填充了path中提到的参数列表,以及请求的 URL 中提供的值。

在服务器端,在初始化<StaticRouter>并呈现 React 应用之前,可以执行检查,以查看请求的 URL 是否与对象集合中定义的任何路由匹配。例如,考虑路由对象的集合。

shared/routes.js中,我们有以下内容:

export const ROUTES = [
    {
        path: '/',
        exact: true
    },
    {
        path: '/dashboard/',
        strict: true
    },
    {
        path: '/github/:githubId'
    }
];

前面的数组包含路由对象,可在matchPath中使用这些对象检查请求的 URL 是否与前面列表中的任何路由匹配:

app.get('*', (req, res) => {
    const isRouteAvailable = ROUTES.find(route => {
        return matchPath(req.url, route);
    })
    ...
});

如果找到请求的 URL,则isRouteAvailalbe将是ROUTES列表中匹配的对象,否则当路由对象均不匹配请求的 URL 时,将其设置为undefined。在后一种情况下,可以将未找到的页面标记发送给用户:

if (!isRouteAvailable) {
    res.status(404);
    res.send(`
        <!DOCTYPE HTML>
        <html>
            <head><title>React SSR example</title></head>
            <body>
                <main id='app'>
                Requested page '${req.url}' not found
                </main>
            </body>
        </html>`);
    res.end();
}

当用户请求路径时,比如说/user,在ROUTES中提到的对象都不匹配,并且发送前面的响应,提到404HTTP 状态,响应体提到没有找到请求的路径/user

静态路由上下文属性

<StaticRouter>组件接受道具basenamelocationcontext。与其他路由实现类似,<StaticRouter>中的basename属性用于指定baseURL位置,location中的basename属性用于指定位置属性-pathnamehashsearchstate

context道具仅在<StaticRouter>实现中使用,它包含组件渲染的结果。如前所述,context对象可以填充 HTTP 状态码和其他任意属性。

初始化时,上下文对象可以包含可由渲染组件使用的属性:

const context = {
    message: 'From StaticRouter\'s context object' }

const reactMarkup = ReactDOMServer.renderToString(
    <StaticRouter context={context} location={req.url} >
        <App />
    </StaticRouter>
);

这里,上下文对象包含一个message属性,当找到与请求的 URL 匹配的<Route>组件时,包含该属性的staticContext对象可用于呈现的组件:

<Route
    path='/home'
    render={({ staticContext }) => {
        return (
            <div>
                Inside Home Route, Message - {staticContext.message}
            </div>
        );
    }}
/>

当您尝试访问/home路径时,前面的<Route>匹配,并且呈现staticContext消息属性中提到的值

staticContext道具仅在服务器端环境中可用,因此,当您尝试在同构应用中引用staticContext对象(将在下一节中讨论)时,会抛出一个错误,指出您正在尝试访问 undefined 的属性消息。可以添加检查以查看staticContext是否可用,或者是否可以检查在网页包配置中定义的__isBrowser__属性的值:

<Route
    path='/home'
    render={({ staticContext }) => {
        if (!__isBrowser__) {
            return (
                <div>
                    Inside Home Route, Message - {staticContext.message}
                </div>
            );
        }
        return (
            <div>Inside Home Route, Message</div>
        );
    }}
/>

在前面的示例中,如果页面是在服务器端呈现的,__isBrowser__属性将是false,并且将呈现staticContext对象中指定的消息。

创建同构应用

代码可以在服务器端和客户端环境上运行而几乎没有更改或没有更改的应用称为同构应用。在同构应用中,用户的 web 浏览器发出的第一个请求由服务器处理,任何后续请求由客户端处理。通过在服务器端处理和呈现第一个请求,并发送 HTML、CSS 和 JavaScript 代码,可以提供更好的用户体验,还可以帮助搜索引擎爬虫对页面进行索引。然后,所有后续请求都可以由客户端代码处理,该代码作为服务器第一次响应的一部分发送。

以下是更新的请求-响应流:

要在客户端呈现应用,可以使用<BrowserRouter><HashRouter>组件。在本例中,我们将使用<BrowserRouter>组件。

为客户端代码添加目录后的应用结构如下:

/server-side-app
|--/src
|----/client
|------index.js
|----/server
|------index.js
|----/shared
|------App.js

在这里,shared目录将包含服务器和客户端代码都可以使用的代码。使用<BrowserRouter>组件的客户端特定代码位于client目录中的index.js文件中:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { App } from "../shared/App";

// using hydrate instead of render in SSR app
ReactDOM.hydrate(
    <BrowserRouter>
        <App />
    </BrowserRouter>,
    document.getElementById("app")
);

这里,使用ReactDOM类中的hydrate方法,而不是调用render方法来呈现应用。hydrate方法专门设计用于处理初始呈现发生在服务器端(使用ReactDOMServer)的情况,并且更新页面特定部分的所有后续路由更改请求都由客户端代码处理。hydrate方法用于将事件侦听器附加到服务器端呈现的标记。

下一步是构建应用,以便在构建时生成客户端包,并将其包含在服务器的第一个响应中。

网页包配置

现有的 webpack 配置构建服务器端应用并运行nodemon实用程序来监视更改。要生成客户端包,我们需要包含另一个网页包配置文件-webpack-client.config.babel.js

import path from 'path';
import webpack from 'webpack';

export default {
    entry: './src/client/index.js',
    output: {
        path: path.resolve(__dirname, './dist/public'),
        filename: 'bundle.js',
        publicPath: '/'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: 'babel-loader'
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            __isBrowser__: "true"
        })
    ]
}

前面的配置解析了/src/client/index.j文件中的依赖项,并在/dist/public/bundle.js处创建了一个 bundle。此捆绑包包含运行应用所需的所有客户端代码;不仅是index.js文件中的代码,还有shared目录中声明的组件。

当前的npm start脚本也需要修改,以便客户端应用代码与服务器端代码一起构建。让我们创建一个同时导出服务器和客户端网页包配置的文件-webpack.config.babel.js

import clientConfig from './webpack-client.config.babel';
import serverConfig from './webpack-server.config.babel';

export default [clientConfig, serverConfig];

最后,npm start脚本更新为参考前面的配置文件:

"start": "webpack --config webpack.config.babel.js --mode development --watch"

前面的脚本将生成包含服务器端代码的server.js和包含客户端代码的bundle.js

服务器端配置

最后一步是更新服务器端代码,将客户端包(bundle.js作为第一个响应的一部分包含在内。服务器端代码可以包含一个<script>标记,用于指定源(src属性中的bundle.js文件:

res.send(`
    <!DOCTYPE HTML>
    <html>
        <head>
            <title>React SSR example</title>
            <script src='/bundle.js' defer></script>
        ...
    </html>
`);

此外,为了让我们的 express 服务器提供 JavaScript 文件,我们还包括了用于提供静态内容的中间件功能:

app.use(express.static('dist/public'))

前面的代码允许从dist/public目录提供静态文件,如 JavaScript 文件、CSS 文件和图像。在使用app.get()之前,应包含上述语句。

当您在/home路径访问应用时,第一个响应来自服务器,并且除了呈现与/home路径匹配的<Route>之外,响应中还包括客户端包bundle.js。通过浏览器下载bundle.js文件,然后通过客户端代码处理路由路径中的任何更改。

总结

在本章中,我们研究了如何使用ReactDOMserver.renderToString方法在服务器端(使用 Node.js 和 Express.js)呈现 React 应用。React Router 中的<StaticRouter>组件可用于包装应用的根组件,从而使您能够添加与服务器端请求的 URL 路径匹配的<Route>组件。<StaticRouter>组件接受道具contextlocation。呈现组件中的staticContext道具(仅在服务器端可用)包含context道具中的<StaticRouter>提供的数据。当您想使用<Redirect>组件重定向用户时,它还可以用于添加属性

matchPath函数用于确定请求的 URL 是否与{path, exact, strict, sensitive}形状的提供对象匹配。这类似于库如何将请求的 URL 与页面中可用的<Route>组件相匹配。matchPath函数使我们能够确定请求的 URL 是否与集合中的任何 routes 对象匹配;这为我们提供了一个提前发送 404:Page not found 响应的机会

还可以创建一个同构的 React 应用,在服务器端呈现第一个请求,在客户端呈现后续请求。这是通过在从服务器发送第一个响应时包含客户端包文件来实现的。客户端代码在第一个请求之后接管,这使您能够更新页面中与请求的路由匹配的特定部分。

第 7 章在 React 本机应用中使用 NativeRouter,我们将了解如何使用NativeRouter组件在使用 React 本机创建的本机移动应用中定义路由**