A Web application accesses a specific HTML page through a URL, and each URL corresponds to a resource. In traditional Web applications, the browser sends a request to the server through the URL, and the server reads the resources and sends the processed page content to the browser. However, in single-page applications, all url changes are handled by the browser. When the URL changes, the browser replaces the content with JS. For the application rendered by the server, when a URL resource is requested, the server sends the corresponding page content to the browser. The browser downloads the JS referenced by the page and initializes the client route. The subsequent route redirects to the browser, and the server is only responsible for the first rendering of the request from the browser

Start by creating four page components in the SRC directory of the previously built project

Install the React Web side to rely on the React -router-dom

Note: React-router-dom version 4.x

Previous section: Project construction

The source address is at the end of the article

The server code for this section has been rewrited. please click here for details

The front-end routing

JSX uses the BrowserRouter component to wrap the root node and the NavLink component to wrap the text in the Li tag

import { 
  BrowserRouter as Router,
  Route,
  Switch,
  Redirect,
  NavLink
} from "react-router-dom";
import Bar from "./views/Bar";
import Baz from "./views/Baz";
import Foo from "./views/Foo";
import TopList from "./views/TopList";
Copy the code
render() { return ( <Router> <div> <div className="title">This is a react ssr demo</div> <ul className="nav"> <li><NavLink to="/bar">Bar</NavLink></li> <li><NavLink to="/baz">Baz</NavLink></li> <li><NavLink to="/foo">Foo</NavLink></li> <li><NavLink to="/top-list">TopList</NavLink></li> </ul> <div className="view"> <Switch> <Route path="/bar" component={Bar} /> <Route path="/baz" component={Baz} /> <Route path="/foo" component={Foo} /> <Route  path="/top-list" component={TopList} /> <Redirect from="/" to="/bar" exact /> </Switch> </div> </div> </Router> ); }Copy the code

In the above code, each routing view is placeholder with Route, and the components corresponding to the routing view need to be imported in the current component. If there are nested routes, the view components will be scattered among different components to be imported. When there are too many nested components, it becomes difficult to maintain

Next, import all view components in a JS file, export a list of routing configuration objects, specify routing paths with Path, and specify routing view components with Component

src/router/index.js

import Bar from ".. /views/Bar"; import Baz from ".. /views/Baz"; import Foo from ".. /views/Foo"; import TopList from ".. /views/TopList"; const router = [ { path: "/bar", component: Bar }, { path: "/baz", component: Baz }, { path: "/foo", component: Foo }, { path: "/top-list", component: TopList, exact: true } ]; export default router;Copy the code

Import the configured Route object in app.jsx and loop back to Route

<div className="view">
  <Switch>
    {
      router.map((route, i) => (
        <Route key={i} path={route.path} component={route.component} 
        exact={route.exact} />
      ))
    }
    <Redirect from="/" to="/bar" exact />
  </Switch>
</div>
Copy the code

The component property of Route can pass not only the component type but also the callback function. The callback function passes the child routes of the current component through props, and then continues the loop

To support component nesting, we use Route to encapsulate a NestedRoute component

src/router/NestedRoute.jsx

import React from "react"; import { Route } from "react-router-dom"; Const NestedRoute = (route) => (< route path={route.path} exact={route. Render ={(props) => < route.component_props {/ render={(props) => < route.component_props... props} router={route.routes}/>} /> ); export default NestedRoute;Copy the code

Then export it from SRC /router/index.js

import NestedRoute from "./NestedRoute"; . export { router, NestedRoute }Copy the code

App.jsx

import { router, NestedRoute } from "./router";
Copy the code
<div className="view"> <Switch> { router.map((route, i) => ( <NestedRoute key={i} {... route} /> )) } <Redirect from="/" to="/bar" exact /> </Switch> </div>Copy the code

Use nested routes like the following

const router = [
  {
    path: "/a",
    component: A
  },
  {
    path: "/b",
    component: B
  },
  {
    path: "/parent",
    component: Parent,
    routes: [
      {
        path: "/child",
        component: Child,
      }
    ]
  }
];
Copy the code

Parent.jsx

this.props.router.map((route, i) => ( <NestedRoute key={i} {... route} /> ))Copy the code

The back-end routing

Unlike the client, the server route is stateless. React StaticRouter provides a stateless components, to give the url to StaticRouter, call ReactDOMServer. RenderToString () can be matched to the routing view

Differentiate between client and server in app.jsx, and export the different root components

let App; If (process.env.react_env === "server") {// Export Root component App = Root; } else { App = () => { return ( <Router> <Root /> </Router> ); }; } export default App;Copy the code

Next change entry-server.js to wrap the root component with StaticRouter, pass in the context context and location, and use functions to create a new component

import React from "react";
import { StaticRouter } from "react-router-dom";
import Root from "./App";

const createApp = (context, url) => {
  const App = () => {
    return (
      <StaticRouter context={context} location={url}>
        <Root/>  
      </StaticRouter>
    )
  }
  return <App />;
}

module.exports = {
  createApp
};
Copy the code

Get the createApp function from server.js

let createApp; let template; let readyPromise; if (isProd) { let serverEntry = require(".. /dist/entry-server"); createApp = serverEntry.createApp; template = fs.readFileSync("./dist/index.html", "utf-8"); App. use("/dist", express.static(path.join(__dirname, ".. /dist"))); } else { readyPromise = require("./setup-dev-server")(app, (serverEntry, htmlTemplate) => { createApp = serverEntry.createApp; template = htmlTemplate; }); }Copy the code

When the server passes in the current URL while processing the request, the server matches the view component corresponding to the current URL

const render = (req, res) => { console.log("======enter server======"); console.log("visit url: " + req.url); let context = {}; let component = createApp(context, req.url); let html = ReactDOMServer.renderToString(component); let htmlStr = template.replace("<! --react-ssr-outlet-->", `<div id='app'>${html}</div>`); // Send the rendered HTML string to the client res.send(htmlStr); }Copy the code

404 and redirect

When the requested server resource does not exist, the server needs to make a 404 response, the route has been redirected, and the server needs to redirect to the specified URL. The StaticRouter provides a props to pass the context object context. The staticContext is used to get and set the status code when rendering the routing component. The server uses the status code to determine the response when rendering the routing component. If a redirect occurs during server route rendering, attributes related to the redirect, such as the URL, are automatically added to the context

To handle the 404 state, we encapsulate a state component, StatusRoute

src/router/StatusRoute.jsx

import React from "react"; import { Route } from "react-router-dom"; Const StatusRoute = (props) => (<Route render={({staticContext}) => {// If (staticContext) {// Set the staticContext.status = props. Code; } return props.children; }} / >); export default StatusRoute;Copy the code

Export from SRC /router/index.js

import StatusRoute from "./StatusRoute"; . export { router, NestedRoute, StatusRoute }Copy the code

Use the StatusRoute component in app.jsx

<div className="view"> <Switch> { router.map((route, i) => ( <NestedRoute key={i} {... route} /> )) } <Redirect from="/" to="/bar" exact /> <StatusRoute code={404}> <div> <h1>Not Found</h1> </div> </StatusRoute> </Switch> </div>Copy the code

The render function is modified as follows

let context = {}; let component = createApp(context, req.url); let html = ReactDOMServer.renderToString(component); if (! Context.status) {let htmlStr = template.replace("<! --react-ssr-outlet-->", `<div id='app'>${html}</div>`); // Send the rendered HTML string to the client res.send(htmlStr); } else {res.status(context.status).send("error code: "+ context.status); }Copy the code

When rendering, the server checks context.status. If the status attribute does not exist, a route is matched. If the status attribute does exist, the server sets the status code and responds to the result

In the App. JSX USES a Redirect routing < Redirect the from = “/” to = “/ bar exact” / >, http://localhost:3000 will be redirected to http://localhost:3000/bar, The StaticRouter route is stateless and cannot be redirected. When accessing http://localhost:3000, the server returns HTML fragments rendered in app.jsx, not content rendered by the bar.jsx component

The render method for bar.jsx is as follows

render() {
  return (
    <div>
      <div>Bar</div>
    </div>
  );
}
Copy the code

Because the client routing, the browser address Bar has been changed to http://localhost:3000/bar, and apply colours to a drawing gives the Bar. The contents of the JSX, but do not match the client and server rendering

Add a line of console.log(context) to server.jsx

let context = {}; let component = createApp(context, req.url); let html = ReactDOMServer.renderToString(component); console.log(context); .Copy the code

Then visit http://loclahost:3000 and you can see the following output on your terminal

======enter server======
visit url: /
{ action: 'REPLACE',
  location: { pathname: '/bar', search: '', hash: '', state: undefined },
  url: '/bar' }
Copy the code

Context is used to obtain the URL for server-side redirection

If (context.url) {// The static route sets the url res.redirect(context.url) when a redirection occurs; return; }Copy the code

When you visit http://loclahost:3000, the browser sends two requests, the first request/and the second redirection to /bar

Management of the Head

Each page has its own head information such as title, meta, link, etc. The React-Helmet plugin is used to manage the head, which also supports server-side rendering

Install the react – first helmet

npm install react-helmet

Then import from app.jsx to add a custom head

import { Helmet } from "react-helmet";
Copy the code
<div>
  <Helmet>
    <title>This is App page</title>
    <meta name="keywords" content="React SSR"></meta>
  </Helmet>
  <div className="title">This is a react ssr demo</div>
  ...
</div>
Copy the code

In the rendering of a service, called ReactDOMServer. RenderToString (after) you need to call Helmet. RenderStatic () to acquire information on the head, to the server. Use App in js. JSX the Helmet, Some changes need to be made in entry-server.js and app.jsx

entry-server.js

const createApp = (context, url) => { const App = () => { return ( <StaticRouter context={context} location={url}> <Root setHead={(head) => App.head  = head}/> </StaticRouter> ) } return <App />; }Copy the code

App.jsx

class Root extends React.Component { constructor(props) { super(props); If (process.env.react_env === "server") {this.props. SetHead (Helmet); }}... }Copy the code

Pass a props function setHead to the Root component, and call the setHead function when the Root component is initialized to add a head property to the new App component

Modify the template index.html to add
as a placeholder for head information

<head> <meta charset=" utF-8 "> <meta name="viewport" content="width=device-width, initial =1.0, user-scalable=no"> <link rel="shortcut icon" href="/public/favicon.ico"> <title>React SSR</title> <! --react-ssr-head--> </head>Copy the code

Replace it in server.js

if (! Context.status) {// No status field indicates that the route matches successfully. Must obtain after the component renderToString let head = component. The head. The renderStatic (); Let htmlStr = template.replace (/<title>.*<\/title>/, '${head.title.toString()}').replace("<! --react-ssr-head-->", `${head.meta.toString()}\n${head.link.toString()})`) .replace("<! --react-ssr-outlet-->", `<div id='app'>${html}</div>`); // Send the rendered HTML string to the client res.send(htmlStr); } else {res.status(context.status).send("error code: "+ context.status); }Copy the code

Component is the JSX syntactic transformed object
, component.type is the component type to get this object, here is App in entry-server.js

Note: you must call renderStatic() with the Helmet imported from app.jsx to get the header information

When you visit http://localhost:3000, the header information has already been rendered

Each route corresponds to a view, and each view has its own head information. The view component is nested in the root component, and the same information will be automatically replaced when components are nested and react-Helmet is used

Use react-helmet custom headers in bar.jsx, Baz. JSX, foo. JSX, and toplist.jsx, respectively. Such as

class Bar extends React.Component { render() { return ( <div> <Helmet> <title>Bar</title> </Helmet> <div>Bar</div> </div> ); }}Copy the code

Paint your browser title, enter http://localhost:3000/bar in < title data – the react – helmet = “true” > Bar < / title >

Enter http://localhost:3000/baz title rendering into < title data – the react – helmet = “true” > Baz < / title >

conclusion

This section configures and manages React basic routes, simplifying maintenance and laying a foundation for subsequent data prefetch. The StaticRouter component is used in the server route rendering. This component has two props, Context and location. When rendering, you can give custom properties to the context, such as setting the status code, and location is used to match the route. The React-Helmet plugin provides a simple way to define head information on both the client and the server

Source code of this chapter

Next section: Code splitting and data prefetching