内容简介:Web应用是通过url访问某个具体的HTML页面,每个url都对应一个资源。传统的Web应用中,浏览器通过url向服务器发送请求,服务器读取资源并把处理好的页面内容发送给浏览器,而在单页面应用中,所有url变化的处理都在浏览器端完成,url发生变化时浏览器通过js将内容替换。对于服务端渲染的应用,当请求某个url资源,服务器要将该url对应的页面内容发送给浏览器,浏览器下载页面引用的js后执行客户端路由初始化,随后的路由跳转都是在浏览器端,服务端只负责从浏览器发送请求的第一次渲染首先在之前搭建的项目中然后
Web应用是通过url访问某个具体的HTML页面,每个url都对应一个资源。传统的Web应用中,浏览器通过url向服务器发送请求,服务器读取资源并把处理好的页面内容发送给浏览器,而在单页面应用中,所有url变化的处理都在浏览器端完成,url发生变化时浏览器通过js将内容替换。对于服务端渲染的应用,当请求某个url资源,服务器要将该url对应的页面内容发送给浏览器,浏览器下载页面引用的js后执行客户端路由初始化,随后的路由跳转都是在浏览器端,服务端只负责从浏览器发送请求的第一次渲染
首先在之前搭建的项目中 src
目录下创建4个页面组件
然后安装React Web端依赖react-router-dom
注:react-router-dom版本4.x
上一节:项目搭建
源码地址见文章末尾
前端路由
编写React路由时,我们先用最基本的做法,在 App.jsx
中使用 BrowserRouter
组件包裹根节点,用 NavLink
组件包裹li标签中的文本
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
,当组件嵌套太多,会变得难以维护
接下来针对上述问题进行改造,所有视图组件都在一个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和重定向
当请求服务器资源不存在时,服务器需要做出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管理
每一个页面都有对应的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信息,同时支持客户端和服务端
下一节:数据预取和同步(待更新)
以上所述就是小编给大家介绍的《React服务端渲染(前后端路由同构)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- react同构实践——实现自己的同构模板
- 如何构建一个WEB同构应用
- React Native 三端同构实战
- React 中同构(SSR)原理脉络梳理
- 打造可降级的React服务端同构框架
- Vue SSR技术方案落地实现—构建同构应用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。