コンテンツへスキップ

フレームワーク用環境API

実験的機能

環境 API は実験的です。エコシステムが実験し、それらを基盤として構築できるように、メジャーリリース間での API の安定性は引き続き維持されます。ダウンストリームプロジェクトが新機能を実験し、検証する時間を持った後、将来のメジャーリリースでこれらの新しい API を安定化させる予定です(潜在的な破壊的変更を伴う可能性があります)。

リソース

ぜひフィードバックをお寄せください。

環境とフレームワーク

暗黙的な ssr 環境とその他の非クライアント環境は、開発時にはデフォルトで RunnableDevEnvironment を使用します。これにより、ランタイムが Vite サーバーが実行されているランタイムと同じである必要がありますが、ssrLoadModule と同様に機能し、フレームワークが移行して SSR 開発ストーリーの HMR を有効にできるようになります。実行可能な環境は isRunnableDevEnvironment 関数で保護できます。

ts
export class RunnableDevEnvironment extends DevEnvironment {
  public readonly runner: ModuleRunner
}

class ModuleRunner {
  /**
   * URL to execute.
   * Accepts file path, server path, or id relative to the root.
   * Returns an instantiated module (same as in ssrLoadModule)
   */
  public async import(url: string): Promise<Record<string, any>>
  /**
   * Other ModuleRunner methods...
   */
}

if (isRunnableDevEnvironment(server.environments.ssr)) {
  await server.environments.ssr.runner.import('/entry-point.js')
}

警告

runner は、最初にアクセスされたときにのみ遅延評価されます。runnerprocess.setSourceMapsEnabled を呼び出すか、利用できない場合は Error.prepareStackTrace をオーバーライドすることで作成されると、Vite はソースマップのサポートを有効にすることに注意してください。

Fetch API を介してランタイムと通信するフレームワークは、handleRequest メソッドを介してリクエストを処理する標準化された方法を提供する FetchableDevEnvironment を利用できます。

ts
import {
  createServer,
  createFetchableDevEnvironment,
  isFetchableDevEnvironment,
} from 'vite'

const server = await createServer({
  server: { middlewareMode: true },
  appType: 'custom',
  environments: {
    custom: {
      dev: {
        createEnvironment(name, config) {
          return createFetchableDevEnvironment(name, config, {
            handleRequest(request: Request): Promise<Response> | Response {
              // handle Request and return a Response
            },
          })
        },
      },
    },
  },
})

// Any consumer of the environment API can now call `dispatchFetch`
if (isFetchableDevEnvironment(server.environments.custom)) {
  const response: Response = await server.environments.custom.dispatchFetch(
    new Request('/request-to-handle'),
  )
}

警告

Vite は dispatchFetch メソッドの入力と出力を検証します。リクエストはグローバルな Request クラスのインスタンスである必要があり、レスポンスはグローバルな Response クラスのインスタンスである必要があります。そうでない場合、Vite は TypeError をスローします。

FetchableDevEnvironment はクラスとして実装されていますが、Vite チームによって実装の詳細と見なされており、いつでも変更される可能性があることに注意してください。

デフォルトの RunnableDevEnvironment

SSR セットアップガイドで説明されているように、ミドルウェアモードで設定された Vite サーバーが与えられた場合、環境 API を使用して SSR ミドルウェアを実装してみましょう。ssr と呼ばれる必要はないので、この例では server と名付けます。エラー処理は省略されています。

js
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createServer } from 'vite'

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

const viteServer = await createServer({
  server: { middlewareMode: true },
  appType: 'custom',
  environments: {
    server: {
      // by default, modules are run in the same process as the vite server
    },
  },
})

// You might need to cast this to RunnableDevEnvironment in TypeScript or
// use isRunnableDevEnvironment to guard the access to the runner
const serverEnvironment = viteServer.environments.server

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

  // 1. Read index.html
  const indexHtmlPath = path.resolve(__dirname, 'index.html')
  let template = fs.readFileSync(indexHtmlPath, 'utf-8')

  // 2. Apply Vite HTML transforms. This injects the Vite HMR client,
  //    and also applies HTML transforms from Vite plugins, e.g. global
  //    preambles from @vitejs/plugin-react
  template = await viteServer.transformIndexHtml(url, template)

  // 3. Load the server entry. import(url) automatically transforms
  //    ESM source code to be usable in Node.js! There is no bundling
  //    required, and provides full HMR support.
  const { render } = await serverEnvironment.runner.import(
    '/src/entry-server.js',
  )

  // 4. render the app HTML. This assumes entry-server.js's exported
  //     `render` function calls appropriate framework SSR APIs,
  //    e.g. ReactDOMServer.renderToString()
  const appHtml = await render(url)

  // 5. Inject the app-rendered HTML into the template.
  const html = template.replace(`<!--ssr-outlet-->`, appHtml)

  // 6. Send the rendered HTML back.
  res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
})

ランタイムに依存しない SSR

RunnableDevEnvironment は Vite サーバーと同じランタイムでコードを実行するためにのみ使用できるため、Vite サーバーを実行できるランタイム(Node.js と互換性のあるランタイム)が必要です。これは、ランタイムに依存しないようにするために、生の DevEnvironment を使用する必要があることを意味します。

FetchableDevEnvironment の提案

最初の提案では、DevEnvironment クラスに run メソッドがあり、transport オプションを使用してコンシューマーがランナー側でインポートを呼び出すことができました。テスト中に、この API が推奨するほど普遍的ではないことがわかりました。現在、FetchableDevEnvironment の提案についてフィードバックを求めています。

RunnableDevEnvironment には、モジュールの値を返す runner.import 関数があります。しかし、この関数は生の DevEnvironment では利用できず、Vite の API を使用するコードとユーザーモジュールを切り離す必要があります。

例えば、次の例では、Vite の API を使用するコードからユーザーモジュールの値を使用しています

ts
// code using the Vite's APIs
import { createServer } from 'vite'

const server = createServer()
const ssrEnvironment = server.environment.ssr
const input = {}

const { createHandler } = await ssrEnvironment.runner.import('./entrypoint.js')
const handler = createHandler(input)
const response = handler(new Request('/'))

// -------------------------------------
// ./entrypoint.js
export function createHandler(input) {
  return function handler(req) {
    return new Response('hello')
  }
}

コードがユーザーモジュールと同じランタイムで実行できる場合(つまり、Node.js 固有の API に依存しない場合)、仮想モジュールを使用できます。このアプローチにより、Vite の API を使用するコードから値にアクセスする必要がなくなります。

ts
// code using the Vite's APIs
import { createServer } from 'vite'

const server = createServer({
  plugins: [
    // a plugin that handles `virtual:entrypoint`
    {
      name: 'virtual-module',
      /* plugin implementation */
    },
  ],
})
const ssrEnvironment = server.environment.ssr
const input = {}

// use exposed functions by each environment factories that runs the code
// check for each environment factories what they provide
if (ssrEnvironment instanceof RunnableDevEnvironment) {
  ssrEnvironment.runner.import('virtual:entrypoint')
} else if (ssrEnvironment instanceof CustomDevEnvironment) {
  ssrEnvironment.runEntrypoint('virtual:entrypoint')
} else {
  throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`)
}

// -------------------------------------
// virtual:entrypoint
const { createHandler } = await import('./entrypoint.js')
const handler = createHandler(input)
const response = handler(new Request('/'))

// -------------------------------------
// ./entrypoint.js
export function createHandler(input) {
  return function handler(req) {
    return new Response('hello')
  }
}

例えば、ユーザーモジュールで transformIndexHtml を呼び出すには、次のプラグインを使用できます

ts
function vitePluginVirtualIndexHtml(): Plugin {
  let server: ViteDevServer | undefined
  return {
    name: vitePluginVirtualIndexHtml.name,
    configureServer(server_) {
      server = server_
    },
    resolveId(source) {
      return source === 'virtual:index-html' ? '\0' + source : undefined
    },
    async load(id) {
      if (id === '\0' + 'virtual:index-html') {
        let html: string
        if (server) {
          this.addWatchFile('index.html')
          html = fs.readFileSync('index.html', 'utf-8')
          html = await server.transformIndexHtml('/', html)
        } else {
          html = fs.readFileSync('dist/client/index.html', 'utf-8')
        }
        return `export default ${JSON.stringify(html)}`
      }
      return
    },
  }
}

コードが Node.js API を必要とする場合、ユーザーモジュールから Vite の API を使用するコードと通信するために hot.send を使用できます。ただし、このアプローチはビルドプロセス後に同じように機能しない可能性があることに注意してください。

ts
// code using the Vite's APIs
import { createServer } from 'vite'

const server = createServer({
  plugins: [
    // a plugin that handles `virtual:entrypoint`
    {
      name: 'virtual-module',
      /* plugin implementation */
    },
  ],
})
const ssrEnvironment = server.environment.ssr
const input = {}

// use exposed functions by each environment factories that runs the code
// check for each environment factories what they provide
if (ssrEnvironment instanceof RunnableDevEnvironment) {
  ssrEnvironment.runner.import('virtual:entrypoint')
} else if (ssrEnvironment instanceof CustomDevEnvironment) {
  ssrEnvironment.runEntrypoint('virtual:entrypoint')
} else {
  throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`)
}

const req = new Request('/')

const uniqueId = 'a-unique-id'
ssrEnvironment.send('request', serialize({ req, uniqueId }))
const response = await new Promise((resolve) => {
  ssrEnvironment.on('response', (data) => {
    data = deserialize(data)
    if (data.uniqueId === uniqueId) {
      resolve(data.res)
    }
  })
})

// -------------------------------------
// virtual:entrypoint
const { createHandler } = await import('./entrypoint.js')
const handler = createHandler(input)

import.meta.hot.on('request', (data) => {
  const { req, uniqueId } = deserialize(data)
  const res = handler(req)
  import.meta.hot.send('response', serialize({ res: res, uniqueId }))
})

const response = handler(new Request('/'))

// -------------------------------------
// ./entrypoint.js
export function createHandler(input) {
  return function handler(req) {
    return new Response('hello')
  }
}

ビルド時の環境

CLI では、vite buildvite build --ssr を呼び出すと、後方互換性のために引き続きクライアントのみの環境と SSR のみの環境がビルドされます。

builderundefined でない場合(または vite build --app を呼び出す場合)、vite build は代わりにアプリケーション全体をビルドすることを選択します。これは将来のメジャーバージョンでデフォルトになります。構成されたすべての環境を本番用にビルドするために、ViteBuilder インスタンス(ViteDevServer のビルド時同等物)が作成されます。デフォルトでは、環境のビルドは environments レコードの順序を尊重して直列に実行されます。フレームワークまたはユーザーは、以下を使用して環境がどのようにビルドされるかをさらに構成できます

js
export default {
  builder: {
    buildApp: async (builder) => {
      const environments = Object.values(builder.environments)
      return Promise.all(
        environments.map((environment) => builder.build(environment)),
      )
    },
  },
}

プラグインは buildApp フックも定義できます。順序 'pre'null は構成された builder.buildApp の前に実行され、順序 'post' フックはその後で実行されます。environment.isBuilt は、環境がすでにビルドされているかどうかを確認するために使用できます。

環境に依存しないコード

ほとんどの場合、現在の environment インスタンスは実行されるコードのコンテキストの一部として利用できるため、server.environments を介してアクセスする必要はめったにありません。例えば、プラグインフック内では、環境は PluginContext の一部として公開されているため、this.environment を使用してアクセスできます。プラグイン用環境API を参照して、環境を意識したプラグインの構築方法を学んでください。

MIT ライセンスで公開。(083ff36d)