slicejs-web-framework 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -4,12 +4,16 @@ export default class Router {
|
|
|
4
4
|
this.activeRoute = null;
|
|
5
5
|
this.pathToRouteMap = this.createPathToRouteMap(routes);
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// Navigation Guards
|
|
8
|
+
this._beforeEachGuard = null;
|
|
9
|
+
this._afterEachGuard = null;
|
|
10
|
+
|
|
11
|
+
// Sistema de caché optimizado
|
|
8
12
|
this.routeContainersCache = new Map();
|
|
9
13
|
this.lastCacheUpdate = 0;
|
|
10
14
|
this.CACHE_DURATION = 100; // ms - caché muy corto pero efectivo
|
|
11
15
|
|
|
12
|
-
//
|
|
16
|
+
// Observer para invalidar caché automáticamente
|
|
13
17
|
this.setupMutationObserver();
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -18,7 +22,276 @@ export default class Router {
|
|
|
18
22
|
window.addEventListener('popstate', this.onRouteChange.bind(this));
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
//
|
|
25
|
+
// ============================================
|
|
26
|
+
// NAVIGATION GUARDS API
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Registra un guard que se ejecuta ANTES de cada navegación
|
|
31
|
+
* Puede bloquear o redirigir la navegación
|
|
32
|
+
* @param {Function} guard - Función (to, from, next) => {}
|
|
33
|
+
*/
|
|
34
|
+
beforeEach(guard) {
|
|
35
|
+
if (typeof guard !== 'function') {
|
|
36
|
+
slice.logger.logError('Router', 'beforeEach expects a function');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this._beforeEachGuard = guard;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registra un guard que se ejecuta DESPUÉS de cada navegación
|
|
44
|
+
* No puede bloquear la navegación
|
|
45
|
+
* @param {Function} guard - Función (to, from) => {}
|
|
46
|
+
*/
|
|
47
|
+
afterEach(guard) {
|
|
48
|
+
if (typeof guard !== 'function') {
|
|
49
|
+
slice.logger.logError('Router', 'afterEach expects a function');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this._afterEachGuard = guard;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Crea un objeto con información de ruta para los guards
|
|
57
|
+
* @param {Object} route - Objeto de ruta
|
|
58
|
+
* @param {Object} params - Parámetros de la ruta
|
|
59
|
+
* @returns {Object} Objeto con path, component, params, query, metadata
|
|
60
|
+
*/
|
|
61
|
+
_createRouteInfo(route, params = {}) {
|
|
62
|
+
if (!route) {
|
|
63
|
+
return {
|
|
64
|
+
path: '/404',
|
|
65
|
+
component: 'NotFound',
|
|
66
|
+
params: {},
|
|
67
|
+
query: this._parseQueryParams(),
|
|
68
|
+
metadata: {}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
path: route.fullPath || route.path,
|
|
74
|
+
component: route.parentRoute ? route.parentRoute.component : route.component,
|
|
75
|
+
params: params,
|
|
76
|
+
query: this._parseQueryParams(),
|
|
77
|
+
metadata: route.metadata || {}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parsea los query parameters de la URL actual
|
|
83
|
+
* @returns {Object} Objeto con los query params
|
|
84
|
+
*/
|
|
85
|
+
_parseQueryParams() {
|
|
86
|
+
const queryString = window.location.search;
|
|
87
|
+
if (!queryString) return {};
|
|
88
|
+
|
|
89
|
+
const params = {};
|
|
90
|
+
const urlParams = new URLSearchParams(queryString);
|
|
91
|
+
|
|
92
|
+
for (const [key, value] of urlParams) {
|
|
93
|
+
params[key] = value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return params;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Ejecuta el beforeEach guard si existe
|
|
101
|
+
* @param {Object} to - Información de ruta destino
|
|
102
|
+
* @param {Object} from - Información de ruta origen
|
|
103
|
+
* @returns {String|null} Path de redirección o null si continúa
|
|
104
|
+
*/
|
|
105
|
+
async _executeBeforeEachGuard(to, from) {
|
|
106
|
+
if (!this._beforeEachGuard) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let redirectPath = null;
|
|
111
|
+
let nextCalled = false;
|
|
112
|
+
|
|
113
|
+
const next = (path) => {
|
|
114
|
+
if (nextCalled) {
|
|
115
|
+
slice.logger.logWarning('Router', 'next() called multiple times in guard');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
nextCalled = true;
|
|
119
|
+
|
|
120
|
+
if (path && typeof path === 'string') {
|
|
121
|
+
redirectPath = path;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await this._beforeEachGuard(to, from, next);
|
|
127
|
+
|
|
128
|
+
// Si no se llamó next(), loguear advertencia pero continuar
|
|
129
|
+
if (!nextCalled) {
|
|
130
|
+
slice.logger.logWarning(
|
|
131
|
+
'Router',
|
|
132
|
+
'beforeEach guard did not call next(). Navigation will continue.'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return redirectPath;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
slice.logger.logError('Router', 'Error in beforeEach guard', error);
|
|
139
|
+
return null; // En caso de error, continuar con la navegación
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Ejecuta el afterEach guard si existe
|
|
145
|
+
* @param {Object} to - Información de ruta destino
|
|
146
|
+
* @param {Object} from - Información de ruta origen
|
|
147
|
+
*/
|
|
148
|
+
_executeAfterEachGuard(to, from) {
|
|
149
|
+
if (!this._afterEachGuard) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
this._afterEachGuard(to, from);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
slice.logger.logError('Router', 'Error in afterEach guard', error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================
|
|
161
|
+
// ROUTING CORE (MODIFICADO CON GUARDS)
|
|
162
|
+
// ============================================
|
|
163
|
+
|
|
164
|
+
async navigate(path) {
|
|
165
|
+
const currentPath = window.location.pathname;
|
|
166
|
+
|
|
167
|
+
// Obtener información de ruta actual
|
|
168
|
+
const { route: fromRoute, params: fromParams } = this.matchRoute(currentPath);
|
|
169
|
+
const from = this._createRouteInfo(fromRoute, fromParams);
|
|
170
|
+
|
|
171
|
+
// Obtener información de ruta destino
|
|
172
|
+
const { route: toRoute, params: toParams } = this.matchRoute(path);
|
|
173
|
+
const to = this._createRouteInfo(toRoute, toParams);
|
|
174
|
+
|
|
175
|
+
// EJECUTAR BEFORE EACH GUARD
|
|
176
|
+
const redirectPath = await this._executeBeforeEachGuard(to, from);
|
|
177
|
+
|
|
178
|
+
// Si el guard redirige, navegar a la nueva ruta
|
|
179
|
+
if (redirectPath) {
|
|
180
|
+
// Evitar loops infinitos
|
|
181
|
+
if (redirectPath === path) {
|
|
182
|
+
slice.logger.logError('Router', `Guard redirection loop detected: ${path}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
return this.navigate(redirectPath);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Continuar con la navegación normal
|
|
189
|
+
window.history.pushState({}, path, window.location.origin + path);
|
|
190
|
+
await this._performNavigation(to, from);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Método interno para ejecutar la navegación después de pasar los guards
|
|
195
|
+
* @param {Object} to - Información de ruta destino
|
|
196
|
+
* @param {Object} from - Información de ruta origen
|
|
197
|
+
*/
|
|
198
|
+
async _performNavigation(to, from) {
|
|
199
|
+
// Renderizar la nueva ruta
|
|
200
|
+
await this.onRouteChange();
|
|
201
|
+
|
|
202
|
+
// EJECUTAR AFTER EACH GUARD
|
|
203
|
+
this._executeAfterEachGuard(to, from);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async onRouteChange() {
|
|
207
|
+
// Cancelar el timeout anterior si existe
|
|
208
|
+
if (this.routeChangeTimeout) {
|
|
209
|
+
clearTimeout(this.routeChangeTimeout);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Debounce de 10ms para evitar múltiples llamadas seguidas
|
|
213
|
+
this.routeChangeTimeout = setTimeout(async () => {
|
|
214
|
+
const path = window.location.pathname;
|
|
215
|
+
const routeContainersFlag = await this.renderRoutesComponentsInPage();
|
|
216
|
+
|
|
217
|
+
if (routeContainersFlag) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { route, params } = this.matchRoute(path);
|
|
222
|
+
if (route) {
|
|
223
|
+
await this.handleRoute(route, params);
|
|
224
|
+
}
|
|
225
|
+
}, 10);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async handleRoute(route, params) {
|
|
229
|
+
const targetElement = document.querySelector('#app');
|
|
230
|
+
|
|
231
|
+
const componentName = route.parentRoute ? route.parentRoute.component : route.component;
|
|
232
|
+
const sliceId = `route-${componentName}`;
|
|
233
|
+
|
|
234
|
+
const existingComponent = slice.controller.getComponent(sliceId);
|
|
235
|
+
|
|
236
|
+
if (slice.loading) {
|
|
237
|
+
slice.loading.start();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (existingComponent) {
|
|
241
|
+
targetElement.innerHTML = '';
|
|
242
|
+
if (existingComponent.update) {
|
|
243
|
+
existingComponent.props = { ...existingComponent.props, ...params };
|
|
244
|
+
await existingComponent.update();
|
|
245
|
+
}
|
|
246
|
+
targetElement.appendChild(existingComponent);
|
|
247
|
+
await this.renderRoutesInComponent(existingComponent);
|
|
248
|
+
} else {
|
|
249
|
+
const component = await slice.build(componentName, {
|
|
250
|
+
params,
|
|
251
|
+
sliceId: sliceId,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
targetElement.innerHTML = '';
|
|
255
|
+
targetElement.appendChild(component);
|
|
256
|
+
|
|
257
|
+
await this.renderRoutesInComponent(component);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Invalidar caché después de cambios importantes en el DOM
|
|
261
|
+
this.invalidateCache();
|
|
262
|
+
|
|
263
|
+
if (slice.loading) {
|
|
264
|
+
slice.loading.stop();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
slice.router.activeRoute = route;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async loadInitialRoute() {
|
|
271
|
+
const path = window.location.pathname;
|
|
272
|
+
const { route, params } = this.matchRoute(path);
|
|
273
|
+
|
|
274
|
+
// Para la carga inicial, también ejecutar guards
|
|
275
|
+
const from = this._createRouteInfo(null, {});
|
|
276
|
+
const to = this._createRouteInfo(route, params);
|
|
277
|
+
|
|
278
|
+
// EJECUTAR BEFORE EACH GUARD en carga inicial
|
|
279
|
+
const redirectPath = await this._executeBeforeEachGuard(to, from);
|
|
280
|
+
|
|
281
|
+
if (redirectPath) {
|
|
282
|
+
return this.navigate(redirectPath);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await this.handleRoute(route, params);
|
|
286
|
+
|
|
287
|
+
// EJECUTAR AFTER EACH GUARD en carga inicial
|
|
288
|
+
this._executeAfterEachGuard(to, from);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================
|
|
292
|
+
// MÉTODOS EXISTENTES (SIN CAMBIOS)
|
|
293
|
+
// ============================================
|
|
294
|
+
|
|
22
295
|
setupMutationObserver() {
|
|
23
296
|
if (typeof MutationObserver !== 'undefined') {
|
|
24
297
|
this.observer = new MutationObserver((mutations) => {
|
|
@@ -26,7 +299,6 @@ export default class Router {
|
|
|
26
299
|
|
|
27
300
|
mutations.forEach((mutation) => {
|
|
28
301
|
if (mutation.type === 'childList') {
|
|
29
|
-
// Solo invalidar si se añadieron/removieron nodos que podrían ser rutas
|
|
30
302
|
const addedNodes = Array.from(mutation.addedNodes);
|
|
31
303
|
const removedNodes = Array.from(mutation.removedNodes);
|
|
32
304
|
|
|
@@ -55,7 +327,6 @@ export default class Router {
|
|
|
55
327
|
}
|
|
56
328
|
}
|
|
57
329
|
|
|
58
|
-
// NUEVO: Invalidar caché
|
|
59
330
|
invalidateCache() {
|
|
60
331
|
this.routeContainersCache.clear();
|
|
61
332
|
this.lastCacheUpdate = 0;
|
|
@@ -92,14 +363,12 @@ export default class Router {
|
|
|
92
363
|
return pathToRouteMap;
|
|
93
364
|
}
|
|
94
365
|
|
|
95
|
-
// OPTIMIZADO: Sistema de caché inteligente
|
|
96
366
|
async renderRoutesComponentsInPage(searchContainer = document) {
|
|
97
367
|
let routerContainersFlag = false;
|
|
98
368
|
const routeContainers = this.getCachedRouteContainers(searchContainer);
|
|
99
369
|
|
|
100
370
|
for (const routeContainer of routeContainers) {
|
|
101
371
|
try {
|
|
102
|
-
// Verificar que el componente aún esté conectado al DOM
|
|
103
372
|
if (!routeContainer.isConnected) {
|
|
104
373
|
this.invalidateCache();
|
|
105
374
|
continue;
|
|
@@ -118,18 +387,15 @@ export default class Router {
|
|
|
118
387
|
return routerContainersFlag;
|
|
119
388
|
}
|
|
120
389
|
|
|
121
|
-
// NUEVO: Obtener contenedores con caché
|
|
122
390
|
getCachedRouteContainers(container) {
|
|
123
391
|
const containerKey = container === document ? 'document' : container.sliceId || 'anonymous';
|
|
124
392
|
const now = Date.now();
|
|
125
393
|
|
|
126
|
-
// Verificar si el caché es válido
|
|
127
394
|
if (this.routeContainersCache.has(containerKey) &&
|
|
128
395
|
(now - this.lastCacheUpdate) < this.CACHE_DURATION) {
|
|
129
396
|
return this.routeContainersCache.get(containerKey);
|
|
130
397
|
}
|
|
131
398
|
|
|
132
|
-
// Regenerar caché
|
|
133
399
|
const routeContainers = this.findAllRouteContainersOptimized(container);
|
|
134
400
|
this.routeContainersCache.set(containerKey, routeContainers);
|
|
135
401
|
this.lastCacheUpdate = now;
|
|
@@ -137,17 +403,14 @@ export default class Router {
|
|
|
137
403
|
return routeContainers;
|
|
138
404
|
}
|
|
139
405
|
|
|
140
|
-
// OPTIMIZADO: Búsqueda más eficiente usando TreeWalker
|
|
141
406
|
findAllRouteContainersOptimized(container) {
|
|
142
407
|
const routeContainers = [];
|
|
143
408
|
|
|
144
|
-
// Usar TreeWalker para una búsqueda más eficiente
|
|
145
409
|
const walker = document.createTreeWalker(
|
|
146
410
|
container,
|
|
147
411
|
NodeFilter.SHOW_ELEMENT,
|
|
148
412
|
{
|
|
149
413
|
acceptNode: (node) => {
|
|
150
|
-
// Solo aceptar nodos que sean slice-route o slice-multi-route
|
|
151
414
|
if (node.tagName === 'SLICE-ROUTE' || node.tagName === 'SLICE-MULTI-ROUTE') {
|
|
152
415
|
return NodeFilter.FILTER_ACCEPT;
|
|
153
416
|
}
|
|
@@ -164,7 +427,6 @@ export default class Router {
|
|
|
164
427
|
return routeContainers;
|
|
165
428
|
}
|
|
166
429
|
|
|
167
|
-
// NUEVO: Método específico para renderizar rutas dentro de un componente
|
|
168
430
|
async renderRoutesInComponent(component) {
|
|
169
431
|
if (!component) {
|
|
170
432
|
slice.logger.logWarning('Router', 'No component provided for route rendering');
|
|
@@ -174,85 +436,6 @@ export default class Router {
|
|
|
174
436
|
return await this.renderRoutesComponentsInPage(component);
|
|
175
437
|
}
|
|
176
438
|
|
|
177
|
-
// OPTIMIZADO: Debouncing para evitar múltiples llamadas seguidas
|
|
178
|
-
async onRouteChange() {
|
|
179
|
-
// Cancelar el timeout anterior si existe
|
|
180
|
-
if (this.routeChangeTimeout) {
|
|
181
|
-
clearTimeout(this.routeChangeTimeout);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Debounce de 10ms para evitar múltiples llamadas seguidas
|
|
185
|
-
this.routeChangeTimeout = setTimeout(async () => {
|
|
186
|
-
const path = window.location.pathname;
|
|
187
|
-
const routeContainersFlag = await this.renderRoutesComponentsInPage();
|
|
188
|
-
|
|
189
|
-
if (routeContainersFlag) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const { route, params } = this.matchRoute(path);
|
|
194
|
-
if (route) {
|
|
195
|
-
await this.handleRoute(route, params);
|
|
196
|
-
}
|
|
197
|
-
}, 10);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async navigate(path) {
|
|
201
|
-
window.history.pushState({}, path, window.location.origin + path);
|
|
202
|
-
await this.onRouteChange();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async handleRoute(route, params) {
|
|
206
|
-
const targetElement = document.querySelector('#app');
|
|
207
|
-
|
|
208
|
-
const componentName = route.parentRoute ? route.parentRoute.component : route.component;
|
|
209
|
-
const sliceId = `route-${componentName}`;
|
|
210
|
-
|
|
211
|
-
const existingComponent = slice.controller.getComponent(sliceId);
|
|
212
|
-
|
|
213
|
-
if (slice.loading) {
|
|
214
|
-
slice.loading.start();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (existingComponent) {
|
|
218
|
-
targetElement.innerHTML = '';
|
|
219
|
-
if (existingComponent.update) {
|
|
220
|
-
existingComponent.props = { ...existingComponent.props, ...params };
|
|
221
|
-
await existingComponent.update();
|
|
222
|
-
}
|
|
223
|
-
targetElement.appendChild(existingComponent);
|
|
224
|
-
// Renderizar DESPUÉS de insertar (pero antes de mostrar)
|
|
225
|
-
await this.renderRoutesInComponent(existingComponent);
|
|
226
|
-
} else {
|
|
227
|
-
const component = await slice.build(componentName, {
|
|
228
|
-
params,
|
|
229
|
-
sliceId: sliceId,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
targetElement.innerHTML = '';
|
|
233
|
-
targetElement.appendChild(component);
|
|
234
|
-
|
|
235
|
-
// Renderizar INMEDIATAMENTE después de insertar
|
|
236
|
-
await this.renderRoutesInComponent(component);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Invalidar caché después de cambios importantes en el DOM
|
|
240
|
-
this.invalidateCache();
|
|
241
|
-
|
|
242
|
-
if (slice.loading) {
|
|
243
|
-
slice.loading.stop();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
slice.router.activeRoute = route;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async loadInitialRoute() {
|
|
250
|
-
const path = window.location.pathname;
|
|
251
|
-
const { route, params } = this.matchRoute(path);
|
|
252
|
-
|
|
253
|
-
await this.handleRoute(route, params);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
439
|
matchRoute(path) {
|
|
257
440
|
const exactMatch = this.pathToRouteMap.get(path);
|
|
258
441
|
if (exactMatch) {
|