feat(server): 单域托管 client/dist 与 SPA 回退(E12)
- 新增 SERVE_CLIENT_DIST/CLIENT_DIST_DIR 配置 - 开启后注册 @fastify/static 并对非 /api GET(html) 回退 index.html - docker-compose.yml 与 README 补充用法
这个提交包含在:
+15
@@ -179,6 +179,19 @@ npm run dev
|
||||
- 通过 Vite 代理将 `/api` 转发到 `http://127.0.0.1:3000`。
|
||||
- Access Token 仅保存在内存,Refresh Token 通过 HttpOnly Cookie 续签。
|
||||
|
||||
### 4.7 单域托管 client/dist(可选,试运行建议)
|
||||
```bash
|
||||
cd /home/shunli/Projects/hyb-oa/client
|
||||
npm run build
|
||||
|
||||
cd /home/shunli/Projects/hyb-oa/server
|
||||
SERVE_CLIENT_DIST=true npm run dev
|
||||
```
|
||||
|
||||
说明:
|
||||
- 访问 `http://127.0.0.1:3000` 即可打开前端(同域 cookie,减少 CORS 复杂度)。
|
||||
- SPA 路由(如 `/dashboard`)会回退到 `index.html`;`/api/*` 不参与回退。
|
||||
|
||||
---
|
||||
|
||||
## 5. P2 API 摘要
|
||||
@@ -231,6 +244,8 @@ npm run job:ocr-once
|
||||
- `JWT_SECRET`: JWT & signed-url token 签名密钥
|
||||
- `APP_TIMEZONE`: 默认 `Asia/Shanghai`
|
||||
- `STORAGE_DIR`: 默认 `generated/storage`(相对 `server/`)
|
||||
- `SERVE_CLIENT_DIST`: 是否由 server 托管 `client/dist`(默认 `false`)
|
||||
- `CLIENT_DIST_DIR`: 前端构建产物目录(默认 `../client/dist`,相对 `server/`)
|
||||
- `SIGNED_URL_TTL_SECONDS`: 默认 `600`
|
||||
- `MAX_UPLOAD_SIZE_BYTES`: 默认 `10485760`(10MB)
|
||||
- `RATE_LIMIT_ENABLED`: 是否启用关键接口限流(默认:production=true,其它环境=false)
|
||||
|
||||
+20
-1
@@ -9,6 +9,7 @@ const cookie = require("@fastify/cookie");
|
||||
const multipart = require("@fastify/multipart");
|
||||
const helmet = require("@fastify/helmet");
|
||||
const rateLimit = require("@fastify/rate-limit");
|
||||
const staticPlugin = require("@fastify/static");
|
||||
|
||||
const { getConfig } = require("./config");
|
||||
const authPlugin = require("./plugins/auth");
|
||||
@@ -95,11 +96,29 @@ async function buildApp() {
|
||||
},
|
||||
});
|
||||
|
||||
if (config.serveClientDist) {
|
||||
await app.register(staticPlugin, {
|
||||
root: config.clientDistDir,
|
||||
});
|
||||
app.log.info({ dir: config.clientDistDir }, "Client dist static serving enabled");
|
||||
}
|
||||
|
||||
app.addHook("onRequest", async (request, reply) => {
|
||||
reply.header("x-request-id", request.id);
|
||||
});
|
||||
|
||||
app.setNotFoundHandler(async () => {
|
||||
app.setNotFoundHandler(async (request, reply) => {
|
||||
if (
|
||||
config.serveClientDist &&
|
||||
request.method === "GET" &&
|
||||
!request.url.startsWith("/api") &&
|
||||
typeof request.headers.accept === "string" &&
|
||||
request.headers.accept.includes("text/html") &&
|
||||
typeof reply.sendFile === "function"
|
||||
) {
|
||||
return reply.type("text/html").sendFile("index.html");
|
||||
}
|
||||
|
||||
throw new AppError({
|
||||
code: "SYS_404",
|
||||
message: "Not Found",
|
||||
|
||||
@@ -14,6 +14,7 @@ function getConfig() {
|
||||
const nodeEnv = process.env.NODE_ENV ?? "development";
|
||||
const serverRoot = path.resolve(__dirname, "..");
|
||||
const storageDir = process.env.STORAGE_DIR ?? "generated/storage";
|
||||
const clientDistDir = process.env.CLIENT_DIST_DIR ?? "../client/dist";
|
||||
|
||||
return {
|
||||
nodeEnv,
|
||||
@@ -40,6 +41,8 @@ function getConfig() {
|
||||
|
||||
appTimezone: process.env.APP_TIMEZONE ?? "Asia/Shanghai",
|
||||
storageDir: path.resolve(serverRoot, storageDir),
|
||||
serveClientDist: process.env.SERVE_CLIENT_DIST === "true",
|
||||
clientDistDir: path.resolve(serverRoot, clientDistDir),
|
||||
signedUrlTtlSeconds: Number(process.env.SIGNED_URL_TTL_SECONDS ?? 600),
|
||||
maxUploadSizeBytes: Number(process.env.MAX_UPLOAD_SIZE_BYTES ?? 10 * 1024 * 1024),
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户