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
編寫React路由時,咱們先用最基本的作法,在App.jsx
中使用BrowserRouter
組件包裹根節點,用NavLink
組件包裹li標籤中的文本web
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
,當組件嵌套太多,會變得難以維護express
接下來針對上述問題進行改造,全部視圖組件都在一個js文件中import
,導出一個路由配置對象列表,分別用path
指定路由路徑,component
指定路由視圖組件npm
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信息,同時支持客戶端和服務端
下一節:代碼分割和數據預取