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}
`
); }

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' },
	});
}

你可能感兴趣的:(ssr,javascript,前端,开发语言)