[mini-next.js] 服务端渲染:SSR 实现
概述
服务端渲染(Server-Side Rendering, SSR)是 Next.js 的核心特性之一。它允许在服务器端将 React 组件渲染为 HTML 字符串,然后发送给客户端。本文将深入分析并实现 SSR 的核心功能。
核心概念
什么是 SSR?
传统客户端渲染(CSR):
1. 浏览器请求 HTML
2. 服务器返回空的 HTML + JS
3. 浏览器执行 JS,渲染页面
服务端渲染(SSR):
1. 浏览器请求 HTML
2. 服务器渲染 React 组件为 HTML
3. 浏览器直接显示 HTML
4. 浏览器执行 JS,进行 hydration(水合)
SSR 的优势
- SEO 友好:搜索引擎可以直接抓取 HTML 内容
- 首屏加载快:浏览器立即显示内容,无需等待 JS 执行
- 更好的性能:减少客户端计算负担
功能分析
1. React 组件渲染
输入:React 组件 + Props
输出:HTML 字符串
技术:使用 react-dom/server 的 renderToString
2. HTML 文档生成
输入:组件渲染的 HTML
输出:完整的 HTML 文档
包含:
- HTML 结构(<html>, <head>, <body>)
- 元数据(<meta>, <title>)
- 样式和脚本引用
3. Props 传递
来源:
- 动态路由参数(/posts/:id → { id: "123" })
- 查询参数(?name=value → { name: "value" })
逐步实现
步骤 1:创建渲染器模块结构
import React from 'react';
import { renderToString } from 'react-dom/server';
export function renderPage(Component, props = {}) {
// 渲染逻辑
}
export function generateHTML(body, title = 'Mini Next.js') {
// HTML 生成逻辑
}
export function renderFullPage(Component, props = {}, options = {}) {
// 完整渲染流程
}
设计说明: - 分离关注点:组件渲染、HTML 生成、完整流程 - 可复用:每个函数职责单一
步骤 2:实现组件渲染
export function renderPage(Component, props = {}) {
try {
// 渲染 React 组件为 HTML 字符串
const html = renderToString(React.createElement(Component, props));
return html;
} catch (error) {
console.error('SSR render error:', error);
throw error;
}
}
关键点:
- 使用 React.createElement 创建元素
- renderToString 将组件树转换为 HTML 字符串
- 错误处理:捕获渲染错误
示例:
// 组件
function Home({ name }) {
return <h1>Hello {name}</h1>;
}
// 渲染
const html = renderPage(Home, { name: 'World' });
// 输出: '<h1>Hello World</h1>'
步骤 3:实现 HTML 文档生成
export function generateHTML(body, title = 'Mini Next.js') {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
color: #333;
}
#__next {
min-height: 100vh;
}
</style>
</head>
<body>
<div id="__next">${body}</div>
<script>
// 客户端 hydration 脚本
// 在真实 Next.js 中,这里会加载编译后的客户端代码
console.log('Page loaded');
</script>
</body>
</html>`;
}
关键点:
- 完整的 HTML5 文档结构
- 内联样式(简化实现)
- #__next 容器(Next.js 约定)
- 客户端脚本占位符
设计说明:
- 使用模板字符串生成 HTML
- 转义 body 内容(React 已处理)
- 预留 hydration 脚本位置
步骤 4:实现完整渲染流程
export function renderFullPage(Component, props = {}, options = {}) {
const body = renderPage(Component, props);
return generateHTML(body, options.title);
}
流程: 1. 渲染组件为 HTML 2. 生成完整 HTML 文档 3. 返回最终 HTML
完整实现
import React from 'react';
import { renderToString } from 'react-dom/server';
/**
* 渲染页面组件为 HTML
*/
export function renderPage(Component, props = {}) {
try {
// 渲染 React 组件为 HTML 字符串
const html = renderToString(React.createElement(Component, props));
return html;
} catch (error) {
console.error('SSR render error:', error);
throw error;
}
}
/**
* 生成完整的 HTML 文档
*/
export function generateHTML(body, title = 'Mini Next.js') {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
color: #333;
}
#__next {
min-height: 100vh;
}
</style>
</head>
<body>
<div id="__next">${body}</div>
<script>
// 客户端 hydration 脚本
// 在真实 Next.js 中,这里会加载编译后的客户端代码
console.log('Page loaded');
</script>
</body>
</html>`;
}
/**
* 渲染完整的页面
*/
export function renderFullPage(Component, props = {}, options = {}) {
const body = renderPage(Component, props);
return generateHTML(body, options.title);
}
使用示例
在开发服务器中使用
// src/server/dev-server.js
import { renderFullPage } from '../renderer/ssr-renderer.js';
async handlePageRoute(req, res, pathname, url) {
const route = this.router.matchRoute(pathname);
// 加载组件
const Component = await this.moduleLoader.loadModule(route.filePath);
// 准备 props
const props = {
...route.params, // 动态路由参数
query: Object.fromEntries(url.searchParams), // 查询参数
};
// 渲染页面
const html = renderFullPage(Component, props);
// 返回响应
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
}
页面组件示例
// pages/about.js
import React from 'react';
export default function About({ query }) {
return (
<div>
<h1>About Page</h1>
{query.name && <p>Hello, {query.name}!</p>}
</div>
);
}
访问 /about?name=World:
- query = { name: 'World' }
- 渲染结果包含 <p>Hello, World!</p>
动态路由示例
// pages/posts/[id].js
import React from 'react';
export default function Post({ id }) {
return (
<div>
<h1>Post #{id}</h1>
<p>This is post with ID: {id}</p>
</div>
);
}
访问 /posts/123:
- id = "123"
- 渲染结果包含 <h1>Post #123</h1>
SSR 流程详解
1. 请求到达
浏览器请求: GET /about
↓
服务器接收请求
2. 路由匹配
const route = router.matchRoute('/about');
// { path: '/about', filePath: './pages/about.js', ... }
3. 组件加载
const Component = await moduleLoader.loadModule('./pages/about.js');
// Component = About 函数组件
4. Props 准备
const props = {
query: { name: 'World' } // 从 URL 查询参数提取
};
5. 组件渲染
const html = renderToString(
React.createElement(Component, props)
);
// html = '<div><h1>About Page</h1><p>Hello, World!</p></div>'
6. HTML 文档生成
const fullHTML = generateHTML(html, 'About');
// 完整的 HTML 文档,包含 <html>, <head>, <body> 等
7. 响应返回
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(fullHTML);
与真实 Next.js 的差异
已实现
- ✅ React 组件服务端渲染
- ✅ Props 传递(路由参数、查询参数)
- ✅ HTML 文档生成
未实现(简化)
- ❌ Hydration:客户端 JavaScript 激活
- ❌ 代码分割:按路由分割代码
- ❌ CSS 处理:CSS Modules、Tailwind 等
- ❌ 流式渲染:Streaming SSR
- ❌ Suspense:React Suspense 支持
总结
SSR 的核心是在服务器端将 React 组件转换为 HTML:
- 组件渲染:使用
renderToString将组件树转换为 HTML - HTML 生成:包装成完整的 HTML 文档
- Props 传递:将路由参数和查询参数传递给组件
这个实现虽然简化,但包含了 Next.js SSR 的核心逻辑。在下一篇文章中,我们将实现 App Router,它提供了更强大的嵌套布局功能。
相关文件:
- src/renderer/ssr-renderer.js - 完整实现
- examples/basic-app/pages/ - 示例页面