Files
frontend-miniapp/docs/PRODUCTION_MOBILE_DEPLOYMENT_RUNBOOK.md
cxk 57d1e315b5
Some checks failed
CI / verify (push) Has been cancelled
fix: 修复 H5 构建后处理编码校验
2026-07-15 02:24:03 +08:00

450 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# frontend-miniapp 正式环境部署手册
最后更新2026-07-15
## 适用范围
本文档适用于将本地仓库 `F:\深圳自然博物馆\frontend-miniapp` 的 H5 产物部署到深圳自然博物馆正式环境。
- 正式域名:`https://guide.sznhmuseum.org.cn`
- 主节点:`172.20.14.21`
- 部署目录:`/data/nginx/html/mobile`
- Nginx 容器:`sgs-nginx`
- 同步脚本:`/data/scripts/sync-frontend-nodes.sh`
- 同步节点:`172.20.14.23``172.20.14.26``172.20.14.27``172.20.14.33`
- 远程仓库:`http://192.168.0.93:3333/lyf/frontend-miniapp.git`
不要把服务器密码、证书私钥、token、真实后台账号、腾讯地图正式 key 写入仓库文档或提交记录。
## 部署前检查
进入项目目录:
```powershell
cd 'F:\深圳自然博物馆\frontend-miniapp'
```
检查分支、远程仓库和本地改动:
```powershell
git status --short --branch --untracked-files=all
git log -1 --pretty=format:'%h %s'
git remote -v
```
正式环境当前依赖一组本地生产补丁,拉取远程代码前必须保护。常见补丁文件包括:
```text
.env.production
src/config/dataSource.ts
src/data/adapters/guideDataAdapter.ts
src/data/providers/staticMuseumContentProvider.ts
src/env.d.ts
src/repositories/ExplainRepository.ts
src/usecases/explainUseCase.ts
src/utils/publicUrl.ts
static/guide-data/exhibits.json
static/guide-data/guide-contents.json
static/guide-data/guide-stops.json
static/guide-data/manifest.json
```
## 拉取远程最新代码
先临时保存本地生产补丁:
```powershell
$stamp = Get-Date -Format 'yyyyMMddHHmmss'
$msg = "pre-deploy-local-prod-fixes-$stamp"
if (git status --porcelain) {
git stash push -u -m $msg
Write-Output "STASHED=$msg"
}
```
拉取远程最新代码:
```powershell
git fetch origin
git pull --ff-only origin master
git log -1 --pretty=format:'%h %s'
```
恢复生产补丁:
```powershell
$stash = git stash list | Select-String $msg | Select-Object -First 1
if ($stash) {
$stashRef = ($stash.Line -split ':')[0]
git stash pop $stashRef
}
git status --short --branch --untracked-files=all
```
如出现冲突,先解决冲突并重新确认生产配置,再继续构建。
## 本地构建
安装依赖并做类型检查:
```powershell
corepack pnpm install --frozen-lockfile
$env:NODE_OPTIONS='--max-old-space-size=16384'
corepack pnpm type-check
```
设置正式构建变量。腾讯地图 key 只通过环境变量注入,不写入仓库:
```powershell
$env:NODE_OPTIONS='--max-old-space-size=16384'
$env:VITE_TENCENT_MAP_KEY='<正式腾讯地图 Web Key>'
$env:VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST='172.20.14.21'
$env:VITE_EXPLAIN_ALLOW_STATIC_FALLBACK='true'
```
执行正式 H5 构建:
```powershell
corepack pnpm build:h5
```
`build:h5` 会执行:
1. `uni build -p h5 --mode production`
2. `node scripts/finalize-h5-build.cjs --require-tencent-map-key`
`finalize-h5-build.cjs` 会自动完成:
- 拷贝 `static/nav-assets``static/guide-data``static/guide``static/icons``static/explain``static/sgs-map-sdk``static/three` 和字体资源。
- 使用 Node 按 UTF-8 安全替换构建产物里的 `__REPLACE_WITH_TENCENT_MAP_WEB_KEY__`
- 对全部构建 JS 执行 `node --check`,防止语法错误产物上线。
- 扫描并阻断 `1.92.206.90``guide.whaoyue.com`、腾讯地图 key 占位符等不应进入正式环境的字符串。
- 校验核心静态 JSON 和关键图标文件存在。
如 terser 压缩阶段出现内存不足,可使用不压缩构建:
```powershell
corepack pnpm run build:h5:no-minify
```
严禁再用 PowerShell `Get-Content` / `Set-Content` 手工替换构建后的 JS。PowerShell 默认编码容易破坏 UTF-8 中文注释或字符串,曾导致线上入口 JS 语法错误、页面空白。
## 构建产物检查
构建脚本会自动输出类似:
```text
H5_ENTRY=assets/index-xxxx.js
H5_ENTRY_SIZE=246232
H5_JS_SYNTAX_CHECKED=...
H5_TENCENT_PLACEHOLDER_REPLACED_FILES=2
H5_BAD_STRING_FILE_COUNT=0
H5_MANIFEST_SOURCE_HOST=172.20.14.21
H5_GUIDE_CONTENTS_ROWS=720
H5_EXHIBITS_ROWS=4700
H5_GUIDE_STOPS_ROWS=398
```
如果需要手工复查入口 JS
```powershell
$dist = Join-Path (Get-Location) 'dist\build\h5'
$html = Get-Content -LiteralPath (Join-Path $dist 'index.html') -Raw
$entry = [regex]::Match($html, 'src="/?([^"]*assets/index-[^"]+\.js)"').Groups[1].Value
node --check (Join-Path $dist $entry)
```
预期无错误输出。
## 打包与上传
```powershell
$ts = Get-Date -Format 'yyyyMMddHHmmss'
New-Item -ItemType Directory -Force -Path '.tmp' | Out-Null
$pkg = ".tmp\frontend-miniapp-h5-$ts.tar.gz"
if (Test-Path $pkg) { Remove-Item -LiteralPath $pkg -Force }
tar -czf $pkg -C 'dist\build\h5' .
scp -o StrictHostKeyChecking=no $pkg root@172.20.14.21:/tmp/frontend-miniapp-h5.tar.gz
```
## 主节点部署
部署时必须保留:
- `/data/nginx/html/mobile/gpL0svkeao.txt`
- `/data/nginx/html/mobile/.well-known`
执行:
```powershell
$script = @'
set -euo pipefail
pkg=/tmp/frontend-miniapp-h5.tar.gz
target=/data/nginx/html/mobile
backup_dir=/data/nginx/html/_backups
ts=$(date +%Y%m%d%H%M%S)
backup="$backup_dir/mobile-before-deploy-$ts.tar.gz"
tmp="/tmp/frontend-miniapp-h5-$ts"
test -s "$pkg"
mkdir -p "$target" "$backup_dir"
tar -czf "$backup" -C "$target" .
rm -rf "$tmp"
mkdir -p "$tmp"
tar -xzf "$pkg" -C "$tmp"
test -f "$tmp/index.html"
find "$target" -mindepth 1 -maxdepth 1 ! -name 'gpL0svkeao.txt' ! -name '.well-known' -exec rm -rf {} +
cp -a "$tmp"/. "$target"/
chmod -R a+rX "$target"
entry=$(grep -o "assets/index-[^\"]*\.js" "$target/index.html" | head -n 1 || true)
test -n "$entry"
test -f "$target/$entry"
node --check "$target/$entry"
if grep -R -I -E '1\.92\.206\.90|guide\.whaoyue\.com|__REPLACE_WITH_TENCENT_MAP_WEB_KEY__|http://1\.92\.206\.90:9000' "$target" >/tmp/mobile_bad_matches.txt; then
cat /tmp/mobile_bad_matches.txt >&2
exit 1
fi
docker exec sgs-nginx nginx -t
docker exec sgs-nginx nginx -s reload
printf 'DEPLOY_BACKUP=%s\n' "$backup"
printf 'DEPLOY_ENTRY=%s\n' "$entry"
printf 'DEPLOY_TARGET=%s\n' "$target"
printf 'PRESERVED_GP=%s\n' "$(test -f "$target/gpL0svkeao.txt" && echo yes || echo no)"
printf 'PRESERVED_WELL_KNOWN=%s\n' "$(test -d "$target/.well-known" && echo yes || echo no)"
'@
$script | ssh -o StrictHostKeyChecking=no root@172.20.14.21 'bash -s'
```
## 执行同步脚本
```powershell
ssh -o StrictHostKeyChecking=no root@172.20.14.21 'bash /data/scripts/sync-frontend-nodes.sh'
```
预期最后输出:
```text
All frontend worker nodes synced.
```
同步脚本会同步 `/data/nginx/html`,同时也会处理 `/data/apps/sgs-map` 并通过 PM2 重启 `sgs-map`。这是当前脚本行为,部署 mobile 时也会看到相关输出。
## 线上验证
公网验证:
```powershell
$base = 'https://guide.sznhmuseum.org.cn'
curl.exe -L -s -o NUL -w "home=%{http_code} %{content_type} %{size_download}`n" "$base/"
curl.exe -L -s -o NUL -w "guide_contents=%{http_code} %{content_type} %{size_download}`n" "$base/static/guide-data/guide-contents.json"
curl.exe -L -s -o NUL -w "exhibits=%{http_code} %{content_type} %{size_download}`n" "$base/static/guide-data/exhibits.json"
curl.exe -L -s -o NUL -w "manifest=%{http_code} %{content_type} %{size_download}`n" "$base/static/guide-data/manifest.json"
curl.exe -L -s -o NUL -w "brand_icon=%{http_code} %{content_type} %{size_download}`n" "$base/static/guide/museum-brand-icons.webp"
curl.exe -L -s -o NUL -w "shortcut_icon=%{http_code} %{content_type} %{size_download}`n" "$base/static/icons/poi/shortcut-icons.svg"
curl.exe -L -s -o NUL -w "verify_txt=%{http_code} %{content_type} %{size_download}`n" "$base/gpL0svkeao.txt"
```
入口 JS 验证:
```powershell
$tmp = Join-Path (Get-Location) '.tmp\public-mobile-verify'
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
$htmlPath = Join-Path $tmp 'index.html'
$entryPath = Join-Path $tmp 'entry.js'
curl.exe -L -s "$base/" -o $htmlPath
$html = Get-Content -LiteralPath $htmlPath -Raw
$entry = [regex]::Match($html, 'src="/?([^"]*assets/index-[^"]+\.js)"').Groups[1].Value
curl.exe -L -s "$base/$entry" -o $entryPath
node --check $entryPath
@('1.92.206.90','guide.whaoyue.com','__REPLACE_WITH_TENCENT_MAP_WEB_KEY__') |
Where-Object { (Get-Content -LiteralPath $entryPath -Raw).Contains($_) }
```
预期 `node --check` 通过,旧测试地址和占位符检查无输出。
五台节点一致性验证:
```powershell
$nodes = @('172.20.14.21','172.20.14.23','172.20.14.26','172.20.14.27','172.20.14.33')
$entryPath = "/data/nginx/html/mobile/$entry"
foreach ($node in $nodes) {
ssh -o StrictHostKeyChecking=no root@$node "node --check '$entryPath' >/dev/null && sha256sum '$entryPath' && stat -c %s '$entryPath'"
}
```
五台节点的 size 和 SHA256 应一致。
## 回滚
主节点部署脚本会在 `/data/nginx/html/_backups` 下生成备份,例如:
```text
/data/nginx/html/_backups/mobile-before-deploy-YYYYMMDDHHmmss.tar.gz
```
回滚主节点:
```powershell
$backup = '/data/nginx/html/_backups/mobile-before-deploy-YYYYMMDDHHmmss.tar.gz'
$script = @"
set -euo pipefail
target=/data/nginx/html/mobile
backup='$backup'
test -f "`$backup"
find "`$target" -mindepth 1 -maxdepth 1 ! -name 'gpL0svkeao.txt' ! -name '.well-known' -exec rm -rf {} +
tar -xzf "`$backup" -C "`$target"
chmod -R a+rX "`$target"
docker exec sgs-nginx nginx -t
docker exec sgs-nginx nginx -s reload
"@
$script | ssh -o StrictHostKeyChecking=no root@172.20.14.21 'bash -s'
```
回滚后仍需执行同步脚本:
```powershell
ssh -o StrictHostKeyChecking=no root@172.20.14.21 'bash /data/scripts/sync-frontend-nodes.sh'
```
## 已遇到的问题与处理
### 1. terser 压缩阶段内存不足
现象:
```text
[vite:terser] Worker terminated due to reaching memory limit: JS heap out of memory
```
处理:
- 设置 `NODE_OPTIONS=--max-old-space-size=16384`
- 优先使用 `corepack pnpm build:h5`
- 如仍 OOM使用 `corepack pnpm run build:h5:no-minify`,该命令仍会执行安全后处理和 `node --check`
### 2. 测试服务器 IP 混入正式环境
现象:
```text
Mixed Content: requested insecure image 'http://1.92.206.90:9000/...'
```
处理:
- 正式环境使用 `VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST=172.20.14.21`
- 构建后必须扫描 `1.92.206.90``guide.whaoyue.com`
- 静态资源通过同源路径 `/museum-assets/...` 访问。
### 3. 腾讯地图接口 CORS
现象:
```text
Access to XMLHttpRequest at 'https://apis.map.qq.com/...' has been blocked by CORS policy
```
处理:
- 避免前端直接跨域请求不可 CORS 的接口。
- 需要由 Nginx 或后端代理转发第三方地图 API前端访问同源路径。
- 构建产物中的腾讯地图 key 占位符必须替换为正式 key。
### 4. 3D 模型或 JSON 返回 HTML
现象:
```text
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
```
常见原因:
- 静态资源未复制到 `dist/build/h5/static/...`
- Nginx 对静态资源路径错误回退到 `index.html`
- 部署目录缺文件。
处理:
- 构建后必须执行 `scripts/finalize-h5-build.cjs`,它会调用 `copy-h5-nav-assets.cjs`
- 验证 `/static/guide-data/*.json``/static/nav-assets/...``/static/three/...` 返回正确资源。
- Nginx 静态资源路径应优先 `try_files $uri =404`,不要对资源文件回退 SPA。
### 5. 域名验证文件被覆盖
风险:
- 全量替换 `/data/nginx/html/mobile` 时误删 `gpL0svkeao.txt``.well-known`
处理:
- 清理目录时排除 `gpL0svkeao.txt``.well-known`
- 部署后验证 `https://guide.sznhmuseum.org.cn/gpL0svkeao.txt` 返回 200。
### 6. 同步脚本输出 Node engine warning
现象:
```text
npm warn EBADENGINE Unsupported engine
package: 'camera-controls@3.1.2'
required: { node: '>=22.0.0', npm: '>=10.5.1' }
current: { node: 'v20.19.5', npm: '10.8.2' }
```
处理:
- 当前为已知非阻断 warning。
- 只要脚本最终输出 `All frontend worker nodes synced.`,且 PM2 中 `sgs-map``online`,本次 mobile 静态部署可继续验收。
### 7. 同步脚本会重启 sgs-map
现象:
- 部署 mobile 时,脚本仍会同步 `/data/apps/sgs-map` 并重启 PM2 进程 `sgs-map`
处理:
- 当前按脚本现状执行。
- 验收时关注最终 PM2 状态是否 `online`
- 如未来只需同步 mobile可拆分或新增专用同步脚本避免不必要地重启 map 服务。
### 8. 页面返回 200 但浏览器空白
现象:
```text
https://guide.sznhmuseum.org.cn/ HTTP 200
浏览器控制台SyntaxError: missing ) after argument list
```
本次根因:
- 入口 JS 本身语法损坏,不是 Nginx、证书或后端问题。
- 故障版本中,构建后手工用 PowerShell `Get-Content` / `Set-Content` 替换腾讯地图 key错误编码重写了 JS破坏了中文注释或字符串。
- 典型错误位置表现为 uni 运行时代码附近的中文注释被改写,导致 `node --check``missing ) after argument list``Unexpected identifier`
修复:
- 重新生成干净 H5 产物。
- 使用 `scripts/finalize-h5-build.cjs` 按 UTF-8 替换腾讯地图 key。
- 部署前、主节点替换后、线上验证时都执行 `node --check`
预防:
- 不再手工替换构建产物 JS。
- 标准构建必须使用 `corepack pnpm build:h5``corepack pnpm run build:h5:no-minify`
- 部署脚本中保留 `node --check "$target/$entry"`
## 最近正式部署记录
- 2026-07-13多次部署 `frontend-miniapp``172.20.14.21:/data/nginx/html/mobile`,同步到 `172.20.14.23/26/27/33`。确认公网域名 `https://guide.sznhmuseum.org.cn` 正常,静态讲解数据 `guide-contents=720``exhibits=4700``manifest.sourceHost=172.20.14.21`
- 2026-07-13确认 `pnpm build:h5` 在 terser 阶段可能 OOM正式部署可使用 `build:h5:no-minify` 兜底。
- 2026-07-13新增并验证 `static/icons/poi/shortcut-icons.svg` 线上可访问。
- 2026-07-15排查并修复页面 200 但空白问题。根因为 PowerShell 手工替换构建 JS 时破坏 UTF-8已新增 `scripts/finalize-h5-build.cjs` 统一处理资源复制、腾讯地图 key 替换、JS 语法校验和正式环境字符串扫描。