React SSR同构技术内幕

何为“同构”

简单来说就是“同种结构的不同表现形态”。在这里我们用更通俗的大白话来解释react同构就是:

同一份react代码在服务端执行一遍,再在客户端执行一遍。

同一份react代码,在服务端执行一遍之后【生成源数据源】,我们就可以生成相应的html,但是服务器环境不能绑定事件。所以再在客户端再执行一遍之后就可以正常响应用户的操作【事件绑定】。这样就组成了一个完整的页面。

什么是服务器端渲染

使用 React 构建客户端应用程序,默认情况下,可以在浏览器中输出 React 组件,进行生成 DOM 和操作 DOM。React 也可以在服务端通过 Node.js 转换成 HTML,直接在浏览器端“呈现”处理好的 HTML 字符串,这个过程可以被认为 “同构”,因为应用程序的大部分代码都可以在服务器和客户端上运行。

为什么使用服务器端渲染

与传统 SPA(Single Page Application - 单页应用程序)相比,服务器端渲染(SSR)的优势主要在于:

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
  • 首屏等待短,更好的用户体验,对于缓慢的网络情况或运行缓慢的设备,加载完资源浏览器直接呈现,无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的HTML。

服务端渲染的弊端

  • 由于服务端与浏览器客户端环境区别,选择一些开源库需要注意,部分库是无法在服务端执行,比如你有 document、window 等对象获取操作,都会在服务端就会报错,所以在选择的开源库要做甄别。
  • 使用服务端渲染,比如要起一个专门在服务端渲染的服务,与之前,只管客户端所需静态资源不同,你还需要 Node.js 服务端的和运维部署的知识,对你所需要掌握的知识点要求更多
  • 服务器需要更多的负载,在 Node.js 中完成渲染,由于 Node.js 的原因大量的CPU资源会被占用。

服务端渲染两种方式

(1).传统方式服务端渲染,解决用户体验和更好的 SEO,有诸多工具使用这种方式如React的(Next.js)、Vue的(Nuxt.js)等。有些工具将 webpack 运行在服务端生产环境,实时编译,将编译结果缓存起来,这都还是传统的方式,只不过将 webpack 运行在服务端实时编译,还是开发环境编译预编译好的问题。

(2).半服务器渲染,这是一种创新的方法,前端单页面应用,以前怎么玩儿,现在还怎么玩儿,多的一步是,你得先访问一个Rendora的服务,在前面拦截是否需要服务端渲染。 通常只将首屏服务器渲染,前端拿到的还是SPA。
有很多优势:

  • 方便迁移老的项目,前端和后端代码不需要更改。
  • 可能更快的性能,资源(CPU)消耗可能更少
  • 多种缓存策略
  • 已经拥有 docker 容器方案

核心原理

整体来说 react 服务端渲染原理不复杂,其中最核心的内容就是同构。
node server 接收客户端请求,得到当前的req url path,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props
、context或者store 形式传入组件,然后基于 react 内置的服务端渲染api renderToString() or renderToNodeStream() 把组件渲染为 html字符串或者 stream 流, 在把最终的 html 进行输出前需要将数据注入到浏览器端(注水),server 输出(response)后浏览器端可以得到数据(脱水),浏览器开始进行渲染和节点对比,然后执行组件的componentDidMount 完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束。
技术点确实不少,但更多的是架构和工程层面的,需要把各个知识点进行链接和整合。
这里放一个架构图

image

【长文慎入】一文吃透 React SSR 服务端渲染及同构原理

React SSR同构核心内幕从零开始,揭秘React服务端渲染核心技术

🔥🔥🔥🔥🔥🔥🔥🔥🔥超简单的react服务器渲染(ssr)入坑指南

asset-require-hook过滤掉文件

index.js 项目入口做一些预处理,使用asset-require-hook过滤掉一些类似 import logo from “./logo.svg”; 这样的资源代码。

因为我们服务端只需要纯的HTML代码,不过滤掉会报错。

1
2
3
4
5
6
7
require("asset-require-hook")({
extensions: ["svg", "css", "less", "jpg", "png", "gif"],
name: '/static/media/[name].[ext]'
});
require("babel-core/register")();
require("babel-polyfill");
require("./app");

或者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ignore.js
const ignore=()=> {
var extensions = ['.css', '.scss','.less','.png','.jpg','.gif']; //服务端渲染不加载的文件类型
for (let i = 0, len = extensions.length; i < len; i++) {
require.extensions[extensions[i]] = function () {
return false;
};
}
}
module.exports = ignore;

入口文件:
// app.js
require('./ignore.js')();

数据的 脱水 和 注水

  • 数据注水
    服务端将预取的数据注入到浏览器,使浏览器端可以访问到,客户端进行渲染前将数据传入对应的组件即可,这样就保证了props的一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
res.send(`
<!doctype html>
<html>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
<style>${[...css].join('')}</style>
<script>
window.INITIAL_STATE = ${JSON.stringify(store.getState())}
</script>
</head>
<body>
<div id="root">${content}</div>
<script src="/client/index.js"></script>
</body>
</html>
`);
  • 数据脱水
    上一步数据已经注入到了浏览器端,这一步要在客户端组件渲染前先拿到数据,并且传入组件就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
window.REDUX_STATE = <%- JSON.stringify(state) %>
</script>

...


const store = createStore(rootReducer, window.REDUX_STATE)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

SSR样式处理

内联样式
针对第一种使用内联样式,直接把样式嵌入到页面中,需要用到 css–loader和 style-loader, css-loader可以继续用,但是 style-loader由于存在一些跟浏览器相关的逻辑,所以无法在服务器端继续用了,但好在早就有了替代插件,isomorphic-style-loader,此插件用法跟 style-loader差不多,但是同时支持在服务器端使用

isomorphic-style-loader 会将导入 css文件转换成一个对象供组件使用,其中一部分属性是类名,属性的值是类对应的 css样式,所以可以直接根据这些属性在组件内引入样式,除此之外,还包括几个方法,SSR需要调用其中的 _getCss方法以获取样式字符串,传输到客户端

鉴于上述过程(即将 css样式汇总及转化为字符串)是一个通用流程,所以此插件项目内主动提供了一个用于简化此流程的 HOC组件:withStyles.js

此组件所做的事情也很简单,主要是为 isomorphic-style-loader中的两个方法:__insertCss 和 _getCss 提供了一个接口,以 Context 作为媒介,传递各个组件所引用的样式,最后在服务端和客户端进行汇总,这样一来,就能够在服务端和客户端输出样式了

🔥 renderToNodeStream ReactDOM.hydrate React SSR(服务器端渲染) 细微探究

ReactDOM.render 与 ReactDOM.hydrate 之间主要的区别就在于后者有更小的性能开销(只用于服务器端渲染),更多详细可见 hydrate

  • 它在服务器端被运行了一次,主要是通过 renderToString/renderToNodeStream生成纯净的 html元素,又在客户端运行了一次,主要是将事件等进行正确地注册,二者结合,就整合出了一个可正常交互的页面,这种服务器端和客户端运行同一套代码的操作,也称为 同构

ReactDOM.hydrate()和ReactDOM.render()的区别 从零开始,揭秘React服务端渲染核心技术

  • ReactDOM.render()会将挂载dom节点的所有子节点全部清空掉,再重新生成子节点。
  • ReactDOM.hydrate()则会复用挂载dom节点的子节点,并将其与react的virtualDom关联上。

从零开始,揭秘React服务端渲染核心技术

react-helmet

说到seo优化,有一点大家一定可以答上来,那就是在head标签里加入title标签以及两个meta标签(keywords、description)。

react服务端渲染趟坑之旅

教你如何搭建一个超完美的服务端渲染开发环境

详解React 服务端渲染方案完美的解决方案

react同构直出方案