perf(web): page-data cache + progressive rendering (item 22, frontend)
Page switches no longer refetch the base datasets every time: servers/customers/nodes/protocols are reused across pages for 30s, and every successful mutation (all write paths funnel through api()) invalidates the cache. Rendering is progressive — the page paints immediately with what is already in state and re-renders as each dataset lands, instead of waiting for a Promise.all of 2-5 requests. Overview/system/jobs still fetch fresh on every load.
This commit is contained in:
+48
-15
@@ -128,6 +128,9 @@ async function api(path, options = {}) {
|
|||||||
if (response.status === 401 && path !== '/api/auth/login') showLogin();
|
if (response.status === 401 && path !== '/api/auth/login') showLogin();
|
||||||
throw new Error((data && data.message) || `请求失败 (${response.status})`);
|
throw new Error((data && data.message) || `请求失败 (${response.status})`);
|
||||||
}
|
}
|
||||||
|
// Any successful mutation invalidates the page-data cache (item 22):
|
||||||
|
// every write path in this file funnels through api().
|
||||||
|
if (!['GET', 'HEAD'].includes(options.method || 'GET')) invalidateDataCache();
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,29 +243,59 @@ function pagerHTML(key, total) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 数据加载与路由 ---------- */
|
/* ---------- 数据加载与路由 ---------- */
|
||||||
|
/* 基础数据集页间缓存(第 22 条):servers/customers/nodes/protocols 在
|
||||||
|
30 秒内切页直接复用、不重复请求;任何写操作经 api() 统一失效。
|
||||||
|
渲染不再等全部请求落地:每份数据到达即更新 state 并重渲染当前页。 */
|
||||||
|
const DATA_CACHE_TTL_MS = 30000;
|
||||||
|
const dataCacheAt = { servers: 0, customers: 0, nodes: 0, protocols: 0 };
|
||||||
|
function invalidateDataCache() { for (const key of Object.keys(dataCacheAt)) dataCacheAt[key] = 0; }
|
||||||
|
function cacheFresh(key) { return dataCacheAt[key] && Date.now() - dataCacheAt[key] < DATA_CACHE_TTL_MS; }
|
||||||
|
|
||||||
async function load(page = state.page) {
|
async function load(page = state.page) {
|
||||||
|
const isCurrent = () => state.page === page && !modalOpen();
|
||||||
|
const apply = (fn) => { fn(); if (isCurrent()) render(); };
|
||||||
|
const jobs = [];
|
||||||
|
const errors = [];
|
||||||
|
// Cached base dataset: fetch only when stale. A partial failure keeps
|
||||||
|
// whatever data is already in state (the page renders progressively);
|
||||||
|
// only a total failure surfaces a toast, as the old all-or-nothing
|
||||||
|
// load did.
|
||||||
|
const base = (key, path, fn) => {
|
||||||
|
if (cacheFresh(key)) return;
|
||||||
|
jobs.push(api(path).then((data) => { dataCacheAt[key] = Date.now(); apply(() => fn(data)); }).catch((error) => errors.push(error)));
|
||||||
|
};
|
||||||
|
const fresh = (path, fn) => { jobs.push(api(path).then((data) => apply(() => fn(data))).catch((error) => errors.push(error))); };
|
||||||
|
const setServers = (d) => { state.servers = d.servers; };
|
||||||
|
const setCustomers = (d) => { state.customers = d.customers; state.customerSummary = d.summary || null; };
|
||||||
|
const setNodes = (d) => { state.nodes = d.nodes; state.nodeClients = d.nodeClients; };
|
||||||
|
const setOverview = (d) => { state.overview = d; state.version = d.version || state.version; $('#version-label').textContent = `v${state.version}`; };
|
||||||
try {
|
try {
|
||||||
if (page === 'overview') {
|
if (page === 'overview') {
|
||||||
const [overview, system, servers, customers, routes] = await Promise.all([api('/api/overview'), api('/api/system'), api('/api/servers'), api('/api/customers'), api('/api/nodes')]);
|
fresh('/api/overview', setOverview);
|
||||||
state.overview = overview; state.system = system; state.version = overview.version || state.version;
|
fresh('/api/system', (d) => { state.system = d; });
|
||||||
state.servers = servers.servers; state.customers = customers.customers; state.customerSummary = customers.summary || null; state.nodes = routes.nodes; state.nodeClients = routes.nodeClients;
|
base('servers', '/api/servers', setServers);
|
||||||
$('#version-label').textContent = `v${state.version}`;
|
base('customers', '/api/customers', setCustomers);
|
||||||
|
base('nodes', '/api/nodes', setNodes);
|
||||||
} else if (page === 'servers') {
|
} else if (page === 'servers') {
|
||||||
const [servers, routes] = await Promise.all([api('/api/servers'), api('/api/nodes')]);
|
base('servers', '/api/servers', setServers);
|
||||||
state.servers = servers.servers; state.nodes = routes.nodes; state.nodeClients = routes.nodeClients;
|
base('nodes', '/api/nodes', setNodes);
|
||||||
} else if (page === 'customers') {
|
} else if (page === 'customers') {
|
||||||
const [customers, routes] = await Promise.all([api('/api/customers'), api('/api/nodes')]);
|
base('customers', '/api/customers', setCustomers);
|
||||||
state.customers = customers.customers; state.customerSummary = customers.summary || null; state.nodes = routes.nodes; state.nodeClients = routes.nodeClients;
|
base('nodes', '/api/nodes', setNodes);
|
||||||
} else if (page === 'chains') {
|
} else if (page === 'chains') {
|
||||||
const [servers, customers, routes, protocols] = await Promise.all([api('/api/servers'), api('/api/customers'), api('/api/nodes'), api('/api/protocols')]);
|
base('servers', '/api/servers', setServers);
|
||||||
state.servers = servers.servers; state.customers = customers.customers; state.customerSummary = customers.summary || null; state.nodes = routes.nodes; state.nodeClients = routes.nodeClients;
|
base('customers', '/api/customers', setCustomers);
|
||||||
state.protocols = protocols.profiles; state.realityPresets = protocols.realityPresets || [];
|
base('nodes', '/api/nodes', setNodes);
|
||||||
|
base('protocols', '/api/protocols', (d) => { state.protocols = d.profiles; state.realityPresets = d.realityPresets || []; });
|
||||||
} else if (page === 'operations') {
|
} else if (page === 'operations') {
|
||||||
const [jobs, overview] = await Promise.all([api('/api/jobs'), api('/api/overview')]);
|
fresh('/api/jobs', (d) => { state.jobs = d.jobs; });
|
||||||
state.jobs = jobs.jobs; state.overview = overview; state.version = overview.version || state.version;
|
fresh('/api/overview', setOverview);
|
||||||
$('#version-label').textContent = `v${state.version}`;
|
|
||||||
}
|
}
|
||||||
render();
|
// Render immediately with whatever is already in state (cached or
|
||||||
|
// previously loaded), then re-render as the rest lands.
|
||||||
|
if (isCurrent()) render();
|
||||||
|
await Promise.allSettled(jobs);
|
||||||
|
if (errors.length && errors.length === jobs.length) throw errors[0];
|
||||||
} catch (error) { toast(error.message, true); }
|
} catch (error) { toast(error.message, true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user