Skip to content

Environment API for Frameworks

Experimental

Environment API is experimental. We'll keep the APIs stable during Vite 6 to let the ecosystem experiment and build on top of it. We're planning to stabilize these new APIs with potential breaking changes in Vite 7.

资源:

Please share your feedback with us.

环境和框架

默认情况下,隐式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')
}

WARNING

第一次访问时,请热切地评估runner 。当心VITE启用源地图支持时通过调用runner创建process.setSourceMapsEnabled或覆盖Error.prepareStackTrace源地图支持。

默认的RunnableDevEnvironment

给定按照SSR设置指南所述配置的Vite 服务器,让我们使用环境API实现SSR中间件。错误处理将被省略。

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 服务器相同的进程中运行
    },
  },
})

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

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.全球的
  //    preambles from @vitejs/plugin-react
  template = await server.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
  //    并提供完整的HMR支持。
  const { render } = await environment.runner.import('/src/entry-server.js')

  // 4. render the app HTML. This assumes entry-server.js's exported
  //
  //    例如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 proposal

最初的提案在DevEnvironment类上有一个run方法,该方法允许消费者通过使用transport选项在运行者端调用导入。在测试过程中,我们发现该API不够通用,无法开始推荐。目前,我们正在征求有关FetchableDevEnvironment提案的反馈。

RunnableDevEnvironment有一个runner.import函数,该函数返回模块的值。但是,此函数在原始的DevEnvironment中不可用,并且需要使用Vite 的API和用户模块的代码解耦。

例如,以下示例使用了来自使用Vite 的API的代码中的用户模块的值:

ts
// 使用Vite 的API的代码
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),则可以使用虚拟模块。这种方法消除了从使用Vite 的API的代码中访问值的需要。

ts
// 使用Vite 的API的代码
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
    },
  }
}

如果您的代码需要Node.js API,可以使用hot.send从用户模块中使用Vite 的API的代码进行通信。但是,请注意,这种方法在构建过程后可能不会以相同的方式工作。

ts
// 使用Vite 的API的代码
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 buildvite 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访问它。请参阅插件的环境API,了解如何构建环境感知插件。

Released under the MIT License. (dev)