我绑定的事件哪去了?
在我的上一个博客中,我们实现了最基本的SSR服务端渲染的流程,学习了组件的服务端渲染方式,那就是通过renderToString方法将其渲染为字符串,但是我也说了,由于被渲染成了字符串,所以一系列的事件绑定都没有应用上去。
接下来我们就来学习事件的绑定。
我们都知道SSR有两套代码,一套在服务端运行一次,一套在客户端运行一次,所以我们这里需要改动客户端那一套的代码。改造起来特别简单,就是将你写的react代码在客户端再写一次(由于我们在编写配置文件的时候已经将客户端的配置文件编写了,所以我们只需要将客户端的文件书写出来)。有些同学就可能要问:这不是很麻烦吗?其实一点都不麻烦,代码如下:
import React from 'react'
import {hydrate} from 'react-dom'
import Home from '../pages/Home'
const App = () => {
return (
<Fragment>
<Home />
</Fragment>
)
};
hydrate(<App />, document.getElementById('#root'));
这里我们使用hydrate(专业词汇:注水)代替render,有两个原因:
- 使用render会报错:
因为我们再服务端渲染的时候用了render,在客户端再次运行render函数就会出现重复的情况。 - hydrate复用:
hydrate会尽量复用接收到的服务端返回的内容,来补充事件绑定和浏览器端其他特有的过程。
按照服务端渲染的流程,当浏览器解析完成之后,也一样会解析客户端的那一套js文件,但是我们并不是像原来客户端渲染那样慢,因为浏览器现在呈现给我们的就是一个完整的网页(虽然是以字符串的形式响应的),首屏加载很快,这样浏览器就有空余的事件去解析客户端的react代码。
这样,我们就可以在Home组件里面绑定事件了,而且浏览器也可以成功的响应事件。
难道就能存在一个页面吗?
当然不是,这里我们就会学习到react中路由的渲染,首先我们来回忆一下客户端渲染的路由。
- BrowserRouter/HashRouter
客户端渲染使用的Router是BrowserRouter或HashRouter。
BrowserRouter是以服务器的形式对浏览器BOM对象中的history进行修改;而HashRouter则是使用锚点的方式实现路由的功能。
那么我们再来探讨一下服务器渲染的Router
- StaticRouter
在服务端渲染的时候,我们就不能使用BrowserRouter/HashRouter了,我们使用全新的一个Router——StaticRouter,相信很少有伙伴使用它吧。而我们服务端的路由渲染就要靠他了。
可能有很多同学没注意到,我们在写服务端express的请求时候的请求路径是 “ * ”,记住必须要把请求路径设置为*
- 我们不可能每有一个页面就要去配置一个路由再渲染一次
- 路由跳转的时候也不会也必须用到req
怎么配置呢?
这里我使用更加规范的写法,还请各位理解清楚。
为了比较使用路由我在pages下面新建了一个Login组件
- 新建route文件,作为我们的路由配置
import Home from '../pages/Home'
import Login from '../pages/Login'
export const mainRoute = [
{
path: '/home',
component: Home,
exact: true
},
{
path: '/login',
component: Login
}
];
- 新建Login组件
import React, {Component, Fragment} from 'react'
import Header from '../../components/Header' // 引入了Header组件
export default class Login extends Component {
render() {
return (
<Fragment>
<Header/>
this is login
</Fragment>
)
}
}
- 新建Header组件
import React, {Fragment} from 'react'
import {Link} from 'react-router-dom'
export default () => {
return (
<Fragment>
<Link to='/'>Home</Link><br />
<Link to='/login'>Login</Link><br />
</Fragment>
)
};
- 在服务端、和客户端渲染路由
- src目录下新建config文件夹,在里面新建route.js
route.js就是我们渲染客户端和服务器端的共同配置,望大家仔细阅读一下代码
由于我们在mainRoute里面配置了路由所需要的参数,所以我们在这里直接通过map进行渲染,这个方法也适用与平时我们些项目的时候,我推荐这样写,因为层次结构分明,而且项目越大,这种书写方式优势越大。import React from 'react' import {mainRoute} from "../route"; import {Provider} from 'react-redux' import {BrowserRouter, StaticRouter, Switch, Redirect, Route} from "react-router-dom"; export const Routes = (props) => { // 这里通过判断客户端和服务端传过来的参数进行location和context的指定渲染 // 如果传过来的router是BrowserRouter那就使用BrowserRouter let Router = props.router === 'BrowserRouter' ? BrowserRouter : StaticRouter; let location = props.req ? props.req.url : '', context = props.context ? props.context : ''; console.log(props); return ( <Router location={location} context={context}> {/* StaticRouter需要两个参数 location(固定参数:req.url,表示客户端请求服务器端的地址, 因为服务器端不是浏览器,所以需要传递req.url供服务端识别) context(服务端下需要接受的参数,可以自己定义) */} <Switch> { mainRoute.map((item, index) => { return ( <Route exact={item.exact} key={index} path={item.path} render={props => <item.component {...props}/>}/> ) }) } <Redirect from='/' to='/home'/> </Switch> </Router> ); };
- 服务端引入
修改服务端代码,将冗长的html部分抽离出来- 在服务端文件同级新建utils.js,并修改服务器端文件
import React from 'react' import {renderToString} from "react-dom/server"; import {Routes} from '../config/route' export const render = (req) => { const content = renderToString( // 渲染成字符串 <Routes router='StaticRouter' req={req} context={{}}/> ); return ` <!doctype html> <html lang="en"> <head> <title>hello</title> <body> <div id="root">${content}</div> <script src="/index.js"></script><!-- 引入打包之后的客户端文件 --> </body> </head> </html>` }; /*-------------------服务器端修改文件代码----------------*/ import {render} from './utils' app.use(express.static('public')); // 使用中间件,将public设置成静态文件, //这个文件是打包之后的客户端文件 app.get('*', (req, res) => { res.send(render(req)) });
- 客户端引入,修改客户端代码
import {Routes} from '../config/route' const App = () => { return ( <Routes router='BrowserRouter'/> ) };
- src目录下新建config文件夹,在里面新建route.js
文件目录这样子
这样的话,路由的服务端渲染就完成了。也请大家在平时写项目的时候使用结构化较强的目录,公共的重复的拆分出来。