feat: SPA router for bricks - URL state management

- Intercepts buildUrlwidgetHandler to track navigation to sage_main_content
- Updates browser URL via History API (pushState) on page changes
- Restores page state on browser refresh via ?page= URL parameter
- Supports browser back/forward buttons via popstate event
- Supports deep linking (direct URL access to specific pages)
- URL format: /?page=/module/index.ui
This commit is contained in:
yumoqing 2026-05-27 14:08:39 +08:00
parent 7987c24e26
commit 39fe93438c

223
wwwroot/spa_router.js Normal file
View File

@ -0,0 +1,223 @@
/**
* Bricks SPA Router
*
* 为bricks单页应用提供路由支持
* 1. urlwidget加载内容到sage_main_content时更新浏览器URL
* 2. 浏览器刷新时根据URL恢复当前页面
* 3. 支持浏览器前进/后退按钮
* 4. 支持直接URL访问深链接
*
* URL格式: /?page=/module/index.ui
*
* 工作原理
* - 拦截bricks.buildUrlwidgetHandler当target是sage_main_content时记录路由
* - 使用History API (pushState/replaceState)更新URL
* - 监听popstate事件处理前进/后退
* - 页面加载时检查URL参数并恢复状态
*/
(function() {
'use strict';
// 路由配置
var ROUTE_PARAM = 'page';
var MAIN_CONTENT_ID = 'sage_main_content';
// 路由状态
var currentRoute = null;
var isPopState = false;
var routerReady = false;
// ── URL 操作 ──
function getRouteFromURL() {
var params = new URLSearchParams(window.location.search);
return params.get(ROUTE_PARAM) || null;
}
function pushRoute(route) {
var url = new URL(window.location);
url.searchParams.set(ROUTE_PARAM, route);
history.pushState({ route: route }, '', url.toString());
currentRoute = route;
}
function replaceRoute(route) {
var url = new URL(window.location);
if (route) {
url.searchParams.set(ROUTE_PARAM, route);
} else {
url.searchParams.delete(ROUTE_PARAM);
}
history.replaceState({ route: route }, '', url.toString());
currentRoute = route;
}
// ── 路由加载 ──
function waitForApp(maxWait) {
return new Promise(function(resolve) {
if (bricks && bricks.app) {
resolve(true);
return;
}
var waited = 0;
var interval = setInterval(function() {
waited += 100;
if ((bricks && bricks.app) || waited >= maxWait) {
clearInterval(interval);
resolve(!!(bricks && bricks.app));
}
}, 100);
});
}
function getMainContent() {
if (!bricks || !bricks.app) return null;
return bricks.getWidgetById(MAIN_CONTENT_ID, bricks.app);
}
/**
* 程序化加载路由构建urlwidget并放入sage_main_content
*/
async function loadRoute(route, pushHistory) {
if (!route) return;
var ready = await waitForApp(5000);
if (!ready) {
console.error('[SPA Router] bricks.app not ready');
return;
}
var mainContent = getMainContent();
if (!mainContent) {
console.error('[SPA Router] #' + MAIN_CONTENT_ID + ' not found');
return;
}
// 避免重复加载
if (currentRoute === route && !isPopState) return;
console.log('[SPA Router] Loading:', route);
var desc = {
widgettype: 'urlwidget',
options: { url: route }
};
try {
var widget = await bricks.widgetBuild(desc, bricks.app);
if (widget && !(widget instanceof bricks.Popup) && !(widget instanceof bricks.NewWindow)) {
mainContent.clear_widgets();
mainContent.add_widget(widget);
currentRoute = route;
if (pushHistory && !isPopState) {
pushRoute(route);
}
}
} catch (e) {
console.error('[SPA Router] Load failed:', route, e);
}
isPopState = false;
}
// ── 拦截 buildUrlwidgetHandler ──
function installInterceptor() {
var _orig = bricks.buildUrlwidgetHandler;
bricks.buildUrlwidgetHandler = function(w, target, rtdata, desc) {
var url = desc && desc.options ? desc.options.url : null;
var targetId = target ? target.id : null;
// 只拦截目标是sage_main_content的urlwidget
if (targetId === MAIN_CONTENT_ID && url && routerReady) {
// 获取原始handler
var handler = _orig(w, target, rtdata, desc);
if (typeof handler === 'function') {
// 包装执行原始handler后更新URL
return async function() {
await handler();
// 跳过已经是当前路由的情况
if (currentRoute !== url && !isPopState) {
console.log('[SPA Router] Route changed:', url);
pushRoute(url);
}
};
}
}
return _orig(w, target, rtdata, desc);
};
console.log('[SPA Router] Interceptor installed');
}
// ── popstate 处理 ──
function onPopState(event) {
var route = (event.state && event.state.route) || getRouteFromURL();
console.log('[SPA Router] popstate →', route);
isPopState = true;
if (route) {
loadRoute(route, false);
}
}
// ── 初始化 ──
async function init() {
console.log('[SPA Router] Initializing...');
// 等bricks.js加载完
if (typeof bricks === 'undefined') {
await waitForApp(10000);
}
if (!bricks) {
console.error('[SPA Router] bricks not found, aborting');
return;
}
installInterceptor();
window.addEventListener('popstate', onPopState);
// 检查URL是否有初始路由
var initialRoute = getRouteFromURL();
if (initialRoute) {
console.log('[SPA Router] Deep link:', initialRoute);
// 等shell加载完再恢复
await waitForApp(5000);
await new Promise(function(r) { setTimeout(r, 300); });
await loadRoute(initialRoute, false);
// 用replaceState标记初始状态
replaceRoute(initialRoute);
} else {
// 记录初始状态
replaceRoute(null);
}
routerReady = true;
console.log('[SPA Router] Ready');
}
// 启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// 调试API
window.BricksRouter = {
loadRoute: function(route) { return loadRoute(route, true); },
current: function() { return currentRoute; },
back: function() { history.back(); },
forward: function() { history.forward(); }
};
})();