同构渲染(Isomorphic Rendering)是SSR(服务器端渲染)的核心概念,指同一套代码既能在服务器端运行,也能在客户端运行。下面我将从原理到实践全面介绍SSR同构渲染。
1. 基本工作流程
1. 用户请求
2. 服务器执行React/Vue渲染
3. 返回完整HTML
4. 浏览器加载JS
5. JS“接管”页面(Hydration)
6. 后续交互由前端框架处理
2. 关键机制对比
机制 | 服务器端 | 客户端 |
---|---|---|
渲染目标 | 生成完整HTML | DOM更新 |
数据获取 | 直接调用API | 通过fetch/XHR获取 |
生命周期 | 只执行到componentDidMount前 | 完整生命周期 |
路由处理 | 静态路由匹配 | 动态路由导航 |
1. React同构示例
// shared/App.js - 同构组件
import React from 'react';
const App = ({ serverData }) => (
<div>
<h1>同构应用</h1>
<p>服务器数据:{serverData}</p>
</div>
);
export default App;
// server/render.js - 服务器渲染
import { renderToString } from 'react-dom/server';
import App from '../shared/App';
const html = renderToString(<App serverData="从API获取的数据" />);
// client/hydrate.js - 客户端注水
import { hydrate } from 'react-dom';
import App from '../shared/App';
hydrate(<App serverData={window.__INITIAL_DATA__} />, document.getElementById('root'));
2. Vue同构示例
// shared/App.vue
<template>
<div>
<h1>同构应用</h1>
<p>服务器数据:{{ serverData }} </p>
</div>
</template>
<script>
export default {
props: ['serverData']
}
</script>
// server/entry-server.js
import { renderToString } from '@vue/serrver-renderer';
import { createApp } from './app';
export async function render(url) {
const { app } = createApp();
const html = await renderToString(app);
return html;
}
// client/entry-client.js
import { createApp } from './app';
const { app } = createApp();
app.mount('#app');
1. 数据预取方案
// 定义静态数据需求方法
class PostPage extends React.Component {
static async getInitialProps({ req }) {
const res = await fetch(`https://api.example.com/posts/${req.params.id}`);
return { post: await res.json() };
}
render() {
return <article>{this.props.post.content}</article>;
}
}
// 服务器端处理
async function handleRender(req, res) {
const props = await PostPage.getInitialProps({ req });
const html = renderToString(<PostPage {...props} />);
// 将数据注入到HTML中
res.send(`
${html}
`);
}
2. 状态同构方案(Redux)
// shared/store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
export function createServerStore(initialState){
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk)
);
}
// server/render.js
import { Provider } from 'react-redux';
import { createServerStore } from '../shared/store';
async function renderApp(req) {
const store = createServerStore();
await store.dispatch(fetchData(req.url)); // 预取数据
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
);
return {
html,
state: store.getState()
}
}
// client/hydrate.js
import { createClientStore } from '../shared/store';
const store = createClientStore(window.__INITIAL_STATE__);
hydrate(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
1. React Router同构实现
// shared/routes.js
import { StaticRouter, BrowserRouter } from 'react-router-dom';
// 服务器端使用StaticRouter
export function ServerRouter({ url, children }) {
return <StaticRouter location={url}>{children}</StaticRouter>
}
// 客户端使用BrowserRouter
export function ClientRouter({ children }) {
return <BrowserRouter>{children}</BrowserRouter>
}
// server/render.js
import { ServerRouter } from '../shared/routes';
function renderApp(req) {
const html = renderToString(
<ServerRouter url={req.url}>
<App />
</ServerRouter>
);
return html;
}
// client/hydrate.js
import { ClientRouter } from '../shared/routes';
hydrate(
<ClientRouter>
<App />
</ClientRouter>,
document.getElementById('root')
);
2. Vue Router同构实现
// shared/router.js
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router';
export function createVueRouter(isServer) {
const history = isServer
? createMemoryHistory()
: createWebHistory();
return createRouter({
history,
routes: [/* 路由配置 */]
});
}
// server/entry-server.js
import { createVueRouter } from '../shared/router';
export async function render(url) {
const router = createVueRouter(true);
await router.push(url);
await router.isReady();
const app = createApp({ router });
const html = await renderToString(app);
return { html };
}
// client/entry-client.js
import { createVueRouter } from '../shared/router';
const router = createVueRouter(false);
const app = createApp({ router });
app.mount('#app');
1. 组件级缓存
// React组件缓存装饰器
function cachable(Component) {
const cache = new Map()
return class CachedComponent extends React.Component {
static async getInitialProps(ctx) {
const cacheKey = JSON.stringify(ctx.req.url);
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const props = await Component.getInitialProps(ctx);
cache.set(cacheKey, props);
return props;
}
render() {
return <Component {...this.props} />
}
};
}
@cachable
class ExpensiveComponent extends React.Component {
// ...
}
2. 流式渲染
// React流式渲染示例
import { renderToNodeStream } from 'react-dom/server';
app.get('/', (req, res) => {
res.write('流式渲染 ');
const stream = renderToNodeStream(
<App location={rerq.url} />
);
stream.pipe(res, {end: false });
stream.on('end', () => {
res.write('');
res.end();
})
})
3. 渐进式注水
// 使用React.lazy和Suspense实现渐进式注水
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>关键内容</h1>
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
// 客户端注水时优先处理关键内容
hydrateRoot(document.getElementById('root'), <App />, {
onRecoverableError(error) {
console.log('可恢复错误:', error);
}
})
1. 全局变量问题
// 安全使用window/document的方案
const canUseDOM = typeof window !== 'undefined' && typeof window.document !== 'undefined';
function getDocument() {
return canUseDOM ? document : null;
}
// 使用
const doc = getDocument();
if (doc) {
// 客户端特有操作
doc.title = '同构应用';
}
2. 样式处理方案
// CSS Modules同构处理
import styles from './App.module.css';
function App() {
return (
<div className={styles.container}>
{/* 内容 */}
</div>
);
}
// 服务器端收集样式
import { ServerStyleSheet } from 'styled-components';
const sheet = new ServerStyleSheet();
const html = renderToString(sheet.collectStyled(<App />));
const styleTags = sheet.getStyleTags();
// 注入到HTML
res.send(`
${styleTags}
${html}
`);
3. 第三方库兼容性
// 动态导入浏览器特有库
function loadBrowserLibrary() {
if (typeof window === 'undefined') {
return Promise.resolve(null); // 服务器端返回空
}
return import('browser-only-library').then(mod => mod.default);
}
// 使用
loadBrowserLibrary().then(lib => {
if (lib) {
// 客户端特有逻辑
lib.init();
}
})
1. 渲染一致性测试
// 使用Jest测试同构渲染
describe('同构渲染测试', () => {
let serverHTML, clientHTML;
beforeAll(async () => {
// 模拟服务器渲染
serverHTML = renderToString(<App />);
// 模拟客户端渲染
const container = document.createElement('div');
document.body.appendChild(container);
render(<App />, container);
clientHTML = container.innerHTML;
});
it('服务器和客户端渲染结果应该匹配', () => {
// 简化比较,忽略data-reactid等属性
const cleanServer = serverHTML.replace(/ data-[^=]+="[^"]*"/g, '');
const cleanClient = clientHTML.replace(/ data-[^=]+="[^"]*"/g, '');
expect(cleanServer).toEqual(cleanClient);
});
})
2. 性能基准测试
// 使用 benchmark.js 测试渲染性能
const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;
suite
.add('服务器端渲染', {
defer: true,
fn: deferred => {
renderToString(<App />, () => deferred.resolve());
}
})
.add('客户端渲染', {
fn: () => {
const container = document.createElement('div');
render(<App />, container);
}
})
.on('cycle', event => {
console.log(String(event.target));
})
.run();
1. 微前端同构
// 使用Module Federation实现同构微前端
// shell-app/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
remotes: {
remoteApp: isServer
? 'remoteApp@http://localhost:3001/server/remoteEntry.js'
: 'remoteApp@http://localhost:3001/client/remoteEntry.js'
}
})
]
};
// 动态加载远程组件
const RemoteComponent = React.lazy(() => import('remoteApp/Component'));
function App() {
return (
<Suspense fallback="加载中...">
<RemoteComponent />
</Suspense>
)
}
2. 边缘同构渲染
// Cloudflare Workers 同构渲染示例
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const html = url.pathname.startsWith('/_next')
? await fetchFromOrigin(request) // 静态资源直接回源
: await renderApp(request); // 页面请求执行SSR
return new Response(html, {
headers: { 'Content-Type': 'text/html' },
});
}
async function renderApp(request) {
// 执行 React SSR
const stream = await renderToReadableStream(<App url={request.url} />);
return new Response(stream, {
headers: { 'Content-Type': 'text/html' },
});
}