1.vue的ssr方案
- vue的createApp+express接管主服务器控制服务端渲染并与客户端水合(本章讲解的方案)
- Nuxt.js(如果是新项目建议用这个)
2.涉及技术
- vue + vite + 任意UI框架 + vue-router
- express
- nginx + pm2
3.步骤
- 将main.js文件修改为SSR激活模式,使用createSSRApp
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
export function createApp() {
const app = createSSRApp(App)
const router = createRouter()
app.use(router)
return { app ,router}
}
- 修改第一步使用的createRouter ,使用方法将router进行返回以便于服务端使用,注意在服务端时仅能使用createMemoryHistory模式
import { createMemoryHistory, createRouter as createRouterBase, createWebHistory } from "vue-router"
export function createRouter(){
const isServer = typeof window === 'undefined'
const history = isServer ? createMemoryHistory() : createWebHistory()
return createRouterBase({
history: history,
routes:[
{
path:'/',
name:'home',
component: ()=>import('../components/HelloWorld.vue')
},{
path:'/about',
name:'about',
component: ()=>import('../components/About.vue')
}
]
})
}
- 在src目录下创建服务端和客户端入口文件entry-server.js和entry-client.js,名字可以任意区注意区分即可
import { createApp } from './main'
const { app, router } = createApp()
router.isReady().then(() => {
app.mount('#app')
})
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'
export async function render(url) {
const { app, router } = createApp()
await router.push(url)
await router.isReady()
const ctx = {}
const html = await renderToString(app, ctx)
const head= `meta标签用于SEO优化`
return { html, head }
}
- 修改根目录下的index.html,添加占位标记来替代服务端内容
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/entry-client.js"></script>
</body>
</html>
- 根目录创建server.js文件用于项目启动
import fs from 'node:fs/promises'
import express from 'express'
const isProduction = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 5173
const base = process.env.BASE || '/'
const templateHtml = isProduction
? await fs.readFile('./dist/client/index.html', 'utf-8')
: ''
const app = express()
let vite
if (!isProduction) {
const { createServer } = await import('vite')
vite = await createServer({
server: { middlewareMode: true },
appType: 'custom',
base,
})
app.use(vite.middlewares)
} else {
const compression = (await import('compression')).default
const sirv = (await import('sirv')).default
app.use(compression())
app.use(base, sirv('./dist/client', { extensions: [] }))
}
app.use('*all', async (req, res) => {
try {
const url = req.originalUrl.replace(base, '')
let template
let render
if (!isProduction) {
template = await fs.readFile('./index.html', 'utf-8')
template = await vite.transformIndexHtml(url, template)
render = (await vite.ssrLoadModule('/src/entry-server.js')).render
} else {
template = templateHtml
render = (await import('./dist/server/entry-server.js')).render
}
const rendered = await render(url)
const html = template
.replace(`<!--app-head-->`, rendered.head ?? '')
.replace(`<!--app-html-->`, rendered.html ?? '')
res.status(200).set({ 'Content-Type': 'text/html' }).send(html)
} catch (e) {
vite?.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`)
})
- 部署采用nginx+pm2进行管理,SSR线上部署主要解决接口跨域的问题,在开发环境的时候可以在vite.config.js文件设置代理路径,但在线上环境主要为nginx设置接口转发,所以在线上需要nginx区分前后端设置

server {
listen 5173; # 前端服务端口
server_name localhost;
location ^~ /api/ {
proxy_pass http:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
import fs from "node:fs/promises";
import os from "node:os";
import express from "express";
import compression from "compression";
import sirv from "sirv";
const port = process.env.PORT || 5174;
const base = process.env.BASE || "/";
function getServerIP() {
const interfaces = os.networkInterfaces();
for (const interfaceName in interfaces) {
const iface = interfaces[interfaceName];
for (const alias of iface) {
if (alias.family === "IPv4" && !alias.internal) {
return alias.address;
}
}
}
return "127.0.0.1";
}
const serverIP = getServerIP();
const templateHtml = await fs.readFile("./client/index.html", "utf-8");
const app = express();
app.use(compression());
app.use(base, sirv("./client", { extensions: [] }));
app.use("*all", async (req, res) => {
console.log(req.originalUrl);
if (req.originalUrl.startsWith("/api")) {
console.log("API requests are handled by Nginx");
return res.status(404).end("API requests are handled by Nginx");
}
try {
const url = req.originalUrl.replace(base, "");
const { render } = await import("./server/entry-server.js");
const rendered = await render(url);
const html = templateHtml.replace(`<!--app-head-->`, rendered.head ?? "");
res.status(200).set({ "Content-Type": "text/html" }).send(html);
} catch (e) {
console.error(e.stack);
res.status(500).end("Internal Server Error");
}
});
app.listen(port, () => {
console.log(`Server started at http://${serverIP}:${port}`);
});
pm2 start ./server.js --name demo-ssr --max-memory-restart 500M
- 修改package.json文件启动方式
"scripts": {
"dev": "node server",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
"preview": "cross-env NODE_ENV=production node server"
},
4.注意
- 打包后需要将server.js文件放入dist目录下,其位置决定了pm2管理时运行的位置,上述pm2命令其server.js位置放置在dist/server目录下
- 对于不同的UI框架可能会有部分UI框架无法正常引入,会提示没有install或者没有导出函数,这个暂未处理
- 需要在dist目录安装依赖express、compression、sirv