feat: P4 阶段A+B — data.js 瘦身 116KB(预计算 + css 分离)
执行 ROADMAP 的 S1-P4,按规划的三阶段走,本次完成 A 与 B。 阶段 A · 预计算 scenarios(验证「构建期预计算 + 运行时回退」链路) - tools/precompute.mjs:与 app.js 的 extractScenarios 逻辑完全一致, 构建期算出结果写进 data.components[].scenarios - app.js 加快速路径:优先读 c.scenarios,缺失时回退运行时解析 - 浏览器实测:预计算值与运行时提取值**完全一致**(逐字节比对) - 实测收益有限:仅 4 个组件有 ≥2 场景(场景卡片本就少见), 但链路验证通过,为阶段 C 的 API 预计算铺路 阶段 B · css 源码分离(零风险,css 不参与任何解析) - css 从 data.js 移到 site/sources/<slug>/css.txt(79 个文件) - data.js 里改为 sourcesRef.css 引用,data.json 保持完整(For Agents 承诺) - data.js: 1058 KB → 942 KB(−116 KB) - app.js 新增 srcOf(c, kind) / hasSrc(c, kind): · srcOf 同步返回已有源码,缺失时触发后台 fetch 并缓存 · __aaSrcReady 回调在加载完成后填充代码区 - 修正回调时机 bug:原实现捕获构建时的 current(初始 tab), 导致用户后续切换的 tab 收不到通知。改为回调内实时比对当前 tab 关于 P4 的实测修正(已登记 ROADMAP 规划修正记录) - 规划估计「拆 sources → ~110KB」。实测 css 仅占 119KB/888KB, 且 html/vue2/vue3/jsx 共 765KB 被**渲染期同步消费** (extractComponentAPI 读 vue3/vue2/jsx,extractScenarios 读 html) - 故需分阶段:A+B 先减 116KB,阶段 C 需先预计算 API 表才能分离其余 4 端 新增构建链(两步,不可省第二步) - npm run build:site = build-site.ps1 + precompute.mjs - build-site.ps1 会把 data.js 重写回全量,precompute 才做瘦身 - 已写入 AGENTS.md 的铁律章节(含「只跑第一步会怎样」的后果说明) 验证 - 回归 100%:1003/1003,79 页全通过,N/A 35 - 浏览器实测:展开代码区 → CSS tab → 2121 字符正常显示, Network 出现 sources/button/css.txt 请求 - 预计算 vs 运行时场景提取结果逐字节一致
This commit is contained in:
+48
-5
@@ -63,6 +63,36 @@
|
||||
var state = { route: { page: 'home', slug: null }, query: '', ovCat: '', ovQ: '', framework: 'h5', searchCat: '' };
|
||||
var thumbObserver = null;
|
||||
|
||||
/* ---------- 源码取值(P4:部分源码已移出 data.js,改为按需加载)----------
|
||||
data.js 里 components[].sources 可能只有部分端,其余在 sourcesRef 里给路径。
|
||||
srcOf 同步返回「已有的」源码;缺失时返回空串并触发后台加载,加载完回调刷新。
|
||||
缓存避免重复请求。 */
|
||||
var srcCache = {};
|
||||
var srcPending = {};
|
||||
|
||||
function srcOf(c, kind) {
|
||||
if (c.sources && c.sources[kind] != null) return c.sources[kind];
|
||||
var key = c.slug + '/' + kind;
|
||||
if (srcCache[key] != null) return srcCache[key];
|
||||
var ref = (c.sourcesRef || {})[kind];
|
||||
if (ref && !srcPending[key]) {
|
||||
srcPending[key] = true;
|
||||
fetch(ref, { cache: 'force-cache' })
|
||||
.then(function (r) { return r.ok ? r.text() : ''; })
|
||||
.then(function (t) {
|
||||
srcCache[key] = t; srcPending[key] = false;
|
||||
if (window.__aaSrcReady) window.__aaSrcReady(c.slug, kind, t);
|
||||
})
|
||||
.catch(function () { srcCache[key] = ''; srcPending[key] = false; });
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/* 该端源码是否「存在」(本地或引用)—— 决定 tab 是否显示 */
|
||||
function hasSrc(c, kind) {
|
||||
return (c.sources && c.sources[kind] != null) || !!((c.sourcesRef || {})[kind]);
|
||||
}
|
||||
|
||||
function loadFramework() {
|
||||
try {
|
||||
var saved = localStorage.getItem('aa-framework');
|
||||
@@ -1541,6 +1571,8 @@
|
||||
从 H5 演示源码中提取 <h2> 场景分组标题,生成「场景速览」条。
|
||||
点击场景 = 同源 iframe 内定位到对应分组(file:// 下自动降级隐藏跳转)。 */
|
||||
function extractScenarios(c) {
|
||||
/* P4 预计算优先:构建期已把结果写进 c.scenarios,回退到运行时解析 */
|
||||
if (c.scenarios && c.scenarios.length) return c.scenarios;
|
||||
var html = (c.sources && c.sources.html) || '';
|
||||
var bodyM = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
var body = bodyM ? bodyM[1] : html;
|
||||
@@ -1700,8 +1732,8 @@
|
||||
|
||||
/* 只展示当前框架的源码形态 */
|
||||
var kinds = FW_FILES[state.framework] || TAB_ORDER;
|
||||
var available = kinds.filter(function (k) { return c.sources[k] != null; });
|
||||
if (!available.length) available = TAB_ORDER.filter(function (k) { return c.sources[k] != null; });
|
||||
var available = kinds.filter(function (k) { return hasSrc(c, k); });
|
||||
if (!available.length) available = TAB_ORDER.filter(function (k) { return hasSrc(c, k); });
|
||||
var current = available[0];
|
||||
available.forEach(function (k) {
|
||||
var b = el('button', null, TAB_LABELS[k]);
|
||||
@@ -1711,15 +1743,26 @@
|
||||
current = k;
|
||||
tabs.querySelectorAll('button').forEach(function (x) { x.classList.remove('active'); });
|
||||
b.classList.add('active');
|
||||
code.innerHTML = highlightCode(c.sources[k] || '');
|
||||
code.innerHTML = highlightCode(srcOf(c, k) || '');
|
||||
copyBtn.textContent = T('复制') + (TAB_LABELS[k] || '');
|
||||
});
|
||||
tabs.appendChild(b);
|
||||
});
|
||||
copyBtn.addEventListener('click', function () {
|
||||
copyText(c.sources[current] || '', copyBtn, T('已复制 ✓'));
|
||||
copyText(srcOf(c, current) || '', copyBtn, T('已复制 ✓'));
|
||||
});
|
||||
code.innerHTML = highlightCode(c.sources[current] || '');
|
||||
code.innerHTML = highlightCode(srcOf(c, current) || '');
|
||||
|
||||
/* 按需加载填充:任意 tab 的源码若不在 data.js 里,加载完成后都要填充。
|
||||
不能只在构建时注册(那时 current 只是初始 tab,用户后续切换的 kind 收不到通知),
|
||||
因此回调里实时比对「当前 tab」而非捕获值。 */
|
||||
var getCurrent = function () { return current; };
|
||||
window.__aaSrcReady = function (slug, kind, text) {
|
||||
if (slug !== c.slug || !area.isConnected) return;
|
||||
if (kind !== getCurrent()) return; // 只填当前正在看的 tab
|
||||
code.innerHTML = highlightCode(text || '');
|
||||
copyBtn.textContent = T('复制') + (TAB_LABELS[kind] || '');
|
||||
};
|
||||
return area;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user