Skip to content

Beyond Fast

ViteConf 2023

Watch the replay!

サーバサイドレンダリング

注意

SSR は特に、Node.js で同じアプリケーションを実行し、HTML を先読みレンダリングし、最後にクライアントでハイドレートすることをサポートするフロントエンドフレームワーク(React、Preact、Vue、Svelte など)を指します。 従来のサーバサイドフレームワークとの統合をお探しの場合は、代わりに バックエンド統合ガイド を確認してください。

次のガイドは、選択したフレームワークで SSR を使用した経験があることも前提としており、Vite 固有の統合の詳細のみに焦点を当てています。

低レベル API

これは、ライブラリやフレームワーク製作者のための低レベル API です。アプリケーションを作成することが目的ならば、まず Awesome Vite の SSR セクションにある高レベルの SSR プラグインとツールを確認してください。とはいえ、多くのアプリケーションは、Vite のネイティブの低レベル API 上に直接構築されています。

ヘルプ

質問がある場合は、Vite Discord の #ssr チャンネルでコミュニティがいつでも助けてくれます。

プロジェクトの例

Vite はサーバサイドレンダリング ( SSR ) の組み込みサポートを提供します。create-vite-extra には、このガイドのリファレンスとして使用できる SSR セットアップの例が含まれています:

create-vite を実行し、フレームワークオプションで Others > create-vite-extra を選択することで、これらのプロジェクトをローカルで生成することもできます。

ソースファイルの構成

一般的な SSR アプリケーションは、次のようなソースファイル構造になります。:

- index.html
- server.js # main application server
- src/
  - main.js          # exports env-agnostic (universal) app code
  - entry-client.js  # mounts the app to a DOM element
  - entry-server.js  # renders the app using the framework's SSR API
- index.html
- server.js # main application server
- src/
  - main.js          # exports env-agnostic (universal) app code
  - entry-client.js  # mounts the app to a DOM element
  - entry-server.js  # renders the app using the framework's SSR API

index.htmlentry-client.js を参照し、サーバサイドでレンダリングされたマークアップを挿入するためにプレースホルダを含める必要があります。:

html
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>

正確に置き換えることができる限り、<!--ssr-outlet--> の代わりに任意のプレースホルダを使用することができます。

条件付きロジック

SSR とクライアントに基づいて条件付きロジックを実行する場合は次を使用できます。

js
if (import.meta.env.SSR) {
  // ... サーバのロジック
}
if (import.meta.env.SSR) {
  // ... サーバのロジック
}

これはビルド中に静的に置き換えられるため、未使用のブランチのツリーシェイクが可能になります。

開発サーバのセットアップ

SSR をビルドする際、メインサーバを完全に制御し、Vite を本番環境から切り離したいと思うでしょう。したがってミドルウェアモードで Vite を使用することをお勧めします。これは express での例です:

server.js

js
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import express from 'express'
import { createServer as createViteServer } from 'vite'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

async function createServer() {
  const app = express()

  // ミドルウェアモードで Vite サーバを作成し、app type を 'custom' に指定します。
  // これにより、Vite 自体の HTML 配信ロジックが無効になり、親サーバが
  // 制御できるようになります。
  const vite = await createViteServer({
    server: { middlewareMode: true },
    appType: 'custom'
  })

  // Vite の接続インスタンスをミドルウェアとして使用。独自の express ルータ
  // (express.Route()) を利用する場合は、router.use を使用してください
  app.use((req, res, next) => {
    // サーバが再起動すると(例えばユーザがvite.config.jsを修飾した後)、
    // `vite.middlewares` は再割り当てされます。
    // ラッパーハンドラの内部で `vite.middlewares` を呼び出すと、
    // 常に最新の Vite ミドルウェアが使用されます。
    vite.middlewares.handle(req, res, next)
  })

  app.use('*', async (req, res) => {
    // index.html を提供します - 次にこれに取り組みます。
  })

  app.listen(5173)
}

createServer()
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import express from 'express'
import { createServer as createViteServer } from 'vite'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

async function createServer() {
  const app = express()

  // ミドルウェアモードで Vite サーバを作成し、app type を 'custom' に指定します。
  // これにより、Vite 自体の HTML 配信ロジックが無効になり、親サーバが
  // 制御できるようになります。
  const vite = await createViteServer({
    server: { middlewareMode: true },
    appType: 'custom'
  })

  // Vite の接続インスタンスをミドルウェアとして使用。独自の express ルータ
  // (express.Route()) を利用する場合は、router.use を使用してください
  app.use((req, res, next) => {
    // サーバが再起動すると(例えばユーザがvite.config.jsを修飾した後)、
    // `vite.middlewares` は再割り当てされます。
    // ラッパーハンドラの内部で `vite.middlewares` を呼び出すと、
    // 常に最新の Vite ミドルウェアが使用されます。
    vite.middlewares.handle(req, res, next)
  })

  app.use('*', async (req, res) => {
    // index.html を提供します - 次にこれに取り組みます。
  })

  app.listen(5173)
}

createServer()

ここで viteViteDevServer のインスタンスです。 vite.middlewares は、connect 互換の Node.js フレームワークでミドルウェアとして使用できる Connect インスタンスです。

次のステップはサーバサイドでレンダリングされた HTML を提供するための * ハンドラの実装です:

js
app.use('*', async (req, res, next) => {
  const url = req.originalUrl

  try {
    // 1. index.html を読み込む
    let template = fs.readFileSync(
      path.resolve(__dirname, 'index.html'),
      'utf-8',
    )

    // 2. Vite の HTML の変換を適用します。これにより Vite の HMR クライアントが定義され
    //    Vite プラグインからの HTML 変換も適用します。 e.g. global preambles
    //    from @vitejs/plugin-react
    template = await vite.transformIndexHtml(url, template)

    // 3. サーバサイドのエントリポイントを読み込みます。 ssrLoadModule は自動的に
    //    ESM を Node.js で使用できるコードに変換します! ここではバンドルは必要ありません
    //    さらに HMR と同様な効率的な無効化を提供します。
    const { render } = await vite.ssrLoadModule('/src/entry-server.js')

    // 4. アプリケーションの HTML をレンダリングします。これは entry-server.js から
    //    エクスポートされた `render` 関数が、ReactDOMServer.renderToString() などの
    //    適切なフレームワークの SSR API を呼び出すことを想定しています。
    const appHtml = await render(url)

    // 5. アプリケーションのレンダリングされた HTML をテンプレートに挿入します。
    const html = template.replace(`<!--ssr-outlet-->`, appHtml)

    // 6. レンダリングされた HTML をクライアントに送ります。
    res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
  } catch (e) {
    // エラーが検出された場合は、Vite にスタックトレースを修正させ、実際のソースコードに
    // マップし直します。
    vite.ssrFixStacktrace(e)
    next(e)
  }
})
app.use('*', async (req, res, next) => {
  const url = req.originalUrl

  try {
    // 1. index.html を読み込む
    let template = fs.readFileSync(
      path.resolve(__dirname, 'index.html'),
      'utf-8',
    )

    // 2. Vite の HTML の変換を適用します。これにより Vite の HMR クライアントが定義され
    //    Vite プラグインからの HTML 変換も適用します。 e.g. global preambles
    //    from @vitejs/plugin-react
    template = await vite.transformIndexHtml(url, template)

    // 3. サーバサイドのエントリポイントを読み込みます。 ssrLoadModule は自動的に
    //    ESM を Node.js で使用できるコードに変換します! ここではバンドルは必要ありません
    //    さらに HMR と同様な効率的な無効化を提供します。
    const { render } = await vite.ssrLoadModule('/src/entry-server.js')

    // 4. アプリケーションの HTML をレンダリングします。これは entry-server.js から
    //    エクスポートされた `render` 関数が、ReactDOMServer.renderToString() などの
    //    適切なフレームワークの SSR API を呼び出すことを想定しています。
    const appHtml = await render(url)

    // 5. アプリケーションのレンダリングされた HTML をテンプレートに挿入します。
    const html = template.replace(`<!--ssr-outlet-->`, appHtml)

    // 6. レンダリングされた HTML をクライアントに送ります。
    res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
  } catch (e) {
    // エラーが検出された場合は、Vite にスタックトレースを修正させ、実際のソースコードに
    // マップし直します。
    vite.ssrFixStacktrace(e)
    next(e)
  }
})

package.jsondev スクリプトも代わりにサーバスクリプトを使用するように変更する必要があります:

diff
"scripts": {
-   "dev": "vite"
+   "dev": "node server"
  }
"scripts": {
-   "dev": "vite"
+   "dev": "node server"
  }

プロダクションビルド

SSR プロジェクトを本番環境に適用するには次の作業を行う必要があります:

  1. 通常通りクライアントビルドします。
  2. SSR ビルドを作成します、これは import() を介して直接ロードできるので、Vite の ssrLoadModule を経由する必要はありません。

package.json は次のようになります:

json
{
  "scripts": {
    "dev": "node server",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --outDir dist/server --ssr src/entry-server.js"
  }
}
{
  "scripts": {
    "dev": "node server",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --outDir dist/server --ssr src/entry-server.js"
  }
}

これは SSR ビルドだということを示す --ssr フラグに注意してください。 また、SSR エントリを指定する必要があります。

次に server.jsprocess.env.NODE_ENV をチェックして本番固有のロジックを追加する必要があります:

  • ルートの index.html を読み取る代わりに dist/client/index.html を使用します。これはクライアントビルドへの正しいアセットリンクが含まれているためです。

  • await vite.ssrLoadModule('/src/entry-server.js') の代わりに import('./dist/server/entry-server.js') を使用します(このファイルは SSR ビルドした結果のファイルです)。

  • vite 開発サーバの作成とすべての使用を開発専用サーバかどうかの条件分岐の後ろに移動します。次に静的ファイルを提供するミドルウェアを追加し、dist/client からファイルを提供します。

動作するセットアップについては、サンプルプロジェクトを参照してください。

Preload Directives の作成

vite build はビルド出力ディレクトリに .vite/ssr-manifest.json を生成する --ssrManifest フラグをサポートしています:

diff
- "build:client": "vite build --outDir dist/client",
+ "build:client": "vite build --outDir dist/client --ssrManifest",
- "build:client": "vite build --outDir dist/client",
+ "build:client": "vite build --outDir dist/client --ssrManifest",

上記のスクリプトはクライアントビルドの際に dist/client/.vite/ssr-manifest.json を生成します(モジュール ID をクライアントファイルにマップするため、SSR マニフェストはクライアントビルドから生成されます)。マニフェストには、モジュール ID の関連するチャンクおよびアセットファイルへのマッピングが含まれています。

マニフェストを活用するには、フレームワークはサーバのレンダリング呼び出し中に使用されたコンポーネントのモジュール ID を収集する方法を提供する必要があります。

@vitejs/plugin-vue は自動で使用されたコンポーネントモジュール ID を関連する VueSSR コンテキストに登録することを標準でサポートしています:

js
// src/entry-server.js
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
// ctx.modules はレンダリング中にしようされたモジュール ID をセットします。
// src/entry-server.js
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
// ctx.modules はレンダリング中にしようされたモジュール ID をセットします。

本番ブランチの server.js では、マニフェストを読み取って、src/entry-server.js によってエクスポートされた render 関数に渡す必要があります。これにより、非同期ルートで使用されるファイルのプリロードディレクティブをレンダリングするのに十分な情報が得られます!完全な例は demo source をご覧ください。103 Early Hints にも利用できます。

Pre-Rendering / SSG

ルートと特定のルートに必要なデータが事前にわかっている場合は、本番 SSR と同じロジックを使用して、これらのルートを静的 HTML に先読みでレンダリングすることができます。これは、SSG の形式と見なすこともできます。 詳しくは demo pre-render script をご覧ください。

外部 SSR

SSR を実行する場合、依存関係はデフォルトで Vite の SSR トランスフォームモジュールシステムから「外部化」されます。これにより、開発とビルドの両方を高速化します。

Vite の機能がトランスパイルされてない状態で使われている場合のように、Vite のパイプラインによって変換される必要のある依存関係は、ssr.noExternal に追加できます。

リンクされた依存関係については、 Vite の HMR を活用するためにデフォルトでは外部化されていません。これが望ましくない場合、例えばリンクされていないかのように依存関係をテストしたい場合は、ssr.external に追加できます。

エイリアスの操作

あるパッケージを別のパッケージにリダイレクトするエイリアスを設定した場合は、SSR の外部化された依存関係で機能するように、代わりに実際の node_modules パッケージにエイリアスを設定することをお勧めします。Yarnpnpm の両方で npm: のエイリアスをサポートします。

SSR 固有のプラグインロジック

Vue や Svelte などの一部のフレームワークは、クライアントと SSR に基づいてコンポーネントをさまざまな形式にコンパイルします。条件付き変換をサポートするために、Vite は次のプラグインフックの options オブジェクトに、追加の ssr プロパティをに渡します:

  • resolveId
  • load
  • transform

例:

js
export function mySSRPlugin() {
  return {
    name: 'my-ssr',
    transform(code, id, options) {
      if (options?.ssr) {
        // SSR 固有の transform を実行する...
      }
    },
  }
}
export function mySSRPlugin() {
  return {
    name: 'my-ssr',
    transform(code, id, options) {
      if (options?.ssr) {
        // SSR 固有の transform を実行する...
      }
    },
  }
}

loadtransform の options オブジェクトは省略可能であり、rollup は現在このオブジェクトを使用していませんが、将来的に追加のメタデータでこれらのフックを拡張する可能性があります。

注意

Vite 2.7 以前では、options オブジェクトを使うのではなく、固定の ssr 引数を使ってプラグインフックに通知されていました。すべての主要なフレームワークとプラグインは更新されていますが、以前の API を使っている古い記事が見つかる場合があります。

SSR ターゲット

SSR ビルドのデフォルトターゲットは Node 環境ですが、Web Worker でサーバを実行することもできます。パッケージのエントリの解決方法はプラットフォームごとに異なります。ターゲットを Web Worker に設定するには、ssr.target'webworker' に設定します。

SSR バンドル

webworker のランタイムなどの場合、SSR のビルドを 1 つの JavaScript ファイルにバンドルしたい場合があります。ssr.noExternaltrue に設定することで、この動作を有効にできます。これは 2 つのことを行います:

  • すべての依存関係を noExternal として扱う
  • Node.js のビルドインがインポートされた場合、エラーを投げる

SSR の解決条件

デフォルトでは、パッケージエントリの解決は resolve.conditions で設定された条件を SSR ビルドに使用します。この動作をカスタマイズするには、ssr.resolve.conditionsssr.resolve.externalConditions を使用します。

Vite CLI

コマンドラインコマンドの $ vite dev$ vite preview は SSR アプリでも使用することができます。開発サーバには configureServer 、プレビューサーバには configurePreviewServer で SSR ミドルウェアを追加できます。

注意

ポストフックを使用して、SSR ミドルウェアが Vite のミドルウェアの後に実行されるようにしてください。

Released under the MIT License. (fa1cfd34)