跳到主要内容

后端集成

如果你正在使用传统的后端框架(如 Rails、Laravel、Django)来渲染 HTML,但希望使用 Vite 来管理前端资源,本章将指导你如何进行集成。

集成方式概述

Vite 支持与传统后端框架的集成,主要方式有:

  1. 使用现有集成插件:许多后端框架已有社区维护的 Vite 集成插件
  2. 自定义集成:根据本章指南手动配置

现有集成插件

在自行配置之前,建议先查看 Awesome Vite 中是否已有针对你所用后端框架的集成插件:

  • Railsvite_rails
  • Laravellaravel-vite
  • Djangodjango-vite
  • Phoenixphoenix_live_reload
  • Spring Bootvite-spring-boot

如果已有现成的插件,可以直接使用,无需手动配置。

手动集成步骤

如果没有现成的插件,或者需要更精细的控制,可以按照以下步骤进行手动集成。

1. 配置 Vite

首先,修改 vite.config.js 配置:

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
// 配置开发服务器 CORS
server: {
cors: {
// 允许来自后端服务器的跨域请求
origin: 'http://my-backend.example.com',
},
},

build: {
// 生成 manifest 文件,用于后端获取资源路径
manifest: true,

rollupOptions: {
// 覆盖默认的 HTML 入口
input: '/path/to/main.js',
},
},
})

关键配置说明

server.cors:开发模式下,后端服务器需要从 Vite 开发服务器加载资源,必须配置 CORS。

build.manifest:设置为 true 后,构建会在输出目录生成 .vite/manifest.json 文件。这个文件记录了入口文件与打包后文件的映射关系,后端服务器需要读取它来正确引用资源。

rollupOptions.input:指定 JavaScript 入口文件,覆盖默认的 index.html 入口。

2. 添加模块预加载 Polyfill

如果没有禁用模块预加载 polyfill,需要在入口文件开头添加:

// main.js
// 在入口文件最开头导入
import 'vite/modulepreload-polyfill'

// 然后是你的应用代码
import { createApp } from 'vue'
import App from './App.vue'
// ...

这个 polyfill 确保动态导入的模块能正确预加载。

3. 开发环境配置

在开发环境中,后端渲染的 HTML 需要注入 Vite 开发服务器的脚本。

假设 Vite 开发服务器运行在 http://localhost:5173,后端模板应该注入以下内容:

<!-- 开发环境 -->
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/main.js"></script>

静态资源处理

开发环境中,有两种方式处理静态资源:

方式一:代理静态资源请求

配置后端服务器,将静态资源请求代理到 Vite 开发服务器。

以 Nginx 为例:

location /assets/ {
proxy_pass http://localhost:5173;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

方式二:配置 server.origin

// vite.config.js
export default defineConfig({
server: {
origin: 'http://localhost:5173',
},
})

设置后,Vite 生成的资源 URL 会使用完整路径而不是相对路径,确保图片等资源能正确加载。

React 特殊配置

如果你使用 React 和 @vitejs/plugin-react,需要在上述脚本之前添加额外的代码(因为插件无法修改后端服务器返回的 HTML):

<!-- React Fast Refresh 配置(开发环境) -->
<script type="module">
import RefreshRuntime from 'http://localhost:5173/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/main.js"></script>

4. 生产环境配置

运行 vite build 后,会在输出目录生成 .vite/manifest.json 文件。

Manifest 文件结构

一个典型的 manifest 文件如下:

{
"_shared-B7PI925R.js": {
"file": "assets/shared-B7PI925R.js",
"name": "shared",
"css": ["assets/shared-ChJ_j-JJ.css"]
},
"_shared-ChJ_j-JJ.css": {
"file": "assets/shared-ChJ_j-JJ.css",
"src": "_shared-ChJ_j-JJ.css"
},
"logo.svg": {
"file": "assets/logo-BuPIv-2h.svg",
"src": "logo.svg"
},
"views/bar.js": {
"file": "assets/bar-gkvgaI9m.js",
"name": "bar",
"src": "views/bar.js",
"isEntry": true,
"imports": ["_shared-B7PI925R.js"],
"dynamicImports": ["baz.js"]
},
"views/foo.js": {
"file": "assets/foo-BRBmoGS9.js",
"name": "foo",
"src": "views/foo.js",
"isEntry": true,
"imports": ["_shared-B7PI925R.js"],
"css": ["assets/foo-5UjPuW-k.css"]
}
}

Manifest 数据结构

interface ManifestChunk {
src?: string
file: string
css?: string[]
assets?: string[]
isEntry?: boolean
name?: string
names?: string[]
isDynamicEntry?: boolean
imports?: string[]
dynamicImports?: string[]
}

条目类型说明

类型特征Key 格式
入口块isEntry: true相对于项目根目录的 src 路径
动态入口块isDynamicEntry: true相对于项目根目录的 src 路径
非入口块被 import 的共享代码文件名前加 _ 前缀
资源块图片、字体等资源相对于项目根目录的 src 路径
CSS 文件样式文件入口 CSS 无前缀,非入口有 _ 前缀

后端渲染资源链接

后端服务器需要根据 manifest 文件渲染正确的资源链接。以下是推荐的渲染顺序:

对于入口点 views/foo.js

<!-- 1. 入口点自身的 CSS -->
<link rel="stylesheet" href="/assets/foo-5UjPuW-k.css" />

<!-- 2. 依赖块的 CSS(递归) -->
<link rel="stylesheet" href="/assets/shared-ChJ_j-JJ.css" />

<!-- 3. 入口点的 JS -->
<script type="module" src="/assets/foo-BRBmoGS9.js"></script>

<!-- 4. 可选:预加载依赖块 -->
<link rel="modulepreload" href="/assets/shared-B7PI925R.js" />

对于入口点 views/bar.js

<!-- 依赖块的 CSS -->
<link rel="stylesheet" href="/assets/shared-ChJ_j-JJ.css" />

<!-- 入口点的 JS -->
<script type="module" src="/assets/bar-gkvgaI9m.js"></script>

<!-- 可选:预加载依赖块 -->
<link rel="modulepreload" href="/assets/shared-B7PI925R.js" />

5. 后端实现示例

获取依赖块的函数

后端需要实现一个函数来获取入口点的所有依赖块:

import type { Manifest, ManifestChunk } from 'vite'

/**
* 获取入口点的所有依赖块
* @param manifest - manifest 对象
* @param name - 入口点名称
* @returns 依赖块数组(按依赖顺序)
*/
function getImportedChunks(
manifest: Manifest,
name: string,
): ManifestChunk[] {
const seen = new Set<string>()

function getChunks(chunk: ManifestChunk): ManifestChunk[] {
const chunks: ManifestChunk[] = []

for (const file of chunk.imports ?? []) {
const importee = manifest[file]

if (seen.has(file)) {
continue
}

seen.add(file)

// 递归获取依赖
chunks.push(...getChunks(importee))
chunks.push(importee)
}

return chunks
}

return getChunks(manifest[name])
}

生成 HTML 标签

/**
* 生成入口点的 HTML 标签
* @param manifest - manifest 对象
* @param entryName - 入口点名称
* @param basePath - 资源基础路径
* @returns HTML 标签字符串
*/
function renderEntryTags(
manifest: Manifest,
entryName: string,
basePath: string = '/',
): string {
const entry = manifest[entryName]
const importedChunks = getImportedChunks(manifest, entryName)

let html = ''

// 1. 入口点的 CSS
if (entry.css) {
for (const css of entry.css) {
html += `<link rel="stylesheet" href="${basePath}${css}" />\n`
}
}

// 2. 依赖块的 CSS(递归)
for (const chunk of importedChunks) {
if (chunk.css) {
for (const css of chunk.css) {
html += `<link rel="stylesheet" href="${basePath}${css}" />\n`
}
}
}

// 3. 入口点的 JS
html += `<script type="module" src="${basePath}${entry.file}"></script>\n`

// 4. 预加载依赖块(可选)
for (const chunk of importedChunks) {
html += `<link rel="modulepreload" href="${basePath}${chunk.file}" />\n`
}

return html
}

在后端模板中使用

以 EJS 模板为例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My App</title>
<%- renderViteTags('main.js') %>
</head>
<body>
<div id="app"></div>
</body>
</html>

6. 完整集成示例

项目结构

my-project/
├── app.js # 后端入口
├── views/ # 后端模板
│ └── index.ejs
├── public/ # 静态资源
│ └── images/
├── src/ # 前端源码
│ ├── main.js
│ └── App.vue
├── vite.config.js
└── package.json

Vite 配置

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
plugins: [vue()],

server: {
port: 5173,
cors: {
origin: 'http://localhost:3000',
},
},

build: {
manifest: true,
outDir: 'dist',
rollupOptions: {
input: resolve(__dirname, 'src/main.js'),
},
},

resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
})

Express 后端示例

// app.js
const express = require('express')
const path = require('path')
const fs = require('fs')

const app = express()
const isProduction = process.env.NODE_ENV === 'production'

// 设置模板引擎
app.set('view engine', 'ejs')

// 读取 manifest
let manifest = {}
if (isProduction) {
const manifestPath = path.join(__dirname, 'dist/.vite/manifest.json')
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
}

// 辅助函数:获取依赖块
function getImportedChunks(manifest, name) {
const seen = new Set()

function getChunks(chunk) {
const chunks = []
for (const file of chunk.imports || []) {
if (seen.has(file)) continue
seen.add(file)
const importee = manifest[file]
chunks.push(...getChunks(importee), importee)
}
return chunks
}

return getChunks(manifest[name] || {})
}

// 辅助函数:渲染 Vite 标签
function renderViteTags(entryName) {
if (!isProduction) {
// 开发环境:直接引用 Vite 开发服务器
return `
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/src/main.js"></script>
`
}

// 生产环境:使用 manifest
const entry = manifest[entryName]
if (!entry) return ''

const chunks = getImportedChunks(manifest, entryName)
let html = ''

// CSS
if (entry.css) {
for (const css of entry.css) {
html += `<link rel="stylesheet" href="/${css}">\n`
}
}
for (const chunk of chunks) {
if (chunk.css) {
for (const css of chunk.css) {
html += `<link rel="stylesheet" href="/${css}">\n`
}
}
}

// JS
html += `<script type="module" src="/${entry.file}"></script>\n`

// 预加载
for (const chunk of chunks) {
html += `<link rel="modulepreload" href="/${chunk.file}">\n`
}

return html
}

// 中间件:将渲染函数注入 res.locals
app.use((req, res, next) => {
res.locals.renderViteTags = renderViteTags
res.locals.isProduction = isProduction
next()
})

// 静态资源
if (isProduction) {
app.use(express.static(path.join(__dirname, 'dist')))
}

// 路由
app.get('/', (req, res) => {
res.render('index')
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`)
})

EJS 模板

<!-- views/index.ejs -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<%- renderViteTags('src/main.js') %>
</head>
<body>
<div id="app"></div>
</body>
</html>

最佳实践

环境判断

在开发环境和生产环境中使用不同的资源加载方式:

// 开发环境:使用 Vite 开发服务器
// 生产环境:使用构建后的静态资源

const isProduction = process.env.NODE_ENV === 'production'

缓存策略

生产环境建议配置适当的缓存策略:

# 静态资源长期缓存(文件名包含 hash)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# HTML 不缓存
location / {
add_header Cache-Control "no-cache";
}

错误处理

后端读取 manifest 时要处理文件不存在的情况:

let manifest = {}
try {
const manifestPath = path.join(__dirname, 'dist/.vite/manifest.json')
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
} catch (e) {
console.warn('Manifest not found, running in development mode?')
}

资源路径配置

如果应用部署在子路径下,需要配置 base 选项:

// vite.config.js
export default defineConfig({
base: '/my-app/',
})

后端渲染时使用相同的路径:

const basePath = '/my-app/'
html += `<link rel="stylesheet" href="${basePath}${css}">\n`

写在最后

如果你的项目使用传统后端框架(如 Rails、Laravel、Django),本章介绍的集成方式可以让你在享受 Vite 开发体验的同时保持后端渲染的能力。

集成时注意区分开发环境和生产环境的处理方式:开发环境直接连接 Vite 开发服务器,生产环境则需要解析 manifest 文件来获取正确的资源路径。文中的后端实现示例可以作为参考,根据你使用的框架进行调整。