Web 開発の非常に初期の頃、人々はまだ JSP の古いテンプレート構文を使用してフロントエンド ページを作成し、その後 JSP ファイルをサーバーに直接置き、サーバー上のデータを入力して完全なページ コンテンツをレンダリングしていました。 , you can 当時の慣行は自然なサーバーサイドレンダリングだったと言われています。しかし、AJAX テクノロジーの成熟とさまざまなフロントエンド フレームワーク (Vue や React など) の台頭により、フロントエンドとバックエンドを分離する開発モードが徐々に標準になってきました。ページの UI とロジックの開発はサーバーが行いますが、サーバーはデータ インターフェイスの提供のみを担当します。この開発方法でのページ レンダリングは、クライアント サイド レンダリング (Client Side Render、CSR と呼ばれます) とも呼ばれます。
しかし、クライアントサイドレンダリングには、最初の画面の読み込みが遅い、SEOが不親切などの問題もあり、時代の要請に応じてサーバーサイドレンダリング技術であるSSR(Server Side Render)が登場しました。 CSR技術スタックを保持し、CSRのさまざまな課題を解決することも可能です。
1. SSRの基本的な考え方
まず、CSR の問題点を分析してみると、CSR の HTML 製品構造は一般的に次のようになります。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link rel="stylesheet" href="xxx.css" />
</head>
<body>
<!-- 一开始没有页面内容 -->
<div id="root"></div>
<!-- 通过 JS 执行来渲染页面 -->
<script src="xxx.chunk.js"></script>
</body>
</html>
次に、ブラウザのレンダリング プロセスを簡単に説明します。次の簡単な模式図です。
ブラウザーが上記の HTML コンテンツを取得した場合、実際には完全なページ コンテンツをレンダリングすることはできません。これは、この時点では基本的に本文には空の div ノードしかなく、実際のページ コンテンツは埋められていないためです。次に、ブラウザは JS コードのダウンロードと実行を開始します。完全なページは、フレームの初期化、データ要求、DOM 挿入、その他の操作の後でのみレンダリングできます。つまり、CSR 内の完全なページ コンテンツは、基本的に JS コードの実行後にレンダリングされます。これにより、次の 2 つの点で問題が発生します。
- 最初の画面の読み込み速度は比較的遅いです。最初の画面の読み込みは JS の実行に依存します。JS のダウンロードと実行は、特にネットワークが貧弱な場合やパフォーマンスに敏感なローエンド マシンを使用する一部のシナリオでは、非常に時間のかかる操作になる可能性があります。
- SEO (検索エンジン最適化) には適していません。ページの HTML には特定のページ コンテンツがないため、検索エンジンのクローラーはキーワード情報を取得できず、Web サイトのランキングに影響します。
では、SSR はこれらの問題をどのように解決するのでしょうか? まず、SSR シナリオでは、サーバーは完全な HTML コンテンツを生成し、それをブラウザに直接返します。ブラウザは、JS の読み込みに依存せずに、HTML に従って完全なファースト スクリーン コンテンツをレンダリングできます。最初の画面のレンダリング時間を短縮し、その一方で、検索エンジンのクローラーに対してページのコンテンツ全体を表示することもできるため、SEO に役立ちます。
もちろん、SSR はページのコンテンツと構造を生成することしかできず、イベント バインディングを完了することはできません。そのため、イベント バインディングを完了してページをインタラクティブにするためには、CSR の JS スクリプトをブラウザ上で実行する必要があります。水和物(水注入または活性化と訳される)。同時に、サーバーサイドレンダリング + クライアントサイドハイドレートを使用するアプリケーションは、同型アプリケーションとも呼ばれます。
2. SSRのライフサイクル
SSR はサーバー側で事前に完全な HTML コンテンツをレンダリングすると言いましたが、これはどのように機能するのでしょうか?
まず、フロントエンド コードがコンパイルされてサーバー上に配置された後、正常に実行できることを確認する必要があります。次に、フロントエンド コンポーネントをサーバー上でレンダリングして、アプリケーションの HTML を生成およびアセンブルする必要があります。これには、SSR アプリケーションの 2 つの主要なライフ サイクル (ビルド時間と実行時間) が関係します。これを慎重に分類したほうがよいでしょう。
ビルド時間
SSR の構築フェーズでは主に次のことを行います。
-
モジュールの読み込みの問題。本来の構築プロセスに加えてSSRの構築プロセスを追加する必要があり、具体的にはNode.jsで正常に読み込めるようにCommonJS形式でプロダクトを生成する必要があります。もちろん、ESM に対する Node.js 自体のサポートがますます成熟するにつれて、フロントエンド ESM 形式のコードを再利用することもできます。これは、開発段階での Vite の SSR 構築の考え方でもあります。
-
スタイル コードのインポートを削除します。Node.js は CSS のコンテンツを解析できないため、CSS 行を直接インポートすることはサーバー側では実際には不可能です。例外は、次のような CSS モジュールの場合です。
import styles from './index.module.css'
//styles 是一个对象,如{ "container": "xxx" },而不是 CSS 代码
console.log(styles)
3. 外在化(外部化)に頼る。一部のサードパーティ依存関係については、ビルドされたバージョンを使用する必要はありませんが、react-dom などの node_modules から直接読み取るため、これらの依存関係は SSR ビルド プロセス中に構築されず、その結果、構築が大幅に高速化されます。 SSR。
ランタイム
SSR のランタイムは、通常、比較的固定されたライフ サイクル ステージに分割できます。簡単に言うと、次のコア ステージに編成できます。
これらの段階については、以下で詳しく説明します。
- SSRエントリーモジュールをロードします。この段階では、SSR ビルド製品のエントリ、つまりコンポーネントのエントリがどこにあるかを判断し、対応するモジュールをロードする必要があります。
- データのプリフェッチを実行します。このとき、Node 側はデータベースまたはネットワーク リクエストにクエリを実行して、アプリケーションに必要なデータを取得します。
- コンポーネントをレンダリングします。このステージは SSR の中核であり、主にステップ 1 でロードされたコンポーネントを HTML 文字列またはストリームにレンダリングします。
- HTML スプライシング。コンポーネントがレンダリングされた後、完全な HTML 文字列を連結し、それを応答としてブラウザーに返す必要があります。
SSR はビルドとランタイムの連携によってのみ実現できることがわかります。つまり、ビルド ツールだけでは十分ではありません。したがって、Vite プラグインの開発では SSR 機能を厳密に実装できません。いくつかの変更を加える必要があります。 Vite のビルド プロセスにいくつかの全体的な調整とサーバー側のランタイム ロジックの追加を実現できます。
3. ViteをベースにしたSSRプロジェクトの構築
3.1 SSRビルドAPI
Vite は構築ツールとして SSR 構築をどのようにサポートしますか? 言い換えれば、フロントエンド コードを Node.js で正常に実行するにはどうすればよいでしょうか?
ここでは 2 つのケースを説明します。開発環境では、Vite は依然として ESM モジュールのオンデマンド読み込み、つまりバンドルなしの概念を堅持しており、ssrLoadModule API を外部から提供しています。エントリ ファイルのパスを ssrLoadModule に渡すことができます。プロジェクトをパッケージ化する必要があります:
// 加载服务端入口模块
const xxx = await vite.ssrLoadModule('/src/entry-server.tsx')
運用環境では、Vite はデフォルトでパッケージ化され、SSR 構築用に CommonJS 形式でプロダクトを出力します。同様のビルド手順を package.json に追加できます。
{
"build:ssr": "vite build --ssr 服务端入口路径"
}
このようにして、Vite は SSR 専用のビルド製品をパッケージ化します。Vite は、SSR 構築におけるほとんどのことに対してすぐに使えるソリューションを提供してくれたことがわかります。
3.2 プロジェクトの構築
次に、SSR プロジェクトの構築を正式に開始します。スキャフォールディングを通じて React+ts プロジェクトを初期化できます。コマンドは次のとおりです。
npm init vite
npm i
プロジェクトを開き、プロジェクトに付属する src/main.ts を削除し、src ディレクトリに 2 つのエントリ ファイル、entry-client.tsx およびentry-server.tsx を作成します。このうち、entry-client.tsxのコードは以下のとおりです。
// entry-client.ts
// 客户端入口文件
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
ReactDOM.hydrate(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
entry-server.ts のコードは次のとおりです。
// entry-server.ts
// 导出 SSR 组件入口
import App from "./App";
import './index.css'
function ServerEntry(props: any) {
return (
<App/>
);
}
export { ServerEntry };
次に、Express フレームワークを例として Node バックエンド サービスを実装します。後続の SSR ロジックはこのサービスに接続されます。もちろん、次の依存関係をインストールする必要があります。
npm i express -S
npm i @types/express -D
次に、src ディレクトリに新しい ssr-server/index.ts ファイルを作成します。コードは次のとおりです。
// src/ssr-server/index.ts
// 后端服务
import express from 'express';
async function createServer() {
const app = express();
app.listen(3000, () => {
console.log('Node 服务器已启动~')
console.log('http://localhost:3000');
});
}
createServer();
次に、次のスクリプトを package.json に追加します。
{
"scripts": {
// 开发阶段启动 SSR 的后端服务
"dev": "nodemon --watch src/ssr-server --exec 'esno src/ssr-server/index.ts'",
// 打包客户端产物和 SSR 产物
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
// 生产环境预览 SSR 效果
"preview": "NODE_ENV=production esno src/ssr-server/index.ts"
},
}
このうち、プロジェクトには 2 つの追加ツールが必要です。説明します。
- nodemon: ファイルの変更を監視し、ノード サービスを自動的に再起動するツール。
- esno: ts-node に似たツール。ts ファイルの実行に使用されます。最下層は Esbuild に基づいています。
まず、これら 2 つのプラグインをインストールしましょう。
npm i esno nodemon -D
これで、基本的なプロジェクトのスケルトンが構築されたので、後は SSR ランタイムの実装ロジックに注目するだけです。
3.3 SSR ランタイムの実装
特別なバックエンド サービスとして、SSR をミドルウェア形式にカプセル化することができ、後で使用するのに非常に便利です。コードは次のとおりです。
import express, { RequestHandler, Express } from 'express';
import { ViteDevServer } from 'vite';
const isProd = process.env.NODE_ENV === 'production';
const cwd = process.cwd();
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
let vite: ViteDevServer | null = null;
if (!isProd) {
vite = await (await import('vite')).createServer({
root: process.cwd(),
server: {
middlewareMode: 'ssr',
}
})
// 注册 Vite Middlewares
// 主要用来处理客户端资源
app.use(vite.middlewares);
}
return async (req, res, next) => {
// SSR 的逻辑
// 1. 加载服务端入口模块
// 2. 数据预取
// 3. 「核心」渲染组件
// 4. 拼接 HTML,返回响应
};
}
async function createServer() {
const app = express();
// 加入 Vite SSR 中间件
app.use(await createSsrMiddleware(app));
app.listen(3000, () => {
console.log('Node 服务器已启动~')
console.log('http://localhost:3000');
});
}
createServer();
次に、ミドルウェアでの SSR の論理実装に焦点を当てます。まず最初のステップは、サーバー エントリ モジュールをロードすることです。コードは次のとおりです。
async function loadSsrEntryModule(vite: ViteDevServer | null) {
// 生产模式下直接 require 打包后的产物
if (isProd) {
const entryPath = path.join(cwd, 'dist/server/entry-server.js');
return require(entryPath);
}
// 开发环境下通过 no-bundle 方式加载
else {
const entryPath = path.join(cwd, 'src/entry-server.tsx');
return vite!.ssrLoadModule(entryPath);
}
}
このうち、ミドルウェア内のロジックは次のとおりです。
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
// 省略前面的代码
return async (req, res, next) => {
const url = req.originalUrl;
// 1. 服务端入口加载
const { ServerEntry } = await loadSsrEntryModule(vite);
// ...
}
}
次に、サーバー側でデータのプリフェッチ操作を実装しましょう。entry-server.tsx にデータを取得する簡単な関数を追加できます。コードは次のとおりです。
export async function fetchData() {
return { user: 'xxx' }
}
次に、SSR ミドルウェアでデータのプリフェッチ操作を完了できます。
// src/ssr-server/index.ts
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
// 省略前面的代码
return async (req, res, next) => {
const url = req.originalUrl;
// 1. 服务端入口加载
const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);
// 2. 预取数据
const data = await fetchData();
}
}
次に、コア コンポーネントのレンダリング段階に入ります。
// src/ssr-server/index.ts
import { renderToString } from 'react-dom/server';
import React from 'react';
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
// 省略前面的代码
return async (req, res, next) => {
const url = req.originalUrl;
// 1. 服务端入口加载
const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);
// 2. 预取数据
const data = await fetchData();
// 3. 组件渲染 -> 字符串
const appHtml = renderToString(React.createElement(ServerEntry, { data }));
}
}
最初のステップの後でエントリ コンポーネントを取得したので、フロントエンド フレームワークの renderToStringAPI を呼び出してコンポーネントを文字列としてレンダリングできるようになり、コンポーネントの特定のコンテンツが生成されます。次に、コンテンツの置換を容易にするために、ルート ディレクトリの下の HTML に対応するスロットを提供する必要もあります。
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="root"><!-- SSR_APP --></div>
<script type="module" src="/src/entry-client.tsx"></script>
<!-- SSR_DATA -->
</body>
</html>
次に、SSR ミドルウェアの HTML スプライシングのロジックを補足します。
// src/ssr-server/index.ts
function resolveTemplatePath() {
return isProd ?
path.join(cwd, 'dist/client/index.html') :
path.join(cwd, 'index.html');
}
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
// 省略之前的代码
return async (req, res, next) => {
const url = req.originalUrl;
// 省略前面的步骤
// 4. 拼接完整 HTML 字符串,返回客户端
const templatePath = resolveTemplatePath();
let template = await fs.readFileSync(templatePath, 'utf-8');
// 开发模式下需要注入 HMR、环境变量相关的代码,因此需要调用 vite.transformIndexHtml
if (!isProd && vite) {
template = await vite.transformIndexHtml(url, template);
}
const html = template
.replace('<!-- SSR_APP -->', appHtml)
// 注入数据标签,用于客户端 hydrate
.replace(
'<!-- SSR_DATA -->',
`<script>window.__SSR_DATA__=${JSON.stringify(data)}</script>`
);
res.status(200).setHeader('Content-Type', 'text/html').end(html);
}
}
HTML を結合するロジックでは、ページの特定のコンテンツを追加するだけでなく、グローバル データをマウントするスクリプト タグも挿入します。これは何のためにあるのでしょうか?
SSR の基本概念で、ページのインタラクティブ機能をアクティブ化するには、CSR の JavaScript コードを実行してハイドレート操作を実行する必要があると述べました。また、クライアントがハイドレートするときに、プリフェッチされたデータを同期する必要があります。サーバーは、ページのレンダリング結果がサーバー側のレンダリングと一致していることを確認するため、挿入したばかりのデータ スクリプト タグが役に立ちます。サーバーによってプリフェッチされたデータはグローバル ウィンドウにマウントされるため、クライアント レンダリング エントリであるentry-client.tsxでこのデータを取得し、ハイドレートできます。
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
// @ts-ignore
const data = window.__SSR_DATA__;
ReactDOM.hydrate(
<React.StrictMode>
<App data={data}/>
</React.StrictMode>,
document.getElementById('root')
)
これで、基本的に SSR コアのロジックを開発したので、npm run dev コマンドを実行してプロジェクトを開始します。
ブラウザを開いてページのソース コードを表示すると、次の図に示すように、SSR によって生成された HTML が正常に返されたことがわかります。
3.4 生産環境における CSR リソースの処理
ここで npm run build と npm runreview を実行して運用環境をプレビューすると、SSR は正常にコンテンツを返すことができますが、すべての静的リソースと CSR コードが無効であることがわかります。
ただし、開発段階では、Vite Dev Server のミドルウェアが開発段階での静的リソースの処理にすでに役立っており、運用環境のすべてのリソースがパッケージ化されているため、そのような問題はありません。これらのリソースをホストするための別の静的リソース サービス。
この種の問題に対しては、serve-static ミドルウェアを使用してこのサービスを完成させることができます。まず、対応するサードパーティ パッケージを驚愕させてインストールします。
npm i serve-static -S
次に、サーバーにアクセスして登録します。
// 过滤出页面请求
function matchPageUrl(url: string) {
if (url === '/') {
return true;
}
return false;
}
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
return async (req, res, next) => {
try {
const url = req.originalUrl;
if (!matchPageUrl(url)) {
// 走静态资源的处理
return await next();
}
// SSR 的逻辑省略
} catch(e: any) {
vite?.ssrFixStacktrace(e);
console.error(e);
res.status(500).end(e.message);
}
}
}
async function createServer() {
const app = express();
// 加入 Vite SSR 中间件
app.use(await createSsrMiddleware(app));
// 注册中间件,生产环境端处理客户端资源
if (isProd) {
app.use(serve(path.join(cwd, 'dist/client')))
}
// 省略其它代码
}
このようにして、実稼働環境における静的リソース障害の問題を解決しました。ただし、通常の状況では、静的リソースを CDN にアップロードし、Vite のベースをドメイン名プレフィックスとして構成することで、サーバー側の処理を追加せずに CDN 経由で静的リソースに直接アクセスできるようになります。
4. エンジニアリング上の問題
上記で基本的に SSR コアの構築機能とランタイム機能を実現し、最初は Vite ベースの SSR プロジェクトを実行できますが、実際のシナリオでは注意が必要なエンジニアリング上の問題がまだ多くあります。
4.1 ルーティング管理
SPA シナリオでは、通常、Vue の vue-router や React の react-router など、フロントエンド フレームワークごとに異なるルーティング管理ソリューションが存在します。しかし、最終的には、SSR プロセスでルーティング スキームによって実行される機能は似ています。
- 現時点でどのルートをレンダリングするかをフレームワークに指示します。Vue では、router.push を使用してレンダリングするルートを決定でき、React では、StaticRouter を location パラメータとともに使用して完了します。
- ベースプレフィックスを設定します。vue-router のベース パラメーターや、react-router の StaticRouter コンポーネントのベース名など、パスのプレフィックスを指定します。
4.2 状態管理
グローバルな状態管理には、Vue の Vuex と Pinia、React の Redux と Recoil など、フレームワークごとに異なるエコロジーとソリューションがあります。各状態管理ツールの使用方法はこの記事の焦点ではありません。SSR への接続の考え方は比較的単純です。データのプリフェッチ段階では、サーバー側でストアを初期化し、非同期で取得したデータをストアに保存し、次に、データをストアから HTML ステージに転送し、それを取り出してデータ スクリプト タグに配置し、最後にクライアントがハイドレートされたときにウィンドウを通じてプリフェッチされたデータにアクセスします。
4.3 CSRの格下げ
極端な場合には、クライアント側レンダリングである CSR にフォールバックする必要があります。一般に、次のダウングレード シナリオが含まれます。
- サーバーはデータのプリフェッチに失敗したため、データを取得するにはクライアントにダウングレードする必要があります。
- サーバーには例外があり、ボトムアップ CSR テンプレートを返し、それを CSR に完全にダウングレードする必要があります。
- ローカル開発とデバッグでは、SSR をスキップして CSR のみを実行する必要がある場合があります。
最初のケースでは、クライアント エントリ ファイルにデータを再取得するためのロジックが必要であり、次の追加を行うことができます。
// entry-client.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
async function fetchData() {
// 客户端获取数据
}
async fucntion hydrate() {
let data;
if (window.__SSR_DATA__) {
data = window.__SSR_DATA__;
} else {
// 降级逻辑
data = await fetchData();
}
// 也可简化为 const data = window.__SSR_DATA__ ?? await fetchData();
ReactDOM.hydrate(
<React.StrictMode>
<App data={data}/>
</React.StrictMode>,
document.getElementById('root')
)
}
2 番目のシナリオ、つまりサーバーがエラーを実行する場合、前の SSR ミドルウェア ロジックに try/catch ロジックを追加できます。
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
return async (req, res, next) => {
try {
// SSR 的逻辑省略
} catch(e: any) {
vite?.ssrFixStacktrace(e);
console.error(e);
// 在这里返回浏览器 CSR 模板内容
}
}
}
3 番目のケースでは、?csr の url クエリ パラメーターを渡すことで SSR を強制的にスキップし、SSR ミドルウェアに次のロジックを追加できます。
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
return async (req, res, next) => {
try {
if (req.query?.csr) {
// 响应 CSR 模板内容
return;
}
// SSR 的逻辑省略
} catch(e: any) {
vite?.ssrFixStacktrace(e);
console.error(e);
}
}
}
4.4 ブラウザ API の互換性
Node.jsではブラウザ上のウィンドウやドキュメントなどのAPIは利用できないため、サーバー側でこれらのAPIを実行すると以下のエラーが報告されます。
この問題については、まず Vite 組み込み環境変数 import.meta.env.SSR を使用して SSR 環境にあるかどうかを判断し、ブラウザ API がビジネス コードのサーバー側に表示されるのを回避できます。
if (import.meta.env.SSR) {
// 服务端执行的逻辑
} else {
// 在此可以访问浏览器的 API
}
もちろん、polyfill を介してブラウザ API を Node に挿入することもできます。これにより、これらの API が正常に実行され、上記の問題が解決されます。比較的成熟したポリフィル ライブラリ jsdom を使用することをお勧めします。これは次のように使用されます。
const jsdom = require('jsdom');
const { window } = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
const { document } = window;
// 挂载到 node 全局
global.window = window;
global.document = document;
4.5 カスタムヘッド
SSRの過程でコンポーネントの内容を決めることはできますが、
コンテナの div の内容ですが、HTML の head の内容については、コンポーネントの内部状態に基づいて決定できません。ただし、React エコシステムの React-helmet と Vue エコシステムの vue-meta ライブラリは、このような問題を解決するように設計されており、コンポーネントに Head タグを直接書き込んで、コンポーネントの内部状態をコンポーネント上で取得できるようにします。サーバ側。説明するために、react-helmet の例を取り上げます。
// 前端组件逻辑
import { Helmet } from "react-helmet";
function App(props) {
const { data } = props;
return {
<div>
<Helmet>
<title>{ data.user }的页面</title>
<link rel="canonical" href="http://mysite.com/example" />
</Helmet>
</div>
}
}
// 服务端逻辑
import Helmet from 'react-helmet';
// renderToString 执行之后
const helmet = Helmet.renderStatic();
console.log("title 内容: ", helmet.title.toString());
console.log("link 内容: ", helmet.link.toString())
サービスを開始してページにアクセスすると、端末で必要な情報を印刷できることがわかります。このようにして、コンポーネントの状態に応じて Head コンテンツを決定し、HTML の結合段階でそのコンテンツをテンプレートに挿入できます。
4.6 ストリーミングレンダリング
さまざまなフロントエンド フレームワークの最下層は、コンポーネント ツリー全体がレンダリングされるのを待ってから応答するのではなく、ストリーミング レンダリングの機能を実現し、レンダリング中に応答することで、応答が事前にブラウザに到達し、改善が可能になります。最初の画面の読み込みパフォーマンス。Vue のrenderToNodeStreamとReact のrenderToNodeStream は両方ともストリーミング レンダリングの機能を実現しており、一般的な使用法は次のとおりです。
import { renderToNodeStream } from 'react-dom/server';
// 返回一个 Nodejs 的 Stream 对象
const stream = renderToNodeStream(element);
let html = ''
stream.on('data', data => {
html += data.toString()
// 发送响应
})
stream.on('end', () => {
console.log(html) // 渲染完成
// 发送响应
})
stream.on('error', err => {
// 错误处理
})
ただし、ストリーミング レンダリングは最初の画面のパフォーマンスを向上させますが、いくつかの制限ももたらします。コンポーネントの状態に関連するコンテンツを HTML で入力する必要がある場合、ストリーミング レンダリングは使用できません。例えば、react-helmet のカスタム head コンテンツの場合、コンポーネントのレンダリング時に head 情報が収集されていても、ストリーミング レンダリングでは、この時点で HTML の head 部分がブラウザに送信されており、この部分は応答内容は変更できないため、SSR 中に反応ヘルメットは失敗します。
4.7 SSRキャッシュ
SSR は典型的な CPU 負荷の高い操作であり、オンライン マシンの負荷を可能な限り軽減するために、キャッシュの設定は非常に重要です。SSR の実行中、キャッシュされたコンテンツはいくつかの部分に分割できます。
- ファイル読み取りキャッシュ。ディスク読み取り操作の繰り返しをできる限り避け、ディスク IO ごとにキャッシュされた結果をできる限り再利用します。次のコードに示すように:
function createMemoryFsRead() {
const fileContentMap = new Map();
return async (filePath) => {
const cacheResult = fileContentMap.get(filePath);
if (cacheResult) {
return cacheResult;
}
const fileContent = await fs.readFile(filePath);
fileContentMap.set(filePath, fileContent);
return fileContent;
}
}
const memoryFsRead = createMemoryFsRead();
memoryFsRead('file1');
// 直接复用缓存
memoryFsRead('file1');
- データキャッシュをプリフェッチします。リアルタイム性の低い一部のインターフェースデータについては、次回同じリクエストが来たときにデータのプリフェッチ結果を再利用するキャッシュ戦略を採用することで、データのプリフェッチ処理におけるさまざまなIO消費量も削減できます。最初の画面までの時間をある程度短縮します。
- HTML レンダリング キャッシュ。スプライスされた HTML コンテンツがキャッシュの焦点です。この部分をキャッシュできれば、次のキャッシュ ヒット後、renderToString や HTML スプライシングなどの一連の消費を保存でき、サーバーのパフォーマンス上の利点がより明白になります。 。
上記のキャッシュ コンテンツの場合、特定のキャッシュの場所は次のとおりです。
- サーバーメモリ。キャッシュをメモリ上に配置する場合は、過剰なメモリによるサービスのダウンタイムを防ぐためのキャッシュ削除メカニズムを考慮する必要があり、代表的なキャッシュ削除ソリューションとして lru-cache (LRU アルゴリズムに基づく) が挙げられます。
- Redis データベース。従来のバックエンドサーバーの設計思想によるキャッシュ処理に相当します。
- CDNサービス。ページ コンテンツを CDN サービスにキャッシュし、次回同じリクエストが来たときに、ソース サーバーのリソースを消費する代わりに、CDN 上のキャッシュされたコンテンツを使用できます。
4.8 パフォーマンスの監視
実際の SSR プロジェクトでは、SSR のオンライン パフォーマンスの問題が頻繁に発生しますが、完全なパフォーマンス監視メカニズムがなければ、問題を発見してトラブルシューティングすることは困難です。SSR パフォーマンス データには、いくつかの一般的な指標があります。
- SSR商品ロード時間
- データのプリフェッチ時間
- コンポーネントがレンダリングされるとき
- サーバーがリクエストを受信してからレスポンスが返されるまでの所要時間
- SSRキャッシュヒット
- SSR成功率、エラーログ
次のコードに示すように、perf_hooks ツールを使用してデータ収集を完了できます。
import { performance, PerformanceObserver } from 'perf_hooks';
// 初始化监听器逻辑
const perfObserver = new PerformanceObserver((items) => {
items.getEntries().forEach(entry => {
console.log('[performance]', entry.name, entry.duration.toFixed(2), 'ms');
});
performance.clearMarks();
});
perfObserver.observe({ entryTypes: ["measure"] })
// 接下来我们在 SSR 进行打点
// 以 renderToString 为例
performance.mark('render-start');
// renderToString 代码省略
performance.mark('render-end');
performance.measure('renderToString', 'render-start', 'render-end');
次に、サービスを起動してアクセスすると、RBI ログ情報が表示されます。同様に、他のステージの指標も上記の方法でパフォーマンス ログとして収集できますが、運用環境では通常、特定のパフォーマンス監視プラットフォームを組み合わせて上記の指標を管理およびレポートする必要があります。監視サービス。
4.9 SSG/ISR/SPR
一部の静的サイト (ブログ、ドキュメントなど) では、動的に変化するデータが関与しないため、サーバー側レンダリングを使用する必要がない場合があります。この時点では、構築フェーズで展開するための完全な HTML を生成するだけで済みます。構築フェーズで HTML を生成するこの方法は、SSG (静的サイト生成、静的サイト生成) とも呼ばれます。
SSG と SSR の最大の違いは、HTML の生成時間が SSR ランタイムからビルド時間に変更されましたが、コアとなるライフサイクル プロセスは変わっていないことです。
以下は簡単な実装コードです。
// scripts/ssg.ts
// 以下的工具函数均可以从 SSR 流程复用
async function ssg() {
// 1. 加载服务端入口
const { ServerEntry, fetchData } = await loadSsrEntryModule(null);
// 2. 数据预取
const data = await fetchData();
// 3. 组件渲染
const appHtml = renderToString(React.createElement(ServerEntry, { data }));
// 4. HTML 拼接
const template = await resolveTemplatePath();
const templateHtml = await fs.readFileSync(template, 'utf-8');
const html = templateHtml
.replace('<!-- SSR_APP -->', appHtml)
.replace(
'<!-- SSR_DATA -->',
`<script>window.__SSR_DATA__=${JSON.stringify(data)}</script>`
);
// 最后,我们需要将 HTML 的内容写到磁盘中,将其作为构建产物
fs.mkdirSync('./dist/client', { recursive: true });
fs.writeFileSync('./dist/client/index.html', html);
}
ssg();
次に、そのような npm スクリプトを package.json に追加して使用します。
{
"scripts": {
"build:ssg": "npm run build && NODE_ENV=production esno scripts/ssg.ts"
}
}
このようにして、私たちは最初に SSG のロジックを実現しました。もちろん、業界では SSG 以外にも、SPR や ISR など、高そうに聞こえるレンダリング モードがいくつか出回っていますが、実際には、これらは SSR や SSG から派生した新しい機能にすぎません。 :
- SPR は Serverless Pre Render の略で、サーバーレス (FaaS) 環境に SSR サービスを展開し、サーバー インスタンスの自動拡張と縮小を実現し、サーバーの運用と保守のコストを削減します。
- ISR は Incremental Site Rendering の略で、SSG ロジックの一部を構築時から SSR ランタイムに移動し、大量のページに対する時間のかかる SSG 構築の問題を解決します。