Files
TankIC/3DTank/tools/prefetch_assets.js

126 lines
4.2 KiB
JavaScript
Raw Permalink 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.
// prefetch_assets.js - 预拉取车库/大厅所需的静态资源到本地镜像目录
// 用法: node prefetch_assets.js [--concurrency 8] [--only images]
//
// 背景:
// 本地镜像只镜像了 JS,`/play/static/images/*` 全靠 serve.js 现场回源 3dtank.com 并落盘
// (见 serve.js 的「自愈」分支)。从战局返回车库时车库界面要一次性加载大量贴图/图标,
// 首次全部走回源 → 明显卡顿。
// 本脚本把 webpack 资源表(unpacked/resources.json)里的所有静态资源**预先下载**到
// `3DTank/web/play/static/`,之后 serve.js 直接本地直出,返回车库秒开。
//
// 产物位置与 serve.js 的 WEB_DIR 一致:
// URL /play/static/images/xxx.webp → 3DTank/web/play/static/images/xxx.webp
const fs = require('fs');
const path = require('path');
const PROJECT = path.resolve(__dirname, '..');
const ROOT = path.resolve(PROJECT, '..');
const WEB_DIR = path.join(PROJECT, 'web');
const RES_JSON = path.join(ROOT, '3dtank.com', 'play', 'static', 'js', 'unpacked', 'resources.json');
const BASE_URL = 'https://3dtank.com/play/';
const Argv = process.argv.slice(2);
const Arg = (Name, Def) => {
const I = Argv.indexOf('--' + Name);
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
};
const CONCURRENCY = Math.max(1, Number(Arg('concurrency', '8')) || 8);
const HEADERS = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
origin: 'https://3dtank.com',
referer: 'https://3dtank.com/play/',
};
function ReadResourceMap() {
if (!fs.existsSync(RES_JSON)) {
console.error('[prefetch] 找不到资源表: ' + RES_JSON);
process.exit(1);
}
const Json = JSON.parse(fs.readFileSync(RES_JSON, 'utf8'));
const Map = Json.resourceMap || {};
// 去重:多个模块可能引用同一个文件
return Array.from(new Set(Object.values(Map).filter((V) => typeof V === 'string' && V)));
}
function TargetFile(Rel) {
// 安全:禁止跳出 WEB_DIR
const Clean = Rel.replace(/^\/+/, '');
const Out = path.resolve(WEB_DIR, 'play', Clean);
if (!Out.startsWith(path.resolve(WEB_DIR))) return null;
return Out;
}
async function DownloadOne(Rel, Stats) {
const Out = TargetFile(Rel);
if (!Out) {
Stats.skip++;
return;
}
if (fs.existsSync(Out)) {
Stats.cached++;
return;
}
const Url = BASE_URL + Rel.replace(/^\/+/, '');
for (let Try = 0; Try < 3; Try++) {
try {
const Res = await fetch(Url, { headers: HEADERS, redirect: 'follow' });
if (!Res.ok) {
if (Res.status === 404) {
Stats.missing++;
console.warn('[404] ' + Rel);
return;
}
throw new Error('HTTP ' + Res.status);
}
const Buf = Buffer.from(await Res.arrayBuffer());
fs.mkdirSync(path.dirname(Out), { recursive: true });
fs.writeFileSync(Out, Buf);
Stats.downloaded++;
Stats.bytes += Buf.length;
if (Stats.downloaded % 50 === 0) {
console.log(
'[prefetch] 已下载 ' + Stats.downloaded + ' / ' + Stats.total + '(' + (Stats.bytes / 1048576).toFixed(1) + ' MB)',
);
}
return;
} catch (Err) {
if (Try === 2) {
Stats.failed++;
console.warn('[fail] ' + Rel + ' : ' + Err.message);
} else {
await new Promise((R) => setTimeout(R, 400 * (Try + 1)));
}
}
}
}
async function Main() {
const List = ReadResourceMap();
const Stats = { total: List.length, downloaded: 0, cached: 0, missing: 0, failed: 0, skip: 0, bytes: 0 };
console.log('[prefetch] 资源表条目: ' + List.length + ',并发: ' + CONCURRENCY);
console.log('[prefetch] 输出目录: ' + path.join(WEB_DIR, 'play'));
let Index = 0;
async function Worker() {
while (Index < List.length) {
const Rel = List[Index++];
await DownloadOne(Rel, Stats);
}
}
await Promise.all(Array.from({ length: CONCURRENCY }, Worker));
console.log(
'[prefetch] 完成:新下载 ' + Stats.downloaded +
',已存在 ' + Stats.cached +
',404 ' + Stats.missing +
',失败 ' + Stats.failed +
',合计 ' + (Stats.bytes / 1048576).toFixed(1) + ' MB',
);
}
Main();