所有源代码、文档和图片都在 github 的仓库里,点击进入仓库
相关阅读
- React服务端渲染之路01——项目基础架构搭建
- React服务端渲染之路02——最简单的服务端渲染
- React服务端渲染之路03——路由
- React服务端渲染之路04——redux-01
- React服务端渲染之路05——redux-02
- React服务端渲染之路06——优化
- React服务端渲染之路07——添加CSS样式
- React服务端渲染之路08——404和重定向
- React服务端渲染之路09——SEO优化
1. 路由
- 在页面上有了内容之后,我们还需要进行路由的跳转
- 下载路由依赖包,react-router-dom,
npm i react-router-dom -S
- 这里暂时先不介绍多级路由,多级路由放在后边介绍,现在我们只介绍一级路由
1.1 路由介绍
- 我们在使用客户端渲染的时候,页面跳转路由是前端控制的,主要有两种模式,一种是 history 模式,另外一种是 hash 模式。
- 无论是哪种模式,都可以进行路由跳转操作,唯一的不同是 history 模式需要后端控制 404 页面,而 hash 模式不需要
- 在服务端渲染,我们需要的是静态路由(static router),与客户端的路由模式是不一样的
- 路由的控制,不仅仅是在服务端,客户端也需要进行同样的路由规则,那么我们就可以写一份路由,供客户端和服务端一起使用
1.2 路由页面组件
- 路由跳转,至少需要两个页面级组件,所以我们可以先创建一个 News 页面组件,containers/News/index.js
- 之前我们已经有了一个 Home 页面级组件,现在有两个组件,现在页面我们已经准备好了
- containers/News/index.js
// containers/News/index.js
import React, { Component } from 'react';
class News extends Component {
render() {
return (
<div>
<h1>News Page</h1>
</div>
);
}
}
export default News;
1.3 路由文件
- 我们需要导出一个路由组件,组件内部是 Route
- 导出的路由组件,需要作为子组件,才能在页面上渲染
- 现在我们先采用这种模式,后期我们会用另外一种模式,但是不论是哪种模式,实质上是一样的
- 注:
<></>
标签和<Fragment><Fragment/>
标签是一样的,都是一个聚合子元素的一个标签,不增加真实的 DOM 节点 - src/routes.js
// src/routes.js
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './containers/Home';
import News from './containers/News';
export default (
<>
<Route path='/' exact component={Home}/>
<Route path='/news' component={News}/>
</>
);
1.4 客户端路由
- 客户端路由比较简单,直接把路由组件作为子组件传递就可以
- 在这里直接把路由放在这里,实际上是把路由文件里的多个 Route 放在这里,如果我们把路由里的 Route 放在这里,我们会发现实际上是一样的,没有什么区别
import React from 'react'
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import routes from '../routes';
hydrate(
<BrowserRouter>
{routes}
</BrowserRouter>, window.root);
1.5 服务端路由
- 服务端路由相对复杂一些,需要使用无状态路由,也就是静态路由 StaticRouter
,因为服务端并不知道请求是通过什么模式,而且服务端也无需知道,服务端只需要根据客户端发送的请求,做相应的处理即可
- StaticRouter 里需要传递两个属性,一个是 context,一个是 location
- context 主要是用来给组件传递数据信息,这个属性可以在客户端和服务端互相传递参数,比如 css 样式的参数
- location 主要是用来接收请求路径信息,比如 pathname,search,hash 等
- 修改 src/server/index.js
import routes from '../routes';
app.get('*', (req, res) => {
let context = {};
let domContent = renderToString(
<StaticRouter context={context} location={req.path}>
{routes}
</StaticRouter>
);
// html 的内容不改变
res.send(html);
});
- 从代码中我们可以看到,我们移除了 Home 组件,引入了 routes 组件,因为 routes 已经可以匹配到 Home 和 News 组件
- 添加了 StaticRouter 组件,传入了一个 context 值,把 express 的 req.path 传递给了 location 属性,这样服务端就可以知道客户端的路由地址
- 修改了
app.get('/')
为app.get('*')
,因为我们服务端不仅仅是接收同一个路由地址,我们还有 /news 路由,所以我们要把根路径匹配修改为全局匹配。如果不修改为全局匹配呢,也可以,只不过我们如果有 100 个路由,我们就要写 100 个 app.get('/xxx'),如果你愿意的话,也是可以的 原理就是
- 客户端通过路由修改了请求的地址,服务端接收相应,在 StaticRouter 中我们获取到了客户端请求的路由地址
- routes 就开始进行匹配,如果匹配到了,那么就把匹配到的路由组件进行渲染成 HTML 字符串,发送给客户端。
- 如果没有匹配到呢,假如我们访问了
http://localhost:3000/hello
路由,那么我们的 routes 是匹配不到的,既然匹配不到,那么 renderToString 渲染的结果就是空,domContent 作为空值插入 HTML 模板,得到的就是一个空白页面
- 此时,我们就可以在通过不同的 url,访问到不同的页面,查看网页源代码,可以看到不同 url 对应的源代码就是对应组件里的代码
1.5 页面效果与页面源代码
- 首页面效果
- 首页面源代码
- news 页面效果
- news 页面源代码
2. 路由跳转
- 我们已经实现了路由的切换,但是我们是通过输入 url 来完成路由切换的,显然我们应该通过点击实现跳转,所以我们使用 Link 实现路由跳转
- 我们创建一个 Header 导航,在 Header 导航中进行点击路由,实现页面的切换
- 为了样式更加美观,我们引入 [email protected],这里是采用了 又拍云(bootcdn) 的 bootstrap 资源,我们也可以把 bootstrap 放入 public 文件夹下,像引入 client.js 一样引入 bootstrap.css,效果是一样的
2.1 Header 组件
- Header 组件,/components/Header/index.js
- 使用 bootstrap 的导航样式
- /components/Header/index.js
// /components/Header/index.js
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
class Header extends Component {
render() {
return (
<nav className="navbar navbar-inverse navbar-fixed-top">
<div className="container-fluid">
<div className="navbar-header">
<a className="navbar-brand">REACT-SSR</a>
</div>
<div>
<ul className="nav navbar-nav">
<li><Link to="/">首页</Link></li>
<li><Link to="/news">新闻</Link></li>
</ul>
</div>
</div>
</nav>
);
}
}
export default Header;
2.2 把 Header 组件进行同构
- 这里的同构就很简单,直接把 Header 组件放入 BrowserRouter 和 StaticRouter 中,然后稍微进行一下样式的调整,就可以了
- 客户端同构,修改 src/client/index.js
// src/client/index.js
import React from 'react'
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import routes from '../routes';
import Header from '../components/Header';
hydrate(
<BrowserRouter>
<>
<Header/>
<div className="container" style={{marginTop: 70}}>
{routes}
</div>
</>
</BrowserRouter>, window.root);
- 服务端同构,修改 src/server/index.js
// src/server/index.js
import Header from '../compoents/Header';
let domContent = renderToString(
<StaticRouter context={context} location={req.path}>
<>
<Header/>
<div className="container" style={{marginTop: 70}}>
{routes}
</div>
</>
</StaticRouter>
);