mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 09:06:36 +08:00
[优化] 更新开发环境配置,统一代理 HTTP 和 WebSocket 请求
This commit is contained in:
@@ -2,14 +2,17 @@
|
|||||||
|
|
||||||
ATSFlare 管理端新版前端已切换为 Next.js App Router + TypeScript + Tailwind CSS 工程。
|
ATSFlare 管理端新版前端已切换为 Next.js App Router + TypeScript + Tailwind CSS 工程。
|
||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
corepack enable
|
corepack enable
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
# 本地开发
|
# 本地开发(3001 前端同源代理后端 HTTP/WS)
|
||||||
pnpm dev
|
pnpm dev
|
||||||
|
|
||||||
|
# 生产模式启动
|
||||||
|
pnpm start
|
||||||
|
|
||||||
# 类型检查
|
# 类型检查
|
||||||
pnpm typecheck
|
pnpm typecheck
|
||||||
@@ -28,8 +31,10 @@ pnpm build
|
|||||||
|
|
||||||
* 构建采用 Next.js 静态导出模式。
|
* 构建采用 Next.js 静态导出模式。
|
||||||
* `pnpm build` 会先生成 `out/`,随后自动复制为 Go Server 兼容的 `build/` 目录。
|
* `pnpm build` 会先生成 `out/`,随后自动复制为 Go Server 兼容的 `build/` 目录。
|
||||||
* 默认 API Base URL 为 `/api`,如需覆盖可在构建时设置 `NEXT_PUBLIC_API_BASE_URL`。
|
* 默认 API Base URL 为 `/api`,如需覆盖可在构建时设置 `NEXT_PUBLIC_API_BASE_URL`。
|
||||||
* 构建版本号可通过 `NEXT_PUBLIC_APP_VERSION` 注入,例如 `NEXT_PUBLIC_APP_VERSION=v0.4.0 pnpm build`。
|
* 构建版本号可通过 `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/`:业务模块
|
* `features/`:业务模块
|
||||||
* `components/`:复用组件
|
* `components/`:复用组件
|
||||||
* `lib/`:请求、环境变量、工具与常量
|
* `lib/`:请求、环境变量、工具与常量
|
||||||
* `tests/`:测试代码
|
* `tests/`:测试代码
|
||||||
|
|||||||
+13
-13
@@ -1,14 +1,14 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const publicEnvSchema = z.object({
|
const publicEnvSchema = z.object({
|
||||||
NEXT_PUBLIC_API_BASE_URL: z.string().default('/api'),
|
NEXT_PUBLIC_API_BASE_URL: z.string().default('/api'),
|
||||||
NEXT_PUBLIC_APP_VERSION: z.string().default('dev'),
|
NEXT_PUBLIC_APP_VERSION: z.string().default('dev'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawEnv = {
|
const rawEnv = {
|
||||||
NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
|
NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
|
||||||
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,
|
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsedEnv = publicEnvSchema.parse(rawEnv);
|
const parsedEnv = publicEnvSchema.parse(rawEnv);
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ function normalizeBaseUrl(value: string) {
|
|||||||
return value.replace(/\/+$/, '');
|
return value.replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicEnv = {
|
export const publicEnv = {
|
||||||
apiBaseUrl: normalizeBaseUrl(parsedEnv.NEXT_PUBLIC_API_BASE_URL),
|
apiBaseUrl: normalizeBaseUrl(parsedEnv.NEXT_PUBLIC_API_BASE_URL),
|
||||||
appVersion: parsedEnv.NEXT_PUBLIC_APP_VERSION,
|
appVersion: parsedEnv.NEXT_PUBLIC_APP_VERSION,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.6.2",
|
"packageManager": "pnpm@10.6.2",
|
||||||
"scripts": {
|
"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",
|
"build": "next build",
|
||||||
"postbuild": "node scripts/prepare-static-export.mjs",
|
"postbuild": "node scripts/prepare-static-export.mjs",
|
||||||
"start": "NEXT_DEV_PROXY=true next dev -p 3001",
|
"start": "next start",
|
||||||
"start:prod": "next start",
|
|
||||||
"lint": "eslint . --max-warnings=0",
|
"lint": "eslint . --max-warnings=0",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"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
|
cd atsf_server/web
|
||||||
corepack enable
|
corepack enable
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm start
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
* `pnpm start` 默认以 Next.js 开发模式启动,监听 `http://127.0.0.1:3001`
|
* `pnpm dev` 默认以 Next.js 开发模式启动,监听 `http://127.0.0.1:3001`
|
||||||
* 开发服务器会把 `/api/*` 请求反向代理到 `http://127.0.0.1:3000`
|
* 开发服务器会通过同源代理把 `/api/*` 的 HTTP 与 WebSocket 请求统一转发到 `NEXT_DEV_BACKEND_URL`,默认指向 `http://127.0.0.1:3000`
|
||||||
* 如需改后端地址,可在启动前设置 `NEXT_DEV_BACKEND_URL`
|
* 如需改后端地址,可在启动前设置 `NEXT_DEV_BACKEND_URL`
|
||||||
* 这种模式用于本地热更新开发;正式运行和交付仍以 `pnpm build` 后由 Go Server 托管为准
|
* 这种模式用于本地热更新开发;正式运行和交付仍以 `pnpm build` 后由 Go Server 托管为准
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user