SSR同构渲染深度解析
同构渲染(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><body><div id="root">${html}</div><script>window.__INITIAL_PROPS__ = ${JSON.stringify(props)};</script></body></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('<!DOCTYPE html><html><head><title>流式渲染</title></head><body><div id="root">');const stream = renderToNodeStream(<App location={rerq.url} />);stream.pipe(res, {end: false });stream.on('end', () => {res.write('</div><script src="/client.js"></script></body></html>');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(`<html><head>${styleTags}</head><body><div id="root">${html}</div></body></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); // 页面请求执行SSRreturn new Response(html, {headers: { 'Content-Type': 'text/html' },});
}async function renderApp(request) {// 执行 React SSRconst stream = await renderToReadableStream(<App url={request.url} />);return new Response(stream, {headers: { 'Content-Type': 'text/html' },});
}