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
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