旧 main.b7114b43.js 已被新构建替换(10,011,039 字节 / 1335 模块),本地镜像整体重切到新版并清掉旧产物;hornet 工具链按 tools/map_symbols.js 生成的新旧符号映射改名后重新打包(3dtank_bundle.js 与 unpacked/hornet_bundle.js 同步)。tools/symbol_map.json(8.9MB 映射产物)与切割解包目录不进库,可用 map_symbols.js / split.js 重新生成。
231 lines
8.0 KiB
JavaScript
231 lines
8.0 KiB
JavaScript
// test_loader.js - 模拟浏览器加载流程,验证切割版 bundle 能正确加载所有模块
|
||
// 新版: 资源模块从 resources.json 加载,代码模块从独立文件加载
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const http = require('http');
|
||
|
||
const ROOT = path.resolve(__dirname, '..', '3dtank.com');
|
||
const JS_DIR = path.join(ROOT, 'play', 'static', 'js');
|
||
// 自动定位切割版主文件 main.<hash>.js
|
||
const MAIN_PATH = path.join(JS_DIR, fs.readdirSync(JS_DIR).filter((f) => /^main\.[0-9a-f]+\.js$/.test(f)).sort().pop() || '');
|
||
const UNPACKED_DIR = path.join(JS_DIR, 'unpacked');
|
||
const RESOURCES_JSON_PATH = path.join(UNPACKED_DIR, 'resources.json');
|
||
|
||
// 简单的 HTTP 服务器,提供静态文件
|
||
const server = http.createServer((req, res) => {
|
||
let filePath = path.join(ROOT, req.url.split('?')[0]);
|
||
if (req.url === '/' || req.url === '') filePath = path.join(ROOT, 'index.html');
|
||
|
||
if (!fs.existsSync(filePath)) {
|
||
res.writeHead(404);
|
||
res.end('Not Found');
|
||
return;
|
||
}
|
||
const ext = path.extname(filePath).toLowerCase();
|
||
const ct = {
|
||
'.js': 'application/javascript',
|
||
'.html': 'text/html',
|
||
'.json': 'application/json',
|
||
}[ext] || 'application/octet-stream';
|
||
res.writeHead(200, { 'Content-Type': ct });
|
||
fs.createReadStream(filePath).pipe(res);
|
||
});
|
||
|
||
// 模拟 fetch
|
||
function makeFetch() {
|
||
return (url) =>
|
||
new Promise((resolve, reject) => {
|
||
http
|
||
.get(url, (res) => {
|
||
let body = '';
|
||
res.on('data', (d) => (body += d));
|
||
res.on('end', () => {
|
||
resolve({
|
||
ok: res.statusCode === 200,
|
||
status: res.statusCode,
|
||
text: () => Promise.resolve(body),
|
||
json: () => Promise.resolve(JSON.parse(body)),
|
||
});
|
||
});
|
||
})
|
||
.on('error', reject);
|
||
});
|
||
}
|
||
|
||
(async () => {
|
||
// 1. 检查文件结构
|
||
console.log('[1/5] 检查文件结构...');
|
||
const codeFiles = fs.readdirSync(UNPACKED_DIR).filter((f) => /^module_\d+\.js$/.test(f));
|
||
console.log(` unpacked/ 中代码模块文件: ${codeFiles.length}`);
|
||
if (!fs.existsSync(RESOURCES_JSON_PATH)) {
|
||
console.error(' ✗ resources.json 不存在');
|
||
process.exit(1);
|
||
}
|
||
const resourcesJson = JSON.parse(fs.readFileSync(RESOURCES_JSON_PATH, 'utf8'));
|
||
const resourceMap = resourcesJson.resourceMap;
|
||
const resourceCount = Object.keys(resourceMap).length;
|
||
console.log(` resources.json 资源模块: ${resourceCount}`);
|
||
console.log(` 合计: ${codeFiles.length + resourceCount}`);
|
||
|
||
// 2. 启动 HTTP 服务器
|
||
console.log('[2/5] 启动测试 HTTP 服务器...');
|
||
await new Promise((resolve) => server.listen(18888, resolve));
|
||
console.log(' 服务器监听 http://localhost:18888/');
|
||
|
||
const baseUrl = 'http://localhost:18888/play/static/js/';
|
||
const fetch = makeFetch();
|
||
|
||
// 创建模拟的 window 对象
|
||
const sandbox = {
|
||
console: console,
|
||
fetch,
|
||
globalThis: null,
|
||
};
|
||
sandbox.window = sandbox;
|
||
sandbox.globalThis = sandbox;
|
||
sandbox.window.__modules__ = {};
|
||
|
||
// 3. 阶段 1: 加载 resources.json,注册资源模块
|
||
console.log('[3/5] 阶段 1: 从 resources.json 注册资源模块...');
|
||
for (const __id__ in resourceMap) {
|
||
if (!Object.prototype.hasOwnProperty.call(resourceMap, __id__)) continue;
|
||
const __path__ = resourceMap[__id__];
|
||
const __id_num__ = Number(__id__);
|
||
sandbox.window.__modules__[__id_num__] = (function (__p__) {
|
||
return (t, n, i) => {
|
||
'use strict';
|
||
t.exports = i.p + __p__;
|
||
};
|
||
})(__path__);
|
||
}
|
||
console.log(` 已注册资源模块: ${Object.keys(sandbox.window.__modules__).length}`);
|
||
|
||
// 4. 阶段 2: 加载代码模块(fetch + eval)
|
||
console.log('[4/5] 阶段 2: 从独立文件加载代码模块 (fetch + eval)...');
|
||
const mainCode = fs.readFileSync(MAIN_PATH, 'utf8');
|
||
|
||
// 提取 __code_ids__ 数组
|
||
const m = mainCode.match(/const __code_ids__ = \[([^\]]+)\]/);
|
||
if (!m) {
|
||
console.error(' ✗ 主文件中找不到 __code_ids__');
|
||
process.exit(1);
|
||
}
|
||
const codeIds = m[1].split(',').map((x) => parseInt(x.trim(), 10));
|
||
console.log(` 待加载代码模块: ${codeIds.length}`);
|
||
|
||
let loadedCount = 0;
|
||
let failedCount = 0;
|
||
const failedModules = [];
|
||
|
||
const BATCH_SIZE = 50;
|
||
for (let i = 0; i < codeIds.length; i += BATCH_SIZE) {
|
||
const batch = codeIds.slice(i, i + BATCH_SIZE);
|
||
await Promise.all(
|
||
batch.map(async (id) => {
|
||
const paddedId = String(id).padStart(5, '0');
|
||
const url = baseUrl + 'unpacked/module_' + paddedId + '.js';
|
||
try {
|
||
const resp = await fetch(url);
|
||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||
const code = await resp.text();
|
||
// 在 sandbox 中 eval 执行
|
||
const fn = new Function('window', 'console', 'globalThis', code);
|
||
fn(sandbox.window, sandbox.console, sandbox);
|
||
loadedCount++;
|
||
} catch (e) {
|
||
failedCount++;
|
||
failedModules.push({ id, error: e.message });
|
||
}
|
||
})
|
||
);
|
||
if ((i / BATCH_SIZE) % 2 === 0) {
|
||
process.stdout.write(` 进度: ${loadedCount}/${codeIds.length} (失败 ${failedCount})\r`);
|
||
}
|
||
}
|
||
console.log(` 完成: ${loadedCount}/${codeIds.length} 加载成功, ${failedCount} 失败 `);
|
||
|
||
if (failedCount > 0) {
|
||
console.log(' 失败模块 (前 10):');
|
||
failedModules.slice(0, 10).forEach((f) => console.log(` ID ${f.id}: ${f.error}`));
|
||
}
|
||
|
||
// 5. 验证 window.__modules__ 注册情况
|
||
console.log('[5/5] 验证 window.__modules__ 注册...');
|
||
const modules = sandbox.window.__modules__;
|
||
if (!modules) {
|
||
console.error(' ✗ window.__modules__ 未定义');
|
||
server.close();
|
||
process.exit(1);
|
||
}
|
||
|
||
const registeredIds = Object.keys(modules).map(Number);
|
||
const totalExpected = resourceCount + codeIds.length;
|
||
console.log(` 已注册模块总数: ${registeredIds.length} (预期 ${totalExpected})`);
|
||
|
||
// 关键入口模块验证
|
||
const keyIds = [17, 76074, 13431, 26482, 78231, 82347, 87072];
|
||
keyIds.forEach((id) => {
|
||
const exists = typeof modules[id] === 'function';
|
||
const isResource = Object.prototype.hasOwnProperty.call(resourceMap, id);
|
||
const tag = isResource ? '资源' : '代码';
|
||
console.log(` 模块 ${id} [${tag}]: ${exists ? '✓' : '✗'}`);
|
||
});
|
||
|
||
// 检查资源模块是否正确返回路径
|
||
let resourceOkCount = 0;
|
||
let resourceFailCount = 0;
|
||
for (const id in resourceMap) {
|
||
const idNum = Number(id);
|
||
const expectedPath = resourceMap[id];
|
||
const mod = modules[idNum];
|
||
if (typeof mod !== 'function') {
|
||
resourceFailCount++;
|
||
continue;
|
||
}
|
||
// 模拟调用: i.p = "/play/"
|
||
const fakeModule = { exports: {} };
|
||
const fakeRequire = { p: '/play/' };
|
||
try {
|
||
mod(fakeModule, {}, fakeRequire);
|
||
const actualPath = fakeModule.exports;
|
||
if (actualPath === '/play/' + expectedPath) {
|
||
resourceOkCount++;
|
||
} else {
|
||
resourceFailCount++;
|
||
if (resourceFailCount <= 3) {
|
||
console.log(` 路径不匹配 ID ${id}: 期望 "/play/${expectedPath}", 实际 "${actualPath}"`);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
resourceFailCount++;
|
||
}
|
||
}
|
||
console.log(` 资源模块路径验证: ${resourceOkCount}/${resourceCount} 正确`);
|
||
|
||
// 检查代码模块是否都注册为函数
|
||
const codeMissing = codeIds.filter((id) => typeof modules[id] !== 'function');
|
||
console.log(` 代码模块全部注册: ${codeMissing.length === 0 ? '✓' : '✗ 缺失 ' + codeMissing.length + ' 个'}`);
|
||
if (codeMissing.length > 0) {
|
||
console.log(` 缺失 ID (前 10): ${codeMissing.slice(0, 10).join(', ')}`);
|
||
}
|
||
|
||
server.close();
|
||
console.log('\n=== 测试完成 ===');
|
||
const allOk =
|
||
failedCount === 0 &&
|
||
registeredIds.length === totalExpected &&
|
||
resourceOkCount === resourceCount &&
|
||
codeMissing.length === 0;
|
||
|
||
if (allOk) {
|
||
console.log('✓ 所有模块加载并注册成功 (资源从 JSON + 代码从文件)');
|
||
} else {
|
||
console.log('✗ 存在加载或注册失败');
|
||
process.exit(1);
|
||
}
|
||
})().catch((err) => {
|
||
console.error('Fatal:', err);
|
||
server.close();
|
||
process.exit(1);
|
||
});
|