mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
[优化] 更新开发环境配置,统一代理 HTTP 和 WebSocket 请求
This commit is contained in:
@@ -2,14 +2,17 @@
|
||||
|
||||
ATSFlare 管理端新版前端已切换为 Next.js App Router + TypeScript + Tailwind CSS 工程。
|
||||
|
||||
## 常用命令
|
||||
## 常用命令
|
||||
|
||||
```shell
|
||||
corepack enable
|
||||
pnpm install
|
||||
|
||||
# 本地开发
|
||||
pnpm dev
|
||||
# 本地开发(3001 前端同源代理后端 HTTP/WS)
|
||||
pnpm dev
|
||||
|
||||
# 生产模式启动
|
||||
pnpm start
|
||||
|
||||
# 类型检查
|
||||
pnpm typecheck
|
||||
@@ -28,8 +31,10 @@ pnpm build
|
||||
|
||||
* 构建采用 Next.js 静态导出模式。
|
||||
* `pnpm build` 会先生成 `out/`,随后自动复制为 Go Server 兼容的 `build/` 目录。
|
||||
* 默认 API Base URL 为 `/api`,如需覆盖可在构建时设置 `NEXT_PUBLIC_API_BASE_URL`。
|
||||
* 构建版本号可通过 `NEXT_PUBLIC_APP_VERSION` 注入,例如 `NEXT_PUBLIC_APP_VERSION=v0.4.0 pnpm build`。
|
||||
* 默认 API Base URL 为 `/api`,如需覆盖可在构建时设置 `NEXT_PUBLIC_API_BASE_URL`。
|
||||
* 构建版本号可通过 `NEXT_PUBLIC_APP_VERSION` 注入,例如 `NEXT_PUBLIC_APP_VERSION=v0.4.0 pnpm build`。
|
||||
* `pnpm dev` 会以开发模式启动 `http://127.0.0.1:3001`,并通过同源代理把 `/api/*` 的 HTTP 与 WebSocket 请求统一转发到 `NEXT_DEV_BACKEND_URL`,默认是 `http://127.0.0.1:3000`。
|
||||
* `pnpm start` 用于启动已构建产物,对应 Next 生产模式。
|
||||
|
||||
## 目录约定
|
||||
|
||||
@@ -37,4 +42,4 @@ pnpm build
|
||||
* `features/`:业务模块
|
||||
* `components/`:复用组件
|
||||
* `lib/`:请求、环境变量、工具与常量
|
||||
* `tests/`:测试代码
|
||||
* `tests/`:测试代码
|
||||
|
||||
+13
-13
@@ -1,14 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const publicEnvSchema = z.object({
|
||||
NEXT_PUBLIC_API_BASE_URL: z.string().default('/api'),
|
||||
NEXT_PUBLIC_APP_VERSION: z.string().default('dev'),
|
||||
});
|
||||
|
||||
const rawEnv = {
|
||||
NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
|
||||
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
};
|
||||
const publicEnvSchema = z.object({
|
||||
NEXT_PUBLIC_API_BASE_URL: z.string().default('/api'),
|
||||
NEXT_PUBLIC_APP_VERSION: z.string().default('dev'),
|
||||
});
|
||||
|
||||
const rawEnv = {
|
||||
NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
|
||||
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
};
|
||||
|
||||
const parsedEnv = publicEnvSchema.parse(rawEnv);
|
||||
|
||||
@@ -20,7 +20,7 @@ function normalizeBaseUrl(value: string) {
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export const publicEnv = {
|
||||
apiBaseUrl: normalizeBaseUrl(parsedEnv.NEXT_PUBLIC_API_BASE_URL),
|
||||
appVersion: parsedEnv.NEXT_PUBLIC_APP_VERSION,
|
||||
};
|
||||
export const publicEnv = {
|
||||
apiBaseUrl: normalizeBaseUrl(parsedEnv.NEXT_PUBLIC_API_BASE_URL),
|
||||
appVersion: parsedEnv.NEXT_PUBLIC_APP_VERSION,
|
||||
};
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.6.2",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "NEXT_DEV_BACKEND_URL=${NEXT_DEV_BACKEND_URL:-http://127.0.0.1:3000} node scripts/dev-server.mjs",
|
||||
"build": "next build",
|
||||
"postbuild": "node scripts/prepare-static-export.mjs",
|
||||
"start": "NEXT_DEV_PROXY=true next dev -p 3001",
|
||||
"start:prod": "next start",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
|
||||
import next from 'next';
|
||||
|
||||
const port = Number.parseInt(process.env.PORT ?? '3001', 10);
|
||||
const hostname = process.env.HOSTNAME || '127.0.0.1';
|
||||
const backendBaseUrl = new URL(
|
||||
(process.env.NEXT_DEV_BACKEND_URL || 'http://127.0.0.1:3000').replace(/\/+$/, ''),
|
||||
);
|
||||
|
||||
const app = next({
|
||||
dev: true,
|
||||
hostname,
|
||||
port,
|
||||
});
|
||||
|
||||
let handle;
|
||||
let handleUpgrade;
|
||||
|
||||
function isApiRequest(url = '') {
|
||||
return url.startsWith('/api/');
|
||||
}
|
||||
|
||||
function createProxyRequestOptions(req, targetUrl) {
|
||||
const headers = {
|
||||
...req.headers,
|
||||
host: targetUrl.host,
|
||||
};
|
||||
|
||||
if (headers.origin) {
|
||||
headers.origin = targetUrl.origin;
|
||||
}
|
||||
|
||||
return {
|
||||
protocol: targetUrl.protocol,
|
||||
hostname: targetUrl.hostname,
|
||||
port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
|
||||
method: req.method,
|
||||
path: `${targetUrl.pathname}${targetUrl.search}`,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
function writeProxyError(res, error) {
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: `开发代理连接后端失败: ${error.message}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function proxyHttpRequest(req, res) {
|
||||
const targetUrl = new URL(req.url || '/', backendBaseUrl);
|
||||
const requestImpl = targetUrl.protocol === 'https:' ? https.request : http.request;
|
||||
const proxyReq = requestImpl(createProxyRequestOptions(req, targetUrl), (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode || 502, proxyRes.statusMessage, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
});
|
||||
|
||||
proxyReq.on('error', (error) => {
|
||||
if (!res.headersSent) {
|
||||
writeProxyError(res, error);
|
||||
return;
|
||||
}
|
||||
res.destroy(error);
|
||||
});
|
||||
|
||||
req.pipe(proxyReq);
|
||||
}
|
||||
|
||||
function formatUpgradeResponseHeaders(statusCode, statusMessage, headers) {
|
||||
const lines = [`HTTP/1.1 ${statusCode} ${statusMessage}`];
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
lines.push(`${key}: ${item}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
lines.push('', '');
|
||||
return lines.join('\r\n');
|
||||
}
|
||||
|
||||
function proxyWebSocketUpgrade(req, socket, head) {
|
||||
const targetUrl = new URL(req.url || '/', backendBaseUrl);
|
||||
const requestImpl = targetUrl.protocol === 'https:' ? https.request : http.request;
|
||||
const proxyReq = requestImpl(createProxyRequestOptions(req, targetUrl));
|
||||
|
||||
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
|
||||
socket.write(
|
||||
formatUpgradeResponseHeaders(
|
||||
proxyRes.statusCode || 101,
|
||||
proxyRes.statusMessage || 'Switching Protocols',
|
||||
proxyRes.headers,
|
||||
),
|
||||
);
|
||||
|
||||
if (head.length > 0) {
|
||||
proxySocket.write(head);
|
||||
}
|
||||
if (proxyHead.length > 0) {
|
||||
socket.write(proxyHead);
|
||||
}
|
||||
|
||||
proxySocket.pipe(socket).pipe(proxySocket);
|
||||
|
||||
proxySocket.on('error', () => socket.destroy());
|
||||
socket.on('error', () => proxySocket.destroy());
|
||||
});
|
||||
|
||||
proxyReq.on('response', (proxyRes) => {
|
||||
socket.write(
|
||||
formatUpgradeResponseHeaders(
|
||||
proxyRes.statusCode || 502,
|
||||
proxyRes.statusMessage || 'Bad Gateway',
|
||||
proxyRes.headers,
|
||||
),
|
||||
);
|
||||
proxyRes.pipe(socket);
|
||||
});
|
||||
|
||||
proxyReq.on('error', () => {
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
proxyReq.end();
|
||||
}
|
||||
|
||||
await app.prepare();
|
||||
handle = app.getRequestHandler();
|
||||
handleUpgrade = app.getUpgradeHandler();
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (isApiRequest(req.url)) {
|
||||
proxyHttpRequest(req, res);
|
||||
return;
|
||||
}
|
||||
handle(req, res);
|
||||
});
|
||||
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
if (isApiRequest(req.url)) {
|
||||
proxyWebSocketUpgrade(req, socket, head);
|
||||
return;
|
||||
}
|
||||
handleUpgrade(req, socket, head);
|
||||
});
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
console.log(`ATSFlare web dev server listening on http://${hostname}:${port}`);
|
||||
console.log(`Proxying /api/* and websocket upgrades to ${backendBaseUrl.href}`);
|
||||
});
|
||||
+3
-3
@@ -47,13 +47,13 @@ pnpm build
|
||||
cd atsf_server/web
|
||||
corepack enable
|
||||
pnpm install
|
||||
pnpm start
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
* `pnpm start` 默认以 Next.js 开发模式启动,监听 `http://127.0.0.1:3001`
|
||||
* 开发服务器会把 `/api/*` 请求反向代理到 `http://127.0.0.1:3000`
|
||||
* `pnpm dev` 默认以 Next.js 开发模式启动,监听 `http://127.0.0.1:3001`
|
||||
* 开发服务器会通过同源代理把 `/api/*` 的 HTTP 与 WebSocket 请求统一转发到 `NEXT_DEV_BACKEND_URL`,默认指向 `http://127.0.0.1:3000`
|
||||
* 如需改后端地址,可在启动前设置 `NEXT_DEV_BACKEND_URL`
|
||||
* 这种模式用于本地热更新开发;正式运行和交付仍以 `pnpm build` 后由 Go Server 托管为准
|
||||
|
||||
|
||||
Reference in New Issue
Block a user