126 lines
4.2 KiB
JavaScript
126 lines
4.2 KiB
JavaScript
// 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();
|