- 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
224 lines
6.5 KiB
JavaScript
224 lines
6.5 KiB
JavaScript
/**
|
||
* 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(); }
|
||
};
|
||
|
||
})();
|