渲染模式
服务器端渲染
服务器端渲染 (SSR) 是渲染网页内容的最古老方法之一。 SSR 会生成页面的完整 HTML,以响应用户的请求进行渲染。 内容可能包含来自数据存储或外部 API 的数据。
连接和获取操作在服务器端处理。 用于格式化内容的 HTML 也会在服务器端生成。 因此,使用 SSR,我们可以避免为数据获取和模板化进行额外的往返。 因此,客户端不需要渲染代码,也不需要将与之对应的 JavaScript 发送到客户端。
使用 SSR,每个请求都会被独立处理,并被服务器视为一个新的请求。 即使两个连续请求的输出差别不大,服务器也会从头开始处理和生成它。 由于服务器对多个用户都是通用的,因此处理能力由所有在给定时间处于活动状态的用户共享。
经典 SSR 实现
让我们看看如何使用经典 SSR 和 JavaScript 创建一个页面来显示当前时间。
1<!DOCTYPE html>2<html>3 <head>4 <title>Time</title>5 </head>6 <body>7 <div>8 <h1>Hello, world!</h1>9 <b>It is <div id=currentTime></div></b>10 </div>11 </body>12</html>
注意,这与提供相同输出的 CSR 代码不同。 还需要注意的是,虽然 HTML 由服务器渲染,但此处显示的时间是客户端的本地时间,由 JavaScript 函数 tick()
填充。 如果你想显示任何其他特定于服务器的数据,例如服务器时间,你需要在渲染 HTML 之前将其嵌入其中。 这意味着它不会在没有往返服务器的情况下自动刷新。
优点和缺点
在服务器上执行渲染代码并减少 JavaScript 可以提供以下优势。
更少的 JavaScript 导致更快的 FCP 和 TTI
在页面上有多个 UI 元素和应用程序逻辑的情况下,与 CSR 相比,SSR 的 JavaScript 明显更少。 因此,加载和处理脚本所需的时间更少。 FP、FCP 和 TTI 更短,并且 FCP = TTI。 使用 SSR,用户无需等待所有屏幕元素出现并变为可交互状态。
图片源:https://developers.google.com/web/updates/2019/02/rendering-on-the-web
为客户端 JavaScript 提供额外的预算
开发团队需要使用 JS 预算来限制页面上的 JS 数量,以实现所需的性能。 使用 SSR,由于你直接消除了渲染页面所需的 JS,因此它为应用程序可能需要的任何第三方 JS 创建了额外的空间。
支持 SEO
搜索引擎爬虫能够轻松地抓取 SSR 应用程序的内容,从而确保页面上的 SEO 更高。
由于上述优势,SSR 非常适合静态内容。 但是,它也有一些缺点,因此它并不适合所有场景。
缓慢的 TTFB
由于所有处理都在服务器端进行,因此在以下一种或多种情况下,服务器的响应可能会延迟
- 多个同时用户导致服务器负载过重。
- 网络速度慢
- 服务器代码未优化。
一些交互需要完全重新加载页面
由于客户端没有所有代码,因此需要频繁地往返服务器以完成所有关键操作,导致页面完全重新加载。 这可能会增加交互之间的时间,因为用户需要在操作之间等待更长时间。 因此,使用 SSR 无法实现单页应用程序。
为了解决这些缺点,现代框架和库允许在同一个应用程序中对服务器和客户端进行渲染。 我们将在后面的章节中详细介绍这些内容。 首先,让我们看一下使用 Next.js 的更简单的 SSR 形式。
使用 Next.js 的 SSR
Next.js 框架也支持 SSR。 这会在每个请求时预先在服务器上渲染页面。 可以通过从页面中导出一个名为 getServerSideProps()
的异步函数来实现。
export async function getServerSideProps(context) {
return {
props: {}, // will be passed to the page component as props
};
}
context 对象包含 HTTP 请求和响应对象、路由参数、查询字符串、语言环境等的键。
以下实现展示了 getServerSideProps()
用于在使用 React 格式化的页面上渲染数据。 完整的实现可以在 此处 找到。
1// data fetched from an external data source using `getServerSideProps`23const Users = ({ users, error }) => {4 return (5 <section>6 <header>7 <h1>List of users</h1>8 </header>9 {error && <div>There was an error.</div>}10 {!error && users && (11 <table>12 <thead>13 <tr>14 <th>Username</th>15 <th>Email</th>16 <th>Name</th>17 </tr>18 </thead>19 <tbody>20 {users.map((user, key) => (21 <tr key={key}>22 <td>{user.username}</td>23 <td>{user.email}</td>24 <td>{user.name}</td>25 </tr>26 ))}27 </tbody>28 </table>29 )}30 </section>31 );32};3334export async function getServerSideProps() {35 // Fetch data from external API36 const res = await fetch("https://jsonplaceholder.typicode.com/users")37 const data = await res.json();3839 // Pass data to the page via props40 return { props: { data } }41}4243export default Users;
React for the Server
React 可以进行同构渲染,这意味着它可以在浏览器和其他平台(如服务器)上运行。 因此,UI 元素可以使用 React 在服务器端渲染。
React 也可以与通用代码一起使用,通用代码允许相同的代码在多个环境中运行。 这是通过在服务器上使用 Node.js 或称为 Node 服务器来实现的。 因此,通用 JavaScript 可以用来在服务器端获取数据,然后使用同构 React 进行渲染。
让我们看看使这一切成为可能的 React 函数。
ReactDOMServer.renderToString(element);
此函数返回一个与 React 元素相对应的 HTML 字符串。 然后可以将 HTML 渲染到客户端,以便更快地加载页面。
renderToString()
函数可以与 ReactDOM.hydrate()
一起使用。 这将确保渲染的 HTML 在客户端上被原样保留,并且只有事件处理程序在加载后附加。
为了实现这一点,我们在客户端和服务器上使用一个 .js
文件,对应于每个页面。 服务器上的 .js
文件将渲染 HTML 内容,而客户端上的 .js
文件将对其进行水合。
假设你有一个名为 App
的 React 元素,其中包含要在通用 app.js
文件中定义的要渲染的 HTML。 服务器和客户端的 React 都可以识别 App
元素。
服务器上的 ipage.js
文件可以包含以下代码
app.get("/", (req, res) => {
const app = ReactDOMServer.renderToString(<App />);
});
现在,可以使用常量 App
来生成要渲染的 HTML。 客户端的 ipage.js 将包含以下内容,以确保元素 App
被水合。
ReactDOM.hydrate(<App />, document.getElementById("root"));
有关使用 React 进行 SSR 的完整示例,请参见 此处。