Web应用是经过url访问某个具体的HTML页面,每一个url都对应一个资源。传统的Web应用中,浏览器经过url向服务器发送请求,服务器读取资源并把处理好的页面内容发送给浏览器,而在单页面应用中,全部url变化的处理都在浏览器端完成,url发生变化时浏览器经过js将内容替换。对于服务端渲染的应用,当请求某个url资源,服务器要将该url对应的页面内容发送给浏览器,浏览器下载页面引用的js后执行客户端路由初始化,随后的路由跳转都是在浏览器端,服务端只负责从浏览器发送请求的第一次渲染html
首先在以前搭建的项目中src
目录下建立4个页面组件前端
而后安装React Web端依赖react-router-domreact
注:react-router-dom版本4.x
webpack
上一节:项目搭建git
源码地址见文章末尾github
本节服务端代码已进行重写,详情请戳这里web
编写React路由时,咱们先用最基本的作法,在App.jsx
中使用BrowserRouter
组件包裹根节点,用NavLink
组件包裹li标签中的文本express
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";
复制代码
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>
);
}
复制代码
上述代码中每一个路由视图都用Route
占位,而路由视图对应的组件在当前组件中都须要import
进来,若是有路由嵌套,视图组件就会被分散到不一样的组件中被import
,当组件嵌套太多,会变得难以维护npm
接下来针对上述问题进行改造,全部视图组件都在一个js文件中import
,导出一个路由配置对象列表,分别用path
指定路由路径,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;
复制代码
在App.jsx
中导入配置好的路由对象,循环返回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>
复制代码
复杂的应用中免不了组件嵌套的状况,Route
的component
属性不只能够传递组件类型还能够传递回调函数,经过回调函把当前组件的子路由经过props
传递,而后继续循环
为了支持组件嵌套,咱们使用Route
进行封装一个NestedRoute
组件
src/router/NestedRoute.jsx
import React from "react";
import { Route } from "react-router-dom";
const NestedRoute = (route) => (
<Route path={route.path} exact={route.exact}
/*渲染路由对应的视图组件,将路由组件的props传递给视图组件*/
render={(props) => <route.component {...props} router={route.routes}/>}
/>
);
export default NestedRoute;
复制代码
而后从src/router/index.js
中导出
import NestedRoute from "./NestedRoute";
...
export {
router,
NestedRoute
}
复制代码
App.jsx
import { router, NestedRoute } from "./router";
复制代码
<div className="view">
<Switch>
{
router.map((route, i) => (
<NestedRoute key={i} {...route} />
))
}
<Redirect from="/" to="/bar" exact />
</Switch>
</div>
复制代码
使用嵌套的路由像下面这样
const router = [
{
path: "/a",
component: A
},
{
path: "/b",
component: B
},
{
path: "/parent",
component: Parent,
routes: [
{
path: "/child",
component: Child,
}
]
}
];
复制代码
Parent.jsx
this.props.router.map((route, i) => (
<NestedRoute key={i} {...route} />
))
复制代码
服务端路由不一样于客户端,它是无状态的。React提供了一个无状态的组件StaticRouter,向StaticRouter
传递url,调用ReactDOMServer.renderToString()
就能匹配到路由视图
在App.jsx
中区分客户端和服务端,而后export
不一样的根组件
let App;
if (process.env.REACT_ENV === "server") {
// 服务端导出Root组件
App = Root;
} else {
App = () => {
return (
<Router>
<Root />
</Router>
);
};
}
export default App;
复制代码
接下来对entry-server.js
进行修改,使用StaticRouter
包裹根组件,传入上下文context
和location
,同时使用函数来建立一个新的组件
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
};
复制代码
server.js
中获取createApp
函数
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");
// 静态资源映射到dist路径下
app.use("/dist", express.static(path.join(__dirname, "../dist")));
} else {
readyPromise = require("./setup-dev-server")(app, (serverEntry, htmlTemplate) => {
createApp = serverEntry.createApp;
template = htmlTemplate;
});
}
复制代码
在服务端处理请求时把当前url传入,服务端会匹配和当前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>`);
// 将渲染后的html字符串发送给客户端
res.send(htmlStr);
}
复制代码
当请求服务器资源不存在时,服务器须要作出404响应,路由发生了重定向,服务器也须要重定向到指定的url。StaticRouter
提供了一个props用来传递上下文对象context
,在渲染路由组件时经过staticContext
获取并设置状态码,服务端渲染时经过状态码判断作响应处理。若是服务端路由渲染时发生了重定向,经过context
自动添加上与重定向相关信息的属性,如url
为了处理404状态,咱们封装一个状态组件StatusRoute
src/router/StatusRoute.jsx
import React from "react";
import { Route } from "react-router-dom";
const StatusRoute = (props) => (
<Route render={({staticContext}) => {
// 客户端无staticContext对象
if (staticContext) {
// 设置状态码
staticContext.status = props.code;
}
return props.children;
}} />
);
export default StatusRoute;
复制代码
从src/router/index.js
中导出
import StatusRoute from "./StatusRoute";
...
export {
router,
NestedRoute,
StatusRoute
}
复制代码
在App.jsx
中使用StatusRoute
组件
<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>
复制代码
render
函数修改以下
let context = {};
let component = createApp(context, req.url);
let html = ReactDOMServer.renderToString(component);
if (!context.status) { // 无status字段表示路由匹配成功
let htmlStr = template.replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>`);
// 将渲染后的html字符串发送给客户端
res.send(htmlStr);
} else {
res.status(context.status).send("error code:" + context.status);
}
复制代码
服务端渲染时判断context.status
,不存在status
属性表示匹配到路由,存在则设置状态码并响应结果
App.jsx
中使用了一个重定向路由<Redirect from="/" to="/bar" exact />
,访问http://localhost:3000
时就会重定向到http://localhost:3000/bar
,而在StaticRouter
中路由是没有状态的,没法进行重定向,当访问http://localhost:3000
服务端返回的是App.jsx
中渲染的html片断,不包含Bar.jsx
组件渲染的内容
Bar.jsx
的render
方法以下
render() {
return (
<div>
<div>Bar</div>
</div>
);
}
复制代码
由于客户端的路由,浏览器地址栏已经变成了http://localhost:3000/bar
,而且渲染出Bar.jsx
中的内容,可是客户端和服务端渲染不一致
在server.jsx
中增长一行代码console.log(context)
let context = {};
let component = createApp(context, req.url);
let html = ReactDOMServer.renderToString(component);
console.log(context);
...
复制代码
而后访问http://loclahost:3000
,能够在终端看到如下输出信息
======enter server======
visit url: /
{ action: 'REPLACE',
location: { pathname: '/bar', search: '', hash: '', state: undefined },
url: '/bar' }
复制代码
经过context
获取url
进行服务端重定向处理
if (context.url) { // 当发生重定向时,静态路由会设置url
res.redirect(context.url);
return;
}
复制代码
此时访问http://loclahost:3000
,浏览器发送了两次请求,第一次请求/
,第二次重定向到/bar
每个页面都有对应的head信息如title、meta和link等,这里使用react-helmet插件来管理Head,它同时支持服务端渲染
先安装react-helmet
npm install react-helmet
而后在App.jsx
中import
,添加自定义head
import { Helmet } from "react-helmet";
复制代码
<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>
复制代码
在服务端渲染时,调用ReactDOMServer.renderToString()
后须要调用Helmet.renderStatic()
才能获取head相关信息,为了在server.js
中使用App.jsx
中的Helmet
,须要在入口entry-server.js
和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 />;
}
复制代码
App.jsx
class Root extends React.Component {
constructor(props) {
super(props);
if (process.env.REACT_ENV === "server") {
// 当前若是是服务端渲染时将Helmet设置给外层组件的head属性中
this.props.setHead(Helmet);
}
}
...
}
复制代码
给Root
组件传入一个props函数setHead
,在Root
组件初始化时调用setHead
函数给新的App
组件添加一个head
属性
修改模板index.html
,添加<!--react-ssr-head-->
做为head信息占位
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="/public/favicon.ico">
<title>React SSR</title>
<!--react-ssr-head-->
</head>
复制代码
在server.js
中进行替换
if (!context.status) { // 无status字段表示路由匹配成功
// 获取组件内的head对象,必须在组件renderToString后获取
let head = component.type.head.renderStatic();
// 替换注释节点为渲染后的html字符串
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>`);
// 将渲染后的html字符串发送给客户端
res.send(htmlStr);
} else {
res.status(context.status).send("error code:" + context.status);
}
复制代码
component
是<App />
通过jsx语法转换后的对象,component.type
是获取该对象的组件类型,这里是entry-server.js
中的App
注意:这里必须经过
App.jsx
中import
进来的Helmet
调用renderStatic()
后才能获头部信息
访问http://localhost:3000
时,头部信息已经被渲染出来了
每个路由对应一个视图,每个视图都有各自的head信息,视图组件是嵌套在根组件中的,当组件发生嵌套使用react-helmet时会自动替换相同的信息
在Bar.jsx
、Baz.jsx
、Foo.jsx
和TopList.jsx
中分别使用react-helmet自定义标题。如
class Bar extends React.Component {
render() {
return (
<div>
<Helmet>
<title>Bar</title>
</Helmet>
<div>Bar</div>
</div>
);
}
}
复制代码
浏览器输入http://localhost:3000/bar
时标题渲染成<title data-react-helmet="true">Bar</title>
输入http://localhost:3000/baz
时标题渲染成<title data-react-helmet="true">Baz</title>
本节对React基本路由进行配置化管理,使得维护起来更加简单,也为后续数据预取奠基了基础。在服务端路由渲染中使用了StaticRouter
组件,这个组件有context
和location
两个props,渲染时能够自行给context
赋予自定义属性,好比设置状态码,location
则用来匹配路由。服务端渲染中head信息必不可少,react-helmet插件提供了简单的用法来定义head信息,同时支持客户端和服务端
下一节:代码分割和数据预取