跳到内容

框架的环境 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 仅在首次访问时才会被延迟评估。请注意,Vite 通过调用 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 {
              // 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

给定一个以中间件模式配置的 Vite 服务器,如 SSR 设置指南 所述,让我们使用环境 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,您可以使用 hot.send 从用户模块与使用 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}`)
}

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)),
      )
    },
  },
}

插件还可以定义一个 buildApp 钩子。Order 'pre'null 在配置的 builder.buildApp 之前执行,而 order 'post' 钩子在它之后执行。environment.isBuilt 可用于检查是否已经构建了环境。

环境无关的代码

大多数时候,当前 environment 实例将作为正在运行的代码的上下文的一部分提供,因此通过 server.environments 访问它们的需要应该很少。例如,在插件钩子中,环境作为 PluginContext 的一部分公开,因此可以使用 this.environment 访问它。请参阅 插件的环境 API 以了解如何构建环境感知插件。

在 MIT 许可证下发布。(083ff36d)