Environment API для фреймворков
Экспериментально
Environment API является экспериментальным. Мы будем поддерживать стабильность API в Vite 6, чтобы дать экосистеме возможность экспериментировать и строить на его основе. Мы планируем стабилизировать эти новые API с возможными изменениями, нарушающими обратную совместимость, в Vite 7.
Ресурсы:
- Обсуждение отзывов, где мы собираем отзывы о новых API.
- PR Environment API, где новый API был реализован и рассмотрен.
Пожалуйста, поделитесь с нами своим мнением.
Окружения и фреймворки
Неявное окружение ssr
и другие не-клиентские окружения по умолчанию используют RunnableDevEnvironment
во время разработки. Хотя это требует, чтобы среда выполнения совпадала с той, в которой работает сервер Vite, это работает аналогично ssrLoadModule
и позволяет фреймворкам мигрировать и включать HMR для их истории разработки SSR. Вы можете защитить любое исполняемое окружение с помощью функции isRunnableDevEnvironment
.
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
оценивается жадно при первом доступе. Имейте в виду, что Vite включает поддержку карт источников, когда runner
создаётся вызовом process.setSourceMapsEnabled
или переопределением Error.prepareStackTrace
, если это не доступно.
RunnableDevEnvironment
по умолчанию
Учитывая сервер Vite, настроенный в режиме мидлвара, как описано в руководстве по настройке SSR, давайте реализуем мидлвар SSR, используя Environment API. Обработка ошибок опущена.
import { createServer } from 'vite'
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:
// код, использующий 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.
// код, использующий 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
на пользовательском модуле, можно использовать следующий плагин:
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 из пользовательских модулей. Однако имейте в виду, что этот подход может работать не так же после процесса сборки.
// код, использующий 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
. Фреймворк или пользователь могут дополнительно настроить, как окружения будут собираться, используя:
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 для плагинов, чтобы узнать, как создавать плагины, учитывающие окружение.