SSR -> Server-side rendering
服务器端渲染
背景
有些项目里不得不做一些 SEO 的东西,但不想用太传统的方案。
有些时候,明明知道它并没有什么用,但就想试一试。
基于种种有用没用的需求,决定从头到尾搞一次 React 服务器端渲染,虽然市面上已经有蛮多方案了,例如 Next.js,但经历一遍才会知道有哪些坑、需要注意些什么、为什么他们要这样做等等。
React SSR 解决了什么问题
客户端使用 React 开发,大有可能已经采用了基于浏览器的前后端分离开发模式,那么,这种开发模式下,页面是如何呈现的呢。
大致经历这样几个过程:
- 浏览器向服务器发起请求,拉取静态的 HTML 文件;
- 浏览器解析静态文件,拉取需要的 CSS、JavaScript 文件(现阶段浏览器一片空白,我们可以在静态文件里面加入一个加载中的动画效果,告诉用户我们在干活了,不要着急);
- 浏览器执行 JavaScript,根据业务代码,显示一个初始化的页面,再执行当前路由对应的事件(现阶段浏览器已经有了最基本的页面框架,但是没有动态内容,此时应该有各种异步请求发起);
- 待异步请求结束后,可能会重新渲染页面,呈现完整的内容;
在执行 JavaScript 的时候,可能会因为浏览器不支持新 API 的问题,页面报错,功亏一篑。
采用 React 服务器端渲染呢,前面几个过程都相似,只是在第二步的时候,页面已经能完成的呈现了。
那么,React 服务器端渲染大概可以解决如下几个问题。
搜索引擎友好(SEO)
在做一些需要搜索引擎排名的页面事,会受到很大的阻碍。
例如页面的首页,一般我们看到的是这样的源代码,里面有很多内容可以呈现,并且内链完善。
基于浏览器前后端分离的页面看到了如下的源代码,只有简单的 <div id=root></div>
,完全不知道这个页面是什么主题、内容。
对于国内不太聪明的搜索引擎爬虫,第二个页面没有什么价值,相同关键词搜索结果排名不会靠前。
开发很爽,上线后的结果却很痛。
提高首屏渲染能力
访问客户端设备的硬件性能参差不齐,导致不同设备的用户看到不同的加载状态,可能某些比较差的设备,第一屏的内容还没有呈现出来,浏览器就挂了、无响应等。
通过 React 服务器端渲染这么一倒腾,服务器响应的内容已经是一个完整的页面,浏览器只需要一次渲染就能看到。
比较直观的对比,可以参考 北斗-打造高可靠与高性能的React同构解决方案 这里的动图对比。
服务器端渲染一个组件
浏览器端渲染一个组件
import React from 'react';
import ReactDOM from 'react-dom';
const Node = () => {
return <div>浏览器端渲染一个组件</div>;
}
ReactDOM.render(<Node />, document.getElementById('root'));
如何在服务器端渲染呢,根据 React 的 文档 提示,只需要小小改动一下就好了。
import React from 'react';
import ReactDOMServer from 'react-dom/server';
const Node = () => {
return <div>浏览器端渲染一个组件</div>;
}
const SERVER_HTML = ReactDOMServer.renderToString(Node);
console.log(SERVER_HTML);
打印 SERVER_HTML
这个变量,就会看到它就是 <div>浏览器端渲染一个组件</div>
。
搭建一个服务器端渲染的服务器
文件结构
|-- 根目录
|--|-- http.js ## Node.js 服务端启动文件
|--|-- entry-server.jsx ## React 服务器端入口文件
|--|-- app.jsx ## React 根组件
|--|-- webpack.config.js ## webpack 配置文件
webpack 配置文件 webpack.config.js
// 比较简单,其他省略了,主要是把 ES6、JSX 的代码编译,然后打包成 commonjs 的包,提供给服务端使用。
const config = {
entry: {
app: './entry-server.js'
},
output: {
path: './',
filename: 'ssr.js',
libraryExport: 'default',
libraryTarget: 'commonjs2'
}
}
React 根组件 app.jsx
import React from 'react';
const Node = () => {
return <div>浏览器端渲染一个组件</div>;
}
export default Node;
React 服务器端入口文件 entry-server.jsx
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './app';
export default renderHTML = () => {
return ReactDOMServer.renderToString(App);
};
Node.js 服务端启动文件 http.js
const express = require('express');
const SSR = require('./ssr');
const app = express();
app.get('*', (req, res) => {
const __html = SSR();
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<title>TEST SSR</title>
</head>
<body>
${__html}
</body>
</html>
`);
});
app.listen(3000, () => {
console.log('Running on http://localhost:3000/');
});
按道理说这样子就能看到效果了,以上代码是临时改造的,不能保障正常运营,过程是正确的。
加入 react-router
新增 routes.jsx 文件
import React from 'react';
import { Switch, Redirect, Route } from 'react-router-dom';
const PageHome = () => {
return <div>Home</div>
}
const PageList = () => {
return <div>List</div>
}
const Page404 = () => {
return <div>404</div>
}
export default () => {
return (
<Switch>
<Redirect exact from="/" to="/home" />
<Route exact path="/home" component={PageHome} />
<Route exact path="/list" component={PageList} />
<Route component={Page404} />
</Switch>
);
};
修改 app.jsx
import React from 'react';
import { Link } from 'react-router-dom';
import Routes from './routes';
const Node = () => {
return (
<div>
<Link to="/home">Home</Link> 
<Link to="/list">List</Link> 
<Link to="/not-find">404</Link>
<Routes />
</div>
);
}
export default Node;
修改 entry-server.jsx
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import App from './app';
export default renderHTML = (url) => {
let context = {};
const RootApp = (
<StaticRouter location={url} context={context}>
<App />
</StaticRouter>
);
if (context.url) {
// 如果需要重定向
// <Redirect exact from="/" to="/home" />
// 重定向到指定路由
return {
context
}
}
const html = ReactDOMServer.renderToString(RootApp);
return {
context,
html
}
};
修改 http.js
app.get('*', (req, res) => {
const ssr = SSR();
if(ssr.context.url) {
return res.redirect(rendered.context.url); }
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<title>TEST SSR</title>
</head>
<body>
${ssr.html}
</body>
</html>
`);
});
以上操作中,已经完成了一个最简单的静态(没有异步获取数据)应用的服务器端渲染。