一、背景介绍
⻚⾯打开过程
服务端渲染SSR是什么
1.客户端渲染CSR
现代化的前端项目,大部分都是单页应用程序,也就是我们说的 SPA ,整个应用只有一个页面,通过组件的方式,展示不同的页面内容,所有的数据通过请求服务器获取后,在进行客户端的拼装和展示;这就是目前前端框架的默认渲染逻辑,我们称为:客户端渲染方案( Client Side Render 简称: CSR );
加载渲染过程如下: HTML/CSS 代码 --> 加载 JavaScript 代码 --> 执行 JavaScript 代码 --> 渲染页面数据
SPA 应用的客户端渲染方式,最大的问题有两个方面:
1:白屏时间过长,用户体验不好;
2:HTML 中无内容,SEO 不友好;
2.服务端渲染SSR
SSR 的全称是Server Side Rendering,对应的中文名称是:服务端渲染,也就是将渲染的工作放在服务端进行。
加载渲染过程如下: HTML + CSS + JS + Data -> 渲染后的 HTML
3.客户端渲染 vs 服务端渲染
总结:服务端渲染 (SSR) 的核⼼是减少请求
为什么要用服务端渲染
1.加快首屏渲染,减少白屏时间
与传统的web项目直接获取服务端渲染好的HTML不同,单页面应用使用JavaScript在脚本客户端生成HTML来呈现内容,用户需要等待JS解析执行完成后才能看到页面,这就使得白屏加载时间变长,影响用户体验。
2.SEO 友好
对于单页面应用,当搜索引擎的爬虫爬取网站HTMl文件时,通常情况下单页面应用中没有任何的内容,仅有<div id="root"> </div>这么一句话,从而影响排名。
因此,业界借鉴传统的服务端渲染的方案,提出在服务端执行前端框架(React/Vue/Angular)代码生成HTML,然后将渲染好的HTML直接返回给客户端。
二、如何做
传统多页面实现
1.工程化打包
目录结构
在React中实现SSR主要有两种形式
1.手动搭建一个 SSR 框架(React + SSR + express构建demo)
目录结构
实现思路
服务端
- 使⽤ react-dom/server 的 renderToString ⽅法将React 组件渲染成字符串
- 服务端路由返回对应的模板
客户端
- 打包出针对服务端的组件
首选写一下server端 renderToString 将组件转为字符串 renderMarkup方法 模板包装成html
if (typeof window === 'undefined') {
global.window = {};
}
const fs = require('fs');
const path = require('path');
const express = require('express');
const { renderToString } = require('react-dom/server');
const SSR = require('../dist/search-server');
const template = fs.readFileSync(path.join(__dirname, '../dist/search.html'), 'utf-8');
const data = require('./data.json');
const server = (port) => {
const app = express();
app.use(express.static('dist'));
app.get('/search', (req, res) => {
const html = renderMarkup(renderToString(SSR));
res.status(200).send(html);
});
app.listen(port, () => {
console.log('Server is running on port:' + port);
});
};
server(process.env.PORT || 3000);
const renderMarkup = (str) => {
const dataStr = JSON.stringify(data);
return template.replace('<!--HTML_PLACEHOLDER-->', str)
.replace('<!--INITIAL_DATA_PLACEHOLDER-->', `<script>window.__initial_data=${dataStr}</script>`);
}
设置占位符
<!DOCTYPE html>
<html lang="en">
<head>
${ require('raw-loader!./meta.html')}
<title>Document</title>
<script>${ require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js')}</script>
</head>
<body>
<div id="root"><!--HTML_PLACEHOLDER--></div>
<script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react.min.js"></script>
<script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react-dom.min.js"></script>
<!--INITIAL_DATA_PLACEHOLDER-->
</body>
</html>
webpack ssr 打包存在的问题
浏览器的全局变量 (Node.js 中没有 document, window)
·组件适配:将不兼容的组件根据打包环境进⾏适配
·请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios
样式问题 (Node.js ⽆法解析 css)
·⽅案⼀:服务端打包通过 ignore-loader 忽略掉 CSS 的解析
·⽅案⼆:将 style-loader 替换成 isomorphic-style-loader
如何解决样式不显示的问题?
使⽤打包出来的浏览器端 html 为模板
设置占位符,动态插⼊组件
⾸屏数据如何处理?
服务端获取数据
替换占位符
整体react服务端渲染原理并不复杂,具体如下:
node server 接收客户端请求,得到当前的请求url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props、context或者store 形式传入组件
然后基于 react 内置的服务端渲染方法 renderToString()把组件渲染为 html字符串在把最终的 html进行输出前需要将数据注入到浏览器端
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束
2.使用成熟的SSR 框架Next.JS(React + Next + Redux + Koa构建demo)
目录结构
实现流程
- 项目框架搭建;
- 项目目录重构;
- 完善框架-增加css、less解析
- 完善框架-增加UI组件antd
- _document.js加载公共样式
- 子组件覆盖Head中的title
- 头部和底部公共布局
- 项目集成Redux
- 安装react中redux相关的包
- 在reducer文件夹创建testReducer
- store文件夹新建index.js
- 改造_app.js文件利用next-redux-wrapper的withRedux初始化store
- 改造layout.js index.js home.js文件
- store自动保存到localStorage
- featch实现数据请求
- 安装isomorphic-fetch
- _app.js 全局导入
- 封装fetch
打包部署
对于线上项目,如果直接通过 node app 来启动,如果报错了可能直接停止导致整个服务崩溃,一般监控 node 有几种方案。
- supervisor: 一般用作开发环境的使用。
- forever: 管理多个站点,一般每个站点的访问量不大的情况,不需要监控。
- PM2: 网站的访问量比较大,需要完整的监控页面。
PM2配置文件
module.exports = {
apps: [
{
name: 'mi',
script: './server.js', // start script
cwd: './', // current workspace
watch: [
// watch directorys and restart when they change
'.next'
],
ignore_watch: [
// ignore watch
'node_modules',
'logs',
'static'
],
instances: 2, // start 2 instances
node_args: '--harmony',
env: {
NODE_ENV: 'production',
PORT: 5999
},
out_file: './logs/out.log', // normal log
error_file: './logs/err.log', // error log
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm Z' // date format
}
]
};
启动
pm2 start pm2.config.js --env production