This commit is contained in:
449
docs/PRODUCTION_MOBILE_DEPLOYMENT_RUNBOOK.md
Normal file
449
docs/PRODUCTION_MOBILE_DEPLOYMENT_RUNBOOK.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# 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 语法校验和正式环境字符串扫描。
|
||||
@@ -10,8 +10,9 @@
|
||||
"scripts": {
|
||||
"dev:h5": "uni -p h5 --mode development",
|
||||
"dev:test:h5": "uni -p h5 --mode test",
|
||||
"build:test:h5": "uni build -p h5 --mode test && node scripts/copy-h5-nav-assets.cjs",
|
||||
"build:h5": "uni build -p h5 --mode production && node scripts/copy-h5-nav-assets.cjs",
|
||||
"build:test:h5": "uni build -p h5 --mode test && node scripts/finalize-h5-build.cjs --allow-placeholder",
|
||||
"build:h5": "uni build -p h5 --mode production && node scripts/finalize-h5-build.cjs --require-tencent-map-key",
|
||||
"build:h5:no-minify": "uni build -p h5 --mode production --minify false && node scripts/finalize-h5-build.cjs --require-tencent-map-key",
|
||||
"dev:mp-weixin": "uni -p mp-weixin --mode development",
|
||||
"dev:test:mp-weixin": "uni -p mp-weixin --mode test",
|
||||
"build:test:mp-weixin": "uni build -p mp-weixin --mode test",
|
||||
|
||||
145
scripts/finalize-h5-build.cjs
Normal file
145
scripts/finalize-h5-build.cjs
Normal file
@@ -0,0 +1,145 @@
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { spawnSync } = require('node:child_process')
|
||||
|
||||
require('./copy-h5-nav-assets.cjs')
|
||||
|
||||
const projectRoot = path.resolve(__dirname, '..')
|
||||
const h5Root = path.join(projectRoot, 'dist', 'build', 'h5')
|
||||
const placeholder = '__REPLACE_WITH_TENCENT_MAP_WEB_KEY__'
|
||||
const badPatterns = [
|
||||
'http://1.92.206.90:9000',
|
||||
'1.92.206.90',
|
||||
'guide.whaoyue.com',
|
||||
placeholder
|
||||
]
|
||||
const args = new Set(process.argv.slice(2))
|
||||
const requireTencentMapKey = args.has('--require-tencent-map-key')
|
||||
const allowPlaceholder = args.has('--allow-placeholder')
|
||||
const tencentMapKey = (process.env.VITE_TENCENT_MAP_KEY || '').trim()
|
||||
|
||||
if (!fs.existsSync(h5Root)) {
|
||||
throw new Error(`H5 build output not found: ${h5Root}`)
|
||||
}
|
||||
|
||||
const isTextFile = (filePath) => /\.(?:js|mjs|html|json|css|txt|svg|xml)$/i.test(filePath)
|
||||
|
||||
const walkFiles = (dir, files = []) => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const filePath = path.join(dir, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
walkFiles(filePath, files)
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.isFile()) {
|
||||
files.push(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
const allFiles = walkFiles(h5Root)
|
||||
const textFiles = allFiles.filter(isTextFile)
|
||||
const placeholderFiles = textFiles.filter((file) => fs.readFileSync(file, 'utf8').includes(placeholder))
|
||||
const hasUsableTencentMapKey = tencentMapKey.length > 0 && tencentMapKey !== placeholder
|
||||
|
||||
if (placeholderFiles.length > 0) {
|
||||
if (!hasUsableTencentMapKey) {
|
||||
const message = [
|
||||
'H5 build output still contains Tencent map key placeholder.',
|
||||
'Please set VITE_TENCENT_MAP_KEY before production build.'
|
||||
].join(' ')
|
||||
|
||||
if (requireTencentMapKey && !allowPlaceholder) {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
console.warn(`[finalize-h5-build] ${message}`)
|
||||
} else {
|
||||
// 所有构建产物文本替换必须由 Node 按 UTF-8 完成,避免 PowerShell 默认编码破坏中文注释或字符串。
|
||||
for (const file of placeholderFiles) {
|
||||
const text = fs.readFileSync(file, 'utf8')
|
||||
fs.writeFileSync(file, text.split(placeholder).join(tencentMapKey), 'utf8')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const indexPath = path.join(h5Root, 'index.html')
|
||||
const indexHtml = fs.readFileSync(indexPath, 'utf8')
|
||||
const entryMatch = indexHtml.match(/src="\/?([^"]*assets\/index-[^"]+\.js)"/)
|
||||
|
||||
if (!entryMatch) {
|
||||
throw new Error(`Entry JS not found in ${indexPath}`)
|
||||
}
|
||||
|
||||
const entryFile = path.join(h5Root, entryMatch[1])
|
||||
|
||||
if (!fs.existsSync(entryFile)) {
|
||||
throw new Error(`Entry JS file not found: ${entryFile}`)
|
||||
}
|
||||
|
||||
const jsFiles = allFiles.filter((file) => /\.js$/i.test(file))
|
||||
|
||||
for (const file of jsFiles) {
|
||||
const check = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
|
||||
|
||||
if (check.status !== 0) {
|
||||
process.stdout.write(check.stdout || '')
|
||||
process.stderr.write(check.stderr || '')
|
||||
throw new Error(`JavaScript syntax check failed: ${path.relative(projectRoot, file)}`)
|
||||
}
|
||||
}
|
||||
|
||||
const badMatches = []
|
||||
|
||||
for (const file of textFiles) {
|
||||
const text = fs.readFileSync(file, 'utf8')
|
||||
const matchedPattern = badPatterns.find((pattern) => text.includes(pattern))
|
||||
|
||||
if (matchedPattern) {
|
||||
badMatches.push({
|
||||
file: path.relative(projectRoot, file),
|
||||
pattern: matchedPattern
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (badMatches.length > 0) {
|
||||
if (allowPlaceholder && badMatches.every((item) => item.pattern === placeholder)) {
|
||||
console.warn('[finalize-h5-build] Tencent map key placeholder is allowed for this build.')
|
||||
} else {
|
||||
console.error(JSON.stringify(badMatches.slice(0, 20), null, 2))
|
||||
throw new Error('H5 build output contains forbidden test host, old domain, or placeholder.')
|
||||
}
|
||||
}
|
||||
|
||||
const readJson = (relativePath) => JSON.parse(fs.readFileSync(path.join(h5Root, relativePath), 'utf8'))
|
||||
const manifest = readJson('static/guide-data/manifest.json')
|
||||
const guideContents = readJson('static/guide-data/guide-contents.json')
|
||||
const exhibits = readJson('static/guide-data/exhibits.json')
|
||||
const guideStops = readJson('static/guide-data/guide-stops.json')
|
||||
|
||||
const requiredFiles = [
|
||||
'static/guide/museum-brand-icons.webp',
|
||||
'static/icons/poi/shortcut-icons.svg',
|
||||
'static/icons/marker-entrance.svg',
|
||||
'static/Fonts/HarmonyOS_SansSC_Regular.ttf'
|
||||
]
|
||||
const missingFiles = requiredFiles.filter((file) => !fs.existsSync(path.join(h5Root, file)))
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
throw new Error(`Required H5 static assets are missing: ${missingFiles.join(', ')}`)
|
||||
}
|
||||
|
||||
console.log(`H5_ENTRY=${entryMatch[1]}`)
|
||||
console.log(`H5_ENTRY_SIZE=${fs.statSync(entryFile).size}`)
|
||||
console.log(`H5_JS_SYNTAX_CHECKED=${jsFiles.length}`)
|
||||
console.log(`H5_TENCENT_PLACEHOLDER_REPLACED_FILES=${hasUsableTencentMapKey ? placeholderFiles.length : 0}`)
|
||||
console.log(`H5_BAD_STRING_FILE_COUNT=${badMatches.length}`)
|
||||
console.log(`H5_MANIFEST_SOURCE_HOST=${manifest.source?.sourceHost || ''}`)
|
||||
console.log(`H5_GUIDE_CONTENTS_ROWS=${guideContents.rows?.length || 0}`)
|
||||
console.log(`H5_EXHIBITS_ROWS=${exhibits.rows?.length || 0}`)
|
||||
console.log(`H5_GUIDE_STOPS_ROWS=${guideStops.rows?.length || 0}`)
|
||||
Reference in New Issue
Block a user