跳至内容

框架环境 API

实验性功能

环境 API 处于实验阶段。在 Vite 6 中,我们将保持 API 的稳定性,以便生态系统进行实验并在其基础上构建。我们计划在 Vite 7 中稳定这些新的 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 时会急切地对其进行评估。请注意,如果 runner 不可用,则 Vite 会在通过调用 process.setSourceMapsEnabled 或覆盖 Error.prepareStackTrace 创建 runner 时启用源映射支持。

默认 RunnableDevEnvironment

假设 Vite 服务器以中间件模式配置,如SSR 设置指南中所述,让我们使用环境 API 实现 SSR 中间件。错误处理被省略。

js
import { createServer } from 'vite'

const server = 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 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. global
  //    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
  //    required, and provides full HMR support.
  const { render } = await environment.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('./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
// 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)),
      )
    },
  },
}

与环境无关的代码

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

根据 MIT 许可证发布。 (ccee3d7c)