react服务端渲染(SSR)
一、什么是服务端渲染
react是构建客户端应用程序的框架。传统的SPA (单页应用程序 (Single-Page Application)) 是在浏览器加载打包后的js文件,进行dom的生成与渲染。也可以将同一个组件在服务端渲染成html字符串,然后将它响应给浏览器。服务端渲染的react应用程序也被称为“同构”,因为程序大量的代码既可以在客户端运行又可以在服务端运行。
二、为什么要使用服务端渲染
1、提高首屏加载速度,无需等待所有的js加载执行完,用户可以更快地看到完整的页面。
2、利于SEO,搜索引擎爬虫工具可以查看到完全渲染的页面,有利于排到搜索引擎的前面。
缺点:
1、需要在node server环境上运行,而完全静态的SPA可以在任何静态文件服务器上运行。
2、更多的服务端负载。
因此在使用ssr之前要权衡下是否真的需要它,取决于对首屏加载时间的要求和SEO优化。
三、ssr能够实现,本质上是虚拟dom的存在
ssr应用中,react代码需要在客户端和服务端各运行一次,但是node环境是没有dom对象的,如果代码中存在dom操作,那么服务端就无法运行此代码。react中虚拟dom的存在打破了这个局面,虚拟dom其实是一个映射真实dom的js对象。因此在node环境中可以操作虚拟dom,然后服务端将虚拟dom转换成字符串输出;在客户端中也可以操作虚拟dom,然后直接转换成真实的dom对象,完成页面挂载。
四、同构是核心
1、react ssr从renderToString开始
renderToString是react提供的用来做服务端渲染的方法,它可以将jsx转换成html字符串。这样在浏览器中请求页面时,我们就可以将组件引入到服务端转换成html响应给浏览器,以下为示例:
App.jsx
import * as React from 'react'; export default class App extends React.Component { render() { return (<div>hello world!</div>); } }
server.js
import express from "express"; import path from "path"; import React from "react"; import { renderToString } from "react-dom/server"; import App from "./components/App"; const app = express(); app.use(express.static(path.resolve(__dirname, "../dist"))); app.get("/*", (req, res) => { const jsx = <App />; const reactDom = renderToString(jsx); res.writeHead(200, { "Content-Type": "text/html" }); res.end(htmlTemplate(reactDom)); }); app.listen(2048); function htmlTemplate(reactDom) { return ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>React SSR</title> <link rel="stylesheet" type="text/css" href="./styles.css" /> </head> <body> <div id="app">${reactDom}</div> <script src="./app.bundle.js"></script> </body> </html> `; }
上面定义了一个App组件,在server.js中起一个web服务,监听2048端口,引入App组件然后用"react-dom/server"提供的renderToString方法将其转换成html字符串(如下图终端中打印,App组件经过renderToString方法完成了从react组件到html字符串的转换),将html字符串放入模板中一起响应给浏览器。浏览器中输入localhost:2048可以看到获取的document中已经包含了完整的dom结构。这个过程实现了dom的同构,客户端和服务端共用同一套react组件。
2、路由的同构
解决了dom的同构后,还需要对路由进行同构。关键在于,服务端需要用无状态的StaticRouter替换客户端的BrowserRouter或HashRouter。为什么要用StaticRouter呢?因为BrowserRotuer是HTML5提供的history API(pushState
, replaceState,popstate
)来改变浏览器上地址的变化,同时让Route根据url来加载组件。而服务端中不存在HTML5的history对象,服务端可以通过http请求获取url,然后仅根据url来加载组件即可,不需要使用history对象的API改变浏览器地址的变化,所以提供了StaticRouter静态路由。
StaticRouter会根据location属性传的url路由到对应的组件。关于路由的使用,可以单独建一个包含路由信息的文件(routes.js)这样服务端和客户端就共用同一个路由文件,完成了两端的路由同构。
3、状态的同构
状态的同构是让服务器端先执行一下初始化的接口拿到页面初始化需要的状态,然后将这个数据与同构后的html放在一起,这样浏览器渲染页面时就可以拿到初始化的数据来渲染页面,不用再等待相关接口的返回。这个过程也叫作“注水”。此例使用redux来管理状态。
如图所示给需要服务器端渲染的页面加一个serverFetch方法,他将会在服务端运行拿到需要的数据,并且根据需求dispatch相应的action,不需要服务器端渲染的页面就不用加此方法。注意,这里需要返回一个promise,服务器端等这个异步完成拿到数据之后再进行后面的操作,否则还没拿到初始状态就放回给浏览器的html里携带的是个空的状态。
根据请求页面的url获取相应的路由时,这里routes就是和客户端共用的路由配置routes.js,matchPath是react-router-dom提供的方法。拿到组件后检查是否有自定义的serverFetch方法,有的话就表示这个页面需要服务器端渲染,执行这个方法获取初始状态。Promise.all的作用就是用来等待异步完成的,这就是为什么需要在serverFetch里放回promise了。然后就可以从store里拿到state注入到页面了。我们给window对象添加一个REDUX_DATA属性来保存状态。
如上图,服务器响应的html中 包含了此页面的初始化状态。这就是“注水”的过程,将数据注入到html里。
客户端就可以从window.REDUX_DATA中拿到初始状态了,这就是”脱水“。