Skip to content

Environment API для фреймворков

Экспериментально

Environment API является экспериментальным. Мы будем поддерживать стабильность API в Vite 6, чтобы дать экосистеме возможность экспериментировать и строить на его основе. Мы планируем стабилизировать эти новые API с возможными изменениями, нарушающими обратную совместимость, в Vite 7.

Ресурсы:

Пожалуйста, поделитесь с нами своим мнением.

Окружения и фреймворки

Неявное окружение ssr и другие не-клиентские окружения по умолчанию используют RunnableDevEnvironment во время разработки. Хотя это требует, чтобы среда выполнения совпадала с той, в которой работает сервер Vite, это работает аналогично ssrLoadModule и позволяет фреймворкам мигрировать и включать HMR для их истории разработки SSR. Вы можете защитить любое исполняемое окружение с помощью функции isRunnableDevEnvironment.

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

class ModuleRunner {
  /**
   * URL для выполнения.
   * Принимает путь к файлу, путь к серверу или идентификатор относительно корня.
   * Возвращает экземпляр модуля (такой же, как в ssrLoadModule)
   */
  public async import(url: string): Promise<Record<string, any>>
  /**
   * Другие методы ModuleRunner...
   */
}

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

ПРЕДУПРЕЖДЕНИЕ

runner вычисляется лениво (lazy evaluation) только при первом обращении к нему. Обратите внимание, что Vite включает поддержку карт источников (source maps) при создании runner через вызов process.setSourceMapsEnabled или переопределением Error.prepareStackTrace, если первый метод недоступен.

Фреймворки, которые взаимодействуют со своей средой выполнения через Fetch API, могут использовать FetchableDevEnvironment, предоставляющий стандартизированный способ обработки запросов через метод handleRequest:

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 {
              // обрабатываем Request и возвращаем Response
            },
          })
        },
      },
    },
  },
})

// Теперь любой компонент, использующий Environment API, может вызывать `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 по умолчанию

Учитывая сервер Vite, настроенный в режиме мидлвара, как описано в руководстве по настройке SSR, давайте реализуем мидлвар SSR, используя Environment API. Обработка ошибок опущена.

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 server = await createServer({
  server: { middlewareMode: true },
  appType: 'custom',
  environments: {
    server: {
      // по умолчанию модули выполняются в том же процессе, что и сервер Vite.
    },
  },
})

// Возможно, вам потребуется привести это к RunnableDevEnvironment в TypeScript или
// использовать isRunnableDevEnvironment для защиты доступа к runner
const environment = server.environments.node

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

  // 1. Чтение index.html
  const indexHtmlPath = path.resolve(__dirname, 'index.html')
  let template = fs.readFileSync(indexHtmlPath, 'utf-8')

  // 2. Применение HTML-преобразований Vite. Это внедряет клиент HMR Vite,
  //    а также применяет HTML-преобразования от плагинов Vite, например,
  //    глобальные преамбулы от @vitejs/plugin-react
  template = await server.transformIndexHtml(url, template)

  // 3. Загрузка серверного входа. import(url) автоматически преобразует
  //    исходный код ESM для использования в Node.js! Не требуется сборка,
  //    и обеспечивается полная поддержка HMR.
  const { render } = await environment.runner.import('/src/entry-server.js')

  // 4. Рендеринг HTML приложения. Это предполагает, что экспортированная
  //    функция `render` entry-server.js вызывает соответствующие API SSR фреймворка,
  //    например, ReactDOMServer.renderToString()
  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)
})

SSR, независимый от среды выполнения

Поскольку RunnableDevEnvironment может использоваться только для выполнения кода в той же среде выполнения, что и сервер Vite, он требует среды выполнения, которая может запускать сервер Vite (среда выполнения, совместимая с Node.js). Это означает, что вам нужно будет использовать сырой DevEnvironment, чтобы сделать его независимым от среды выполнения.

Предложение FetchableDevEnvironment

Первоначальное предложение имело метод run в классе DevEnvironment, который позволял потребителям вызывать импорт на стороне runner, используя опцию transport. В ходе нашего тестирования мы обнаружили, что API не был достаточно универсальным, чтобы начать его рекомендовать. В данный момент мы собираем отзывы о предложении FetchableDevEnvironment.

RunnableDevEnvironment имеет функцию runner.import, которая возвращает значение модуля. Но эта функция недоступна в сыром DevEnvironment и требует, чтобы код, использующий API Vite, и пользовательские модули были отделены.

Например, следующий пример использует значение пользовательского модуля из кода, использующего API Vite:

ts
// код, использующий API Vite
import { createServer } from 'vite'

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

const { createHandler } = await ssrEnvironment.runner.import('./entry.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), вы можете использовать виртуальный модуль. Этот подход устраняет необходимость доступа к значению из кода, использующего API Vite.

ts
// код, использующий API Vite
import { createServer } from 'vite'

const server = createServer({
  plugins: [
    // плагин, который обрабатывает `virtual:entrypoint`
    {
      name: 'virtual-module',
      /* имплементация плагина */
    },
  ],
})
const ssrEnvironment = server.environment.ssr
const input = {}

// используйте открытые функции от каждой фабрики окружений, которые выполняют код
// проверьте, что они предоставляют для каждой фабрики окружений
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
    },
  }
}

Если ваш код требует API Node.js, вы можете использовать hot.send для связи с кодом, который использует API Vite из пользовательских модулей. Однако имейте в виду, что этот подход может работать не так же после процесса сборки.

ts
// код, использующий API Vite
import { createServer } from 'vite'

const server = createServer({
  plugins: [
    // плагин, который обрабатывает `virtual:entrypoint`
    {
      name: 'virtual-module',
      /* имплементация плагина */
    },
  ],
})
const ssrEnvironment = server.environment.ssr
const input = {}

// используйте открытые функции от каждой фабрики окружений, которые выполняют код
// проверьте, что они предоставляют для каждой фабрики окружений
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 build и vite build --ssr по-прежнему будет собирать только клиентские и только SSR окружения для обратной совместимости.

Когда builder не равен undefined (или при вызове 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)),
      )
    },
  },
}

Код, независимый от окружения

Чаще всего текущий экземпляр environment будет доступен как часть контекста выполняемого кода, поэтому необходимость доступа к ним через server.environments должна быть редкой. Например, внутри хуков плагинов окружение доступно как часть PluginContext, поэтому к нему можно получить доступ с помощью this.environment. См. Environment API для плагинов, чтобы узнать, как создавать плагины, учитывающие окружение.

Выпущено под лицензией MIT.