更好的 SEO:爬虫能够得到完整页面而不是一个 loading 的 HTML,更容易被被搜索引擎收录
相同的技术栈:可以在服务端使用熟悉的技术栈(Node.js: 我来力)实现服务端渲染,不用学习模版引擎、PHP 或者其他语言~~(面向上级:维护成本更低)~~
0JS 支持:在某些极端场景下客户端可能禁用了 JS,SSR 可以在这种情况下也能提供基本的页面展示(例如某洋葱网络浏览器
当然,SSR 也并不是没有缺点:
心智负担更重:由于组件会被渲染两次,一次在 Node.js 环境中,一次在浏览器环境中,开发者必须同时考虑组件在两端运行的情况,小心地保证组件是同时兼容两种运行时的;此外还有路由处理、全局状态污染、渲染动态内容 (如 i18n) 等小坑,开发需要消耗更多的注意力
带宽/性能浪费:如果不能恰当地编写服务端逻辑,就更容易造成带宽/性能的浪费,导致 SSR 的实际效果可能还不如 CSR(并且还不容易看出来):不小心在组件中引入了 node 的标准库导致产物中引入了不必要的 polyfill (例如 buffer
, crypto
)1 打包体积增大;一份数据在服务端渲染成 HTML 同时注入到 window 中在客户端再渲染一次,以「不同的形式」重复传输了两次;或者是忘记设置缓存,导致每个请求都会从服务端重新渲染
如果首屏速度或者 SEO 对网站是极其重要的,或者只是单纯想把用户体验做到最优,那么 SSR 是瑕不掩瑜的,否则请再思考一下 SSR 能够带来什么。
在 React 18 中还引入了一个新的 RFC Server Component
,Server Component 和 SSR 的共同点就是都在服务端运行,除此之外两者并不冲突,可以同时使用。一个 Server Component 写出来可能长这样
export async function MyServerComponent() {
let res = await fetch('https://example.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return <div>{res.json().message}</div>
}
export async function MyServerComponent() {
let res = await fetch('https://example.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return <div>{res.json().message}</div>
}
Server Componet 组件只在服务端渲染一次,把渲染结果传递到浏览器中,在浏览器中不会有“水合”的过程,因此用起来就是“一个返回 React 组件的一次性函数”,并且
那么 静态站点生成 呢?SSG 可以简单理解为预渲染的 SSR
如果网站的服务端渲染结果对于每个用户都是相同的,那么可以提前把所有 HTML 都渲染出来,全站进行缓存。这样的方式特别适合文档、博客等网站,对服务器压力非常小,甚至可以直接部署到 CDN 上完全不用操心运维。
现在回答文章的最开头那个 quiz:答案是可以的,ReactDOMServer 支持运行在浏览器上,因此可以在任何地方进行服务端渲染,当然也包括浏览器。
在 React SSR 中有两个概念:
我制作了一个 demo 来演示 SSR 的流程,这里的「脱水」过程就完全在浏览器中进行,虽然一般只有在服务端脱水才有意义
(可以把左侧竖条往右拖动查看源码)
在服务端,组件被脱水成为一个 HTML 字符串(如 1. 所示),这个字符串在浏览器上被渲染成 DOM (即 2. )。此时点击按钮是不会有响应的,因为脱水后的 HTML 没有交互逻辑。在 3. 中使用 ReactDOM.hydrate(<Test />, ref)
重新水合后组件才可以响应用户交互,此时点击按钮,计数就可以正常增加了。
注意到组件 render
函数实际执行了两次,一次是在服务端渲染生成 HTML 字符串,另一次是在客户端 hydrate 绑定点击事件,也就是说组件需要同时在浏览器和 Node 环境中运行。如果组件依赖了只在浏览器或 Node 上运行的代码,
ReactDOM.hydrate
会假设已有的 DOM 结构和组件 render 得到的 React Node Tree 是匹配的,这样 React 可以跳过创建 DOM 节点的过程,更快完成水合。如果 DOM 节点不匹配,React 会抛出一个错误,虽然 React 会降级到客户端渲染,但是这个问题应该要视为一个 bug 来修复。
现在你已经完全学会 SSR 了!
虽然原理很简单,但是由于服务端和客户端天然有不同的特性,许多地方要额外处理才能正常跑起来。这里有一些小建议:
let
然后在对外暴露的函数里修改状态的注意了:Server 实例是多个客户端共享的,不会像浏览器一样每个标签都创建一个单独的环境。如果模块不纯,那么「每个服务端实例」和「每个浏览器标签」中都会运行一次产生不同的副作用。尽量不要在模块中这么做,