后端集成
如果你正在使用传统的后端框架(如 Rails、Laravel、Django)来渲染 HTML,但希望使用 Vite 来管理前端资源,本章将指导你如何进行集成。
集成方式概述
Vite 支持与传统后端框架的集成,主要方式有:
- 使用现有集成插件:许多后端框架已有社区维护的 Vite 集成插件
- 自定义集成:根据本章指南手动配置
现有集成插件
在自行配置之前,建议先查看 Awesome Vite 中是否已有针对你所用后端框架的集成插件:
- Rails:
vite_rails - Laravel:
laravel-vite - Django:
django-vite - Phoenix:
phoenix_live_reload - Spring Boot:
vite-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 文件来获取正确的资源路径。文中的后端实现示例可以作为参考,根据你使用的框架进行调整。