feat: Node 渲染服务器(express + renderPage + API 反代)
- server/index.mjs:生产模式 Express 服务,静态资源 immutable 缓存 - /api /uploads /media 等前缀反代到 Go API,xfwd 透传客户端 IP - Vike renderPage 承接 SSR,渲染失败返回 500 兜底页 - /healthz 健康检查端点
This commit is contained in:
114
frontend/server/index.mjs
Normal file
114
frontend/server/index.mjs
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Node 渲染服务器(生产模式)。
|
||||
*
|
||||
* 双进程架构对外单点:
|
||||
* - Node :3000 —— 前端静态资源 + Vike SSR(/、/board/:id、/post/:id 真渲染,其余 SPA 壳)
|
||||
* - Go :3001 —— 纯 API(/api、/uploads、/media、/oauth、robots/sitemap/favicon 反代至此)
|
||||
*
|
||||
* dev 模式不经过本文件:`npm run dev`(vike dev :5173)+ Go dev(API :3000)。
|
||||
*/
|
||||
import { Readable } from 'node:stream';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
|
||||
// 本文件仅用于生产(dev 走 `vike dev`);须在加载 vike/server 前定型环境
|
||||
process.env.NODE_ENV ??= 'production';
|
||||
const { renderPage } = await import('vike/server');
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const distClient = path.join(root, 'dist', 'client');
|
||||
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
const goApiUrl = process.env.GO_API_URL || 'http://127.0.0.1:3001';
|
||||
|
||||
/** 反代到 Go 的前缀(对齐旧 Go ServePublicSPA 的资源分发面) */
|
||||
const GO_PROXY_PREFIXES = [
|
||||
'/api',
|
||||
'/uploads',
|
||||
'/media',
|
||||
'/oauth',
|
||||
'/.well-known',
|
||||
'/robots.txt',
|
||||
'/sitemap.xml',
|
||||
'/favicon.ico',
|
||||
];
|
||||
|
||||
if (!fs.existsSync(distClient)) {
|
||||
console.error(`[j13-web] 未找到构建产物 ${distClient},请先执行 npm run build`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
app.use(compression());
|
||||
|
||||
// Node 自身探活(不依赖 Go,供容器健康检查)
|
||||
app.get('/healthz', (_req, res) => {
|
||||
res.status(200).send('ok');
|
||||
});
|
||||
|
||||
// Go 反代:透传 method/headers/body;xfwd 附加 X-Forwarded-*,供 Go 推导站点 base URL。
|
||||
// express app.use(prefix) 会剥掉挂载前缀(req.url 不含 prefix),故 target 须补回对应前缀
|
||||
for (const prefix of GO_PROXY_PREFIXES) {
|
||||
app.use(
|
||||
prefix,
|
||||
createProxyMiddleware({
|
||||
target: `${goApiUrl}${prefix}`,
|
||||
xfwd: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 前端静态资源:dist/client(含 /assets 与 /stickers),assets 带 contenthash → immutable
|
||||
app.use(
|
||||
express.static(distClient, {
|
||||
setHeaders(res, filePath) {
|
||||
if (filePath.includes(`${path.sep}assets${path.sep}`)) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// 其余路径交给 Vike:feed 页 SSR 文档,spa 页输出壳(客户端接管)
|
||||
app.use(async (req, res, next) => {
|
||||
try {
|
||||
const pageContext = await renderPage({
|
||||
urlOriginal: req.originalUrl,
|
||||
headersOriginal: req.headers,
|
||||
});
|
||||
const { httpResponse } = pageContext;
|
||||
if (!httpResponse) return next();
|
||||
|
||||
const { statusCode, headers, body } = httpResponse;
|
||||
res.statusCode = statusCode;
|
||||
for (const [name, value] of headers) res.setHeader(name, value);
|
||||
if (typeof body === 'string') {
|
||||
res.send(body);
|
||||
} else {
|
||||
Readable.fromWeb(body).pipe(res);
|
||||
}
|
||||
} catch (err) {
|
||||
// feedData/onRenderHtml 已内置降级(Go 不可达 → SPA 壳),此处仅兜 renderPage 本身的异常
|
||||
console.error('[j13-web] renderPage 失败:', err);
|
||||
if (!res.headersSent) {
|
||||
res
|
||||
.status(500)
|
||||
.type('html')
|
||||
.send(
|
||||
'<!DOCTYPE html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>服务器错误</title><body><p>页面渲染失败,请刷新重试。</p></body></html>',
|
||||
);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`[j13-web] Node 渲染服务器已启动: http://localhost:${port} (Go API: ${goApiUrl})`);
|
||||
});
|
||||
Reference in New Issue
Block a user