feat: add versioning support for plugin assets and improve loading status handling

This commit is contained in:
shiyu
2026-01-08 10:13:09 +08:00
parent 4e16de973c
commit b7685db0e8
2 changed files with 37 additions and 8 deletions
+3 -1
View File
@@ -67,10 +67,12 @@ async def delete_plugin(request: Request, key_or_id: str):
async def get_bundle(request: Request, key_or_id: str): async def get_bundle(request: Request, key_or_id: str):
"""获取插件前端 bundle""" """获取插件前端 bundle"""
path = await PluginService.get_bundle_path(key_or_id) path = await PluginService.get_bundle_path(key_or_id)
v = (request.query_params.get("v") or "").strip()
cache_control = "public, max-age=31536000, immutable" if v else "no-cache"
return FileResponse( return FileResponse(
path, path,
media_type="application/javascript", media_type="application/javascript",
headers={"Cache-Control": "no-store"}, headers={"Cache-Control": cache_control},
) )
+34 -7
View File
@@ -31,6 +31,19 @@ function renderStatus(text: string, isError: boolean = false) {
root.appendChild(el); root.appendChild(el);
} }
function scheduleStatus(text: string, delayMs: number) {
let canceled = false;
const t = window.setTimeout(() => {
if (canceled) return;
renderStatus(text);
}, delayMs);
return () => {
canceled = true;
window.clearTimeout(t);
};
}
function getQuery() { function getQuery() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const pluginKey = (params.get('pluginKey') || '').trim(); const pluginKey = (params.get('pluginKey') || '').trim();
@@ -89,14 +102,25 @@ function getPluginStylePaths(plugin: PluginItem): string[] {
return styles.filter((s) => typeof s === 'string' && s.trim().length > 0); return styles.filter((s) => typeof s === 'string' && s.trim().length > 0);
} }
async function loadPluginStyles(pluginKey: string, plugin: PluginItem) { function withVersion(url: string, version?: string | null): string {
const v = typeof version === 'string' ? version.trim() : '';
if (!v) return url;
const u = new URL(url, window.location.origin);
u.searchParams.set('v', v);
return u.pathname + u.search;
}
async function loadPluginStyles(pluginKey: string, plugin: PluginItem, version?: string | null) {
const stylePaths = getPluginStylePaths(plugin); const stylePaths = getPluginStylePaths(plugin);
if (stylePaths.length === 0) return; if (stylePaths.length === 0) return;
const tasks = stylePaths.map( const tasks = stylePaths.map(
(p) => (p) =>
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
const href = `/api/plugins/${pluginKey}/assets/${p.replace(/^\/+/, '')}`; const href = withVersion(
`/api/plugins/${pluginKey}/assets/${p.replace(/^\/+/, '')}`,
version
);
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
link.href = href; link.href = href;
@@ -109,8 +133,8 @@ async function loadPluginStyles(pluginKey: string, plugin: PluginItem) {
await Promise.all(tasks); await Promise.all(tasks);
} }
async function loadPluginBundle(pluginKey: string): Promise<RegisteredPlugin> { async function loadPluginBundle(pluginKey: string, version?: string | null): Promise<RegisteredPlugin> {
const url = `/api/plugins/${pluginKey}/bundle.js`; const url = withVersion(`/api/plugins/${pluginKey}/bundle.js`, version);
return new Promise<RegisteredPlugin>((resolve, reject) => { return new Promise<RegisteredPlugin>((resolve, reject) => {
let done = false; let done = false;
@@ -178,32 +202,35 @@ async function main() {
return; return;
} }
renderStatus('Loading plugin...'); const cancelLoading = scheduleStatus('Loading plugin...', 200);
let plugin: PluginItem; let plugin: PluginItem;
try { try {
plugin = await pluginsApi.get(pluginKey); plugin = await pluginsApi.get(pluginKey);
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
cancelLoading();
renderStatus(`Failed to load plugin info: ${msg}`, true); renderStatus(`Failed to load plugin info: ${msg}`, true);
return; return;
} }
try { try {
await loadPluginStyles(pluginKey, plugin); await loadPluginStyles(pluginKey, plugin, plugin.version);
} catch { } catch {
// ignore // ignore
} }
let registered: RegisteredPlugin; let registered: RegisteredPlugin;
try { try {
registered = await loadPluginBundle(pluginKey); registered = await loadPluginBundle(pluginKey, plugin.version);
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
cancelLoading();
renderStatus(`Failed to load plugin bundle: ${msg}`, true); renderStatus(`Failed to load plugin bundle: ${msg}`, true);
return; return;
} }
cancelLoading();
const host = createHostApi(pluginKey); const host = createHostApi(pluginKey);
let cleanup: (() => void) | null = null; let cleanup: (() => void) | null = null;