This commit is contained in:
2026-09-30 14:27:39 +08:00
parent 707711b3bd
commit 7ba6a24505
8 changed files with 45 additions and 19 deletions
+18 -4
View File
@@ -109,16 +109,30 @@ Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查国内 PDA 的独
## TEST H5 静态托管
发布构建使用 `/pda-domestic/` 资源 base 与 hash 路由;开发服务仍使用根路径。浏览器入口为 `/pda-domestic/#/`,不能将 Vite 开发代理当作静态服务器配置。
发布构建使用 `/` 资源 base 与 hash 路由,部署到本应用独立域名根目录,浏览器入口为 `/#/`。开发服务仍使用根路径;不能将 Vite 开发代理当作静态服务器配置。
国内使用本机已安装 HBuilderX:`cli.exe project open --path <本仓库绝对路径>` 后执行 `cli.exe publish web --project <本仓库绝对路径> --webHosting false --sourceMap false`,仅导出本地 Web 静态文件;本次验证编译器 5.26,不上传云托管。
`deploy/Dockerfile.h5` 只复制已构建 `unpackage/dist/build/web/` 及静态 Nginx 配置,不在镜像中复制源码、依赖或重新编译。`.dockerignore` 只放行这两类输入。容器静态目录为 `/srv/pda-h5/pda-domestic/`。
`deploy/Dockerfile.h5` 只复制已构建 `unpackage/dist/build/web/` 及静态 Nginx 配置,不在镜像中复制源码、依赖或重新编译。`.dockerignore` 只放行这两类输入。容器静态目录为 `/srv/pda-h5/`。
从本仓库执行 `docker compose -f deploy/docker-compose.h5.test.yml build --pull=false`,再由获授权的部署流程执行 `docker compose -f deploy/docker-compose.h5.test.yml up -d --wait --wait-timeout 120`。必须设置 `IMAGE_TAG`,`NGINX_BASE_IMAGE` 可指定已缓存的兼容 Alpine Nginx 镜像。容器名及网络别名为 `pda-domestic-h5-test`,仅在外部 `firstleg-test-network` 暴露容器 80,不发布宿主端口。健康检查同时请求 `/healthz` 与 `/pda-domestic/` 的实际首页。
从本仓库执行 `docker compose -f deploy/docker-compose.h5.test.yml build --pull=false`,再由获授权的部署流程执行 `docker compose -f deploy/docker-compose.h5.test.yml up -d --wait --wait-timeout 120`。必须设置 `IMAGE_TAG`,`NGINX_BASE_IMAGE` 可指定已缓存的兼容 Alpine Nginx 镜像。容器名及网络别名为 `pda-domestic-h5-test`,仅在外部 `firstleg-test-network` 暴露容器 80,不发布宿主端口。健康检查同时请求 `/healthz` 与 `/` 的实际首页。
Admin Nginx 将 `/pda-domestic/` 原样代理至 `http://pda-domestic-h5-test:80`,不去掉路径前缀;无尾斜杠入口重定向为 `/pda-domestic/`。PDA 容器只提供本应用静态目录,其他路径返回 404,缺失资源不回退首页,HTML 使用 `Cache-Control: no-store`。API 全部由 Admin 入口代理:国内 `/gateway-api/` 转统一 Gateway `/api/`,海外同源 `/api/` 保持路径。入口须保留 Authorization,支持文件上传下载及至少 11m 请求体。环境域名与上游只在部署侧配置,不修改宿主 Nginx 权限,也不编入 PDA 源码。
独立域名入口将 `/` 代理至本应用静态容器根目录,或将构建目录内容直接发布到站点根目录;不再挂载 PDA 子路径。缺失资源返回 404,不回退首页,HTML 使用 `Cache-Control: no-store`。域名入口还须配置同源 API 代理:两端均将 `/gateway-api/` 转统一 Gateway `/api/`。入口须保留 Authorization,支持文件上传下载及至少 11m 请求体。域名与上游只在部署侧配置,不编入 PDA 源码。
部署配置检查:`node --test tests/h5-deployment.test.mjs`。没有 Docker 运行时的本地合同检查不能替代目标主机 `docker compose config`、Nginx 启动与真实 HTTP 验收。
构建后执行 `node scripts/verify-h5.mjs`,核对 HTML/JS/CSS 资源路径、同源 API 和无本机/旧海外配置泄漏。该检查与本地构建仅证明产物;TEST 页面登录、仓权限、业务操作和 Android 真机另行验收。
### 两端统一 API 配置
两端的 `VITE_API_BASE_URL` 均为统一 Gateway 绝对地址(含 `/api`);`VITE_H5_API_BASE_URL=/gateway-api`;`API_PROXY_TARGET` 为同一 Gateway 源地址(不含 `/api`)。实际地址只放在未提交的 `.env.local`;修改后重启开发服务,发布前重新打包。
两端独立域名入口均配置以下规则;`gateway:9000` 仅适用于同一 Docker 网络,其他部署须替换成实际统一 Gateway 上游。静态容器只提供页面文件,Vite 代理不会随静态包发布。
```nginx
location ^~ /gateway-api/ {
proxy_pass http://gateway:9000/api/;
proxy_set_header Authorization $http_authorization;
client_max_body_size 11m;
}
```
+1 -1
View File
@@ -3,7 +3,7 @@ let selectedApiRoot = ''
selectedApiRoot = import.meta.env.VITE_API_BASE_URL || '/api'
// #endif
// H5 必须请求同源地址,由 Vite 开发服务器代理到国内 Gateway,避免浏览器 CORS。
// H5 必须请求同源地址,由 Vite 开发服务器代理到统一 Gateway,避免浏览器 CORS。
// 前缀不能使用 /api,否则会截获本项目 api/ 目录下的前端模块请求。
// #ifdef H5
selectedApiRoot = import.meta.env.VITE_H5_API_BASE_URL || '/gateway-api'
+1 -1
View File
@@ -1,5 +1,5 @@
ARG NGINX_BASE_IMAGE=nginx:1.27-alpine
FROM ${NGINX_BASE_IMAGE}
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY unpackage/dist/build/web/ /srv/pda-h5/pda-domestic/
COPY unpackage/dist/build/web/ /srv/pda-h5/
EXPOSE 80
+1 -1
View File
@@ -12,7 +12,7 @@ services:
expose:
- "80"
healthcheck:
test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1/healthz && wget -qO /dev/null http://127.0.0.1/pda-domestic/"]
test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1/healthz && wget -qO /dev/null http://127.0.0.1/"]
interval: 5s
timeout: 3s
retries: 12
+1 -3
View File
@@ -8,11 +8,9 @@ server {
add_header Cache-Control "no-store" always;
return 200 "ok\n";
}
location = /pda-domestic { return 302 /pda-domestic/; }
location ^~ /pda-domestic/ {
location / {
index index.html;
try_files $uri $uri/ =404;
add_header Cache-Control "no-store" always;
}
location / { return 404; }
}
+2 -2
View File
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url'
import { createHash } from 'node:crypto'
const project = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const output = resolve(project, 'unpackage/dist/build/web')
const base = '/pda-domestic/'
const base = '/'
function walk(dir) { return readdirSync(dir).flatMap(name => { const path = resolve(dir, name); return statSync(path).isDirectory() ? walk(path) : [path] }) }
assert.ok(existsSync(resolve(output, 'index.html')), 'Build H5 before verifying')
const files = walk(output).sort()
@@ -17,7 +17,7 @@ for (const ref of refs) {
assert.ok(existsSync(resolve(output, ref.slice(base.length))), `Missing asset: ${ref}`)
}
const js = files.filter(path => path.endsWith('.js')).map(path => readFileSync(path, 'utf8')).join('\n')
assert.ok(js.includes(base), 'Runtime router and assets must know the deployment base')
assert.doesNotMatch(html + js, /\/pda-(?:domestic|overseas)\//, 'Build must not reference the retired subdirectory deployment')
assert.doesNotMatch(js, /VITE_ROOT_DIR|VITE_AUTH_API_BASE_URL|VITE_OVERSEAS_API_BASE_URL|overseas-api|overseasAccessToken/, 'Build contains broad local env or old overseas config')
assert.doesNotMatch(js, /https?:\/\/(?:localhost|127\.0\.0\.1):\d+/, 'Build must not depend on a local dev server')
assert.doesNotMatch(js, /https?:\/\/[^"'\s]+\/api["']/, 'API must be same-origin for this H5 deployment')
+20 -6
View File
@@ -6,7 +6,7 @@ const read = path => readFile(new URL(path, import.meta.url), 'utf8')
test('H5 image copies only compiled assets and static nginx configuration', async () => {
const dockerfile = await read('../deploy/Dockerfile.h5')
const copies = dockerfile.split(/\r?\n/).filter(line => line.startsWith('COPY '))
assert.deepEqual(copies, ['COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf', 'COPY unpackage/dist/build/web/ /srv/pda-h5/pda-domestic/'])
assert.deepEqual(copies, ['COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf', 'COPY unpackage/dist/build/web/ /srv/pda-h5/'])
assert.match(dockerfile, /ARG NGINX_BASE_IMAGE=/)
assert.match(dockerfile, /FROM \$\{NGINX_BASE_IMAGE\}/)
assert.doesNotMatch(dockerfile, /RUN |npm|node_modules|COPY \. /)
@@ -15,14 +15,14 @@ test('H5 image copies only compiled assets and static nginx configuration', asyn
assert.ok(ignore.includes('!unpackage/dist/build/web/**'))
assert.doesNotMatch(ignore, /!\.env|!node_modules|!src/)
})
test('static server preserves the app prefix and returns genuine missing-resource errors', async () => {
test('static server serves the domain root and returns genuine missing-resource errors', async () => {
const nginx = await read('../deploy/nginx.conf')
assert.match(nginx, /listen 80;/)
assert.ok(nginx.includes('location ^~ /pda-domestic/'))
assert.ok(nginx.includes('location /'))
assert.match(nginx, /root \/srv\/pda-h5;/)
assert.match(nginx, /try_files \$uri \$uri\/ =404;/)
assert.match(nginx, /Cache-Control "no-store" always/)
assert.match(nginx, /location \/ \{ return 404; \}/)
assert.doesNotMatch(nginx, /pda-domestic|pda-overseas/)
assert.match(nginx, /location = \/healthz/)
assert.doesNotMatch(nginx, /proxy_pass|gateway-api|try_files[^;]*index\.html/)
})
@@ -36,7 +36,7 @@ test('TEST H5 deployment has a required image tag and internal-only dual health
assert.match(compose, /external: true/)
assert.match(compose, /name: firstleg-test-network/)
assert.match(compose, /expose:\s*\n\s*- "80"/)
assert.ok(compose.includes('http://127.0.0.1/healthz && wget -qO /dev/null http://127.0.0.1/pda-domestic/'))
assert.ok(compose.includes('http://127.0.0.1/healthz && wget -qO /dev/null http://127.0.0.1/'))
assert.doesNotMatch(compose, /\bports:|volumes:|API_UPSTREAM|privileged:|pid: host/)
})
@@ -53,8 +53,22 @@ test('only H5 production overrides the toolchain asset base', async () => {
process: { env: { UNI_PLATFORM: platform }, cwd: () => '/project' }
})
assert.equal(factory({ mode }).base,
platform === 'h5' && mode === 'production' ? '/pda-domestic/' : undefined,
platform === 'h5' && mode === 'production' ? '/' : undefined,
`${platform}:${mode}`)
}
}
})
test('H5 proxy uses the shared gateway prefix and preserves API suffixes', async () => {
const source = (await read('../vite.config.js')).replace(/^import .*$/gm, '').replace('export default ', '')
const factory = runInNewContext(source, {
defineConfig: value => value, loadEnv: () => ({ API_PROXY_TARGET: 'https://gateway.example.com' }),
uni: () => ({}), process: { env: { UNI_PLATFORM: 'h5' }, cwd: () => '/project' }
})
const proxy = factory({ mode: 'development' }).server.proxy
assert.deepEqual(Object.keys(proxy), ['/gateway-api'])
assert.equal(proxy['/gateway-api'].target, 'https://gateway.example.com')
for (const suffix of ['/auth/login', '/pda/v1/last-leg/photos/1/download?preview=1']) {
assert.equal(proxy['/gateway-api'].rewrite(`/gateway-api${suffix}`), `/api${suffix}`)
}
})
+1 -1
View File
@@ -26,7 +26,7 @@ export default defineConfig(({ mode }) => {
const proxyTarget = env.API_PROXY_TARGET || 'http://127.0.0.1:9000'
return {
base: process.env.UNI_PLATFORM === 'h5' && mode === 'production' ? '/pda-domestic/' : undefined,
base: process.env.UNI_PLATFORM === 'h5' && mode === 'production' ? '/' : undefined,
plugins: [uni()],
define: {
'import.meta.env.VITE_PDA_UPDATE_URL': JSON.stringify(env.VITE_PDA_UPDATE_URL || '')