onelaraveljs 1.21.0 → 1.21.2
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.
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# 📋 Phân Tích Chi Tiết: Luồng & Logic Hoạt Động của mountView
|
|
2
|
+
|
|
3
|
+
## 🎯 Tổng Quan
|
|
4
|
+
|
|
5
|
+
**mountView** là hàm chính dùng để **mount/render một view** vào DOM. Nó xử lý:
|
|
6
|
+
- Cache layout (tránh destroy layout không cần thiết)
|
|
7
|
+
- Destroy layout cũ nếu cần
|
|
8
|
+
- Render HTML mới
|
|
9
|
+
- Mount lifecycle hooks
|
|
10
|
+
- Scroll position handling
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 📊 Luồng Hoạt Động Chính
|
|
15
|
+
|
|
16
|
+
### **PHASE 1: Lưu thông tin layout cũ (TRƯỚC loadView)**
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
const oldSuperViewPath = this.CURRENT_SUPER_VIEW_PATH;
|
|
20
|
+
const oldSuperView = this.CURRENT_SUPER_VIEW;
|
|
21
|
+
const oldPageView = this.PAGE_VIEW;
|
|
22
|
+
|
|
23
|
+
if (oldSuperView && oldSuperView instanceof ViewEngine) {
|
|
24
|
+
oldSuperView.__._lifecycleManager.unmounted();
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Mục đích:**
|
|
29
|
+
- Lưu lại path của layout cũ trước khi `loadView()` thay đổi nó
|
|
30
|
+
- Gọi `unmounted()` cleanup CSS/scripts của layout cũ
|
|
31
|
+
|
|
32
|
+
**Tại sao cần?**
|
|
33
|
+
- Nếu không lưu, sẽ mất thông tin khi check cache ở bước 2
|
|
34
|
+
- `loadView()` sẽ reset `CURRENT_SUPER_VIEW` → không thể so sánh
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### **PHASE 2: Gọi loadView() - Render view mới**
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const viewResult = this.loadView(viewName, params, route?.$urlPath || '');
|
|
42
|
+
if (viewResult.error) {
|
|
43
|
+
console.error('View rendering error:', viewResult.error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Có 2 trường hợp:**
|
|
49
|
+
|
|
50
|
+
#### **2A. Normal Rendering (Client-Side)**
|
|
51
|
+
```
|
|
52
|
+
loadView()
|
|
53
|
+
↓
|
|
54
|
+
├─ 1. Tạo view instance: view(name, data)
|
|
55
|
+
├─ 2. Check view.__.hasSuperView (có layout không?)
|
|
56
|
+
├─ 3. Nếu có → Tìm super view → Render layout
|
|
57
|
+
├─ 4. Store trong PAGE_VIEW, CURRENT_SUPER_VIEW
|
|
58
|
+
└─ 5. Return {html, superView, ultraView, needInsert}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### **2B. SSR Scanning (Server-Side)**
|
|
62
|
+
```
|
|
63
|
+
scanView()
|
|
64
|
+
↓
|
|
65
|
+
├─ 1. Lấy SSR data từ HTML comments
|
|
66
|
+
├─ 2. Tạo view instances từ SSR data
|
|
67
|
+
├─ 3. Call virtualRender() (chỉ setup relationships, không render HTML)
|
|
68
|
+
├─ 4. Scan DOM + attach event handlers
|
|
69
|
+
└─ 5. Return {html từ SSR, superView, ultraView}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### **PHASE 3: Kiểm tra Cache Layout**
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
const newSuperViewPath = viewResult.superView?.__.path;
|
|
78
|
+
const isSameLayout = newSuperViewPath === oldSuperViewPath;
|
|
79
|
+
|
|
80
|
+
if (!isSameLayout) {
|
|
81
|
+
if (oldSuperView && oldSuperView instanceof ViewEngine) {
|
|
82
|
+
oldSuperView.__._lifecycleManager.unmounted();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Logic:**
|
|
88
|
+
| Kịch bản | isSameLayout | Hành động |
|
|
89
|
+
|---------|-------------|----------|
|
|
90
|
+
| Home → User List | `true` | ❌ KHÔNG destroy layout → Reuse |
|
|
91
|
+
| Home (bảng) → Home (kảnh) | `true` | ❌ KHÔNG destroy layout → Reuse |
|
|
92
|
+
| Home (bảng) → Admin | `false` | ✅ Destroy layout cũ |
|
|
93
|
+
| Home → Login | `false` | ✅ Destroy layout cũ |
|
|
94
|
+
|
|
95
|
+
**Ưu điểm:**
|
|
96
|
+
- Tránh destroy/recreate layout không cần thiết
|
|
97
|
+
- CSS/JS chỉ load 1 lần nếu layout không đổi
|
|
98
|
+
- Performance tốt hơn
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### **PHASE 4: Render HTML & Insert DOM**
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
if (viewResult.needInsert && viewResult.html) {
|
|
106
|
+
const container = this.container ||
|
|
107
|
+
document.querySelector('#app-root') ||
|
|
108
|
+
document.querySelector('#app') ||
|
|
109
|
+
document.body;
|
|
110
|
+
if (container) {
|
|
111
|
+
OneDOM.setHTML(container, html);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Trường hợp `needInsert = false`:**
|
|
117
|
+
- SSR mode: HTML từ server, DOM đã có → chỉ attach events
|
|
118
|
+
- Layout cache: Content view đổi, layout cũ → chỉ update content
|
|
119
|
+
|
|
120
|
+
**Trường hợp `needInsert = true`:**
|
|
121
|
+
- CSR mode: Render HTML từ client
|
|
122
|
+
- Layout khác: Phải render lại toàn bộ
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### **PHASE 5: Emit & Mount Lifecycle**
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
if (this.emitChangedSections) {
|
|
130
|
+
this.emitChangedSections();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (viewResult.ultraView && viewResult.ultraView instanceof ViewEngine) {
|
|
134
|
+
viewResult.ultraView.__._lifecycleManager.mounted();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.CURRENT_SUPER_VIEW_MOUNTED = true;
|
|
138
|
+
this.PAGE_VIEW?.__.scrollToOldPosition() || this.scrollToTop();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Chi tiết:**
|
|
142
|
+
|
|
143
|
+
| Bước | Hành động | Mục đích |
|
|
144
|
+
|------|----------|---------|
|
|
145
|
+
| 1 | `emitChangedSections()` | Notify UI sections thay đổi |
|
|
146
|
+
| 2 | `mounted()` lifecycle | Chạy hook mounted của views |
|
|
147
|
+
| 3 | Set flag `CURRENT_SUPER_VIEW_MOUNTED = true` | Đánh dấu layout đã mount |
|
|
148
|
+
| 4 | `scrollToOldPosition()` hoặc `scrollToTop()` | Xử lý scroll position |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 🔄 Luồng Tổng Quát (Visual)
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
mountView(viewName)
|
|
156
|
+
↓
|
|
157
|
+
├─ PHASE 1: Lưu layout cũ
|
|
158
|
+
│ └─ oldSuperView, oldSuperViewPath
|
|
159
|
+
│
|
|
160
|
+
├─ PHASE 2: loadView() → Render view mới
|
|
161
|
+
│ ├─ CSR: render() → HTML
|
|
162
|
+
│ └─ SSR: virtualRender() → relationships only
|
|
163
|
+
│ └─ Store: viewResult
|
|
164
|
+
│
|
|
165
|
+
├─ PHASE 3: Cache check
|
|
166
|
+
│ ├─ isSameLayout = newPath === oldPath?
|
|
167
|
+
│ └─ Nếu khác → Destroy oldSuperView
|
|
168
|
+
│
|
|
169
|
+
├─ PHASE 4: Insert DOM
|
|
170
|
+
│ └─ OneDOM.setHTML(container, html)
|
|
171
|
+
│
|
|
172
|
+
└─ PHASE 5: Mount lifecycle
|
|
173
|
+
├─ emitChangedSections()
|
|
174
|
+
├─ viewResult.ultraView.mounted()
|
|
175
|
+
├─ Set CURRENT_SUPER_VIEW_MOUNTED = true
|
|
176
|
+
└─ Handle scroll position
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 🎯 Hàm Liên Quan
|
|
182
|
+
|
|
183
|
+
### **1. loadView(name, data, urlPath)**
|
|
184
|
+
|
|
185
|
+
**Input:** View name, data, URL path
|
|
186
|
+
**Output:** `{ html, superView, ultraView, needInsert, error }`
|
|
187
|
+
|
|
188
|
+
**Logic:**
|
|
189
|
+
```
|
|
190
|
+
loadView()
|
|
191
|
+
├─ Check cache: viewStoreKey
|
|
192
|
+
├─ Nếu cached → return cached HTML
|
|
193
|
+
├─ Nếu không:
|
|
194
|
+
│ ├─ Create view: this.view(name, data)
|
|
195
|
+
│ ├─ Check super view: view.__.hasSuperView?
|
|
196
|
+
│ ├─ Nếu có → renderOrScanView(superView, mode='csr')
|
|
197
|
+
│ ├─ Store PAGE_VIEW, CURRENT_SUPER_VIEW
|
|
198
|
+
│ └─ Return { html, superView, ultraView, needInsert }
|
|
199
|
+
└─ Catch error → Return { error }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Cache strategy:**
|
|
203
|
+
```javascript
|
|
204
|
+
const viewStoreKey = name.replace('.', '_') + '_' + urlPath?.replace(/[\/\:]/g, '_');
|
|
205
|
+
const cachedPageView = this.cachedPageViews.get(viewStoreKey);
|
|
206
|
+
if (cachedPageView instanceof ViewEngine) {
|
|
207
|
+
return cachedHTML; // Reuse!
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### **2. scanView(name)**
|
|
214
|
+
|
|
215
|
+
**Input:** View name (SSR mode)
|
|
216
|
+
**Output:** `{ html, superView, ultraView, needInsert, error }`
|
|
217
|
+
|
|
218
|
+
**Logic:**
|
|
219
|
+
```
|
|
220
|
+
scanView()
|
|
221
|
+
├─ Get SSR data from server
|
|
222
|
+
├─ this.ssrViewManager.scan(name)
|
|
223
|
+
├─ Create view instance from SSR data
|
|
224
|
+
├─ Loop through super views:
|
|
225
|
+
│ ├─ While view.__.hasSuperView:
|
|
226
|
+
│ │ ├─ Get super view path
|
|
227
|
+
│ │ ├─ Call scanRenderedView(view)
|
|
228
|
+
│ │ ├─ Scan SSR data for super view
|
|
229
|
+
│ │ └─ Call view.__.__scan(ssrData)
|
|
230
|
+
│ └─ Attach event handlers via DOM scan
|
|
231
|
+
├─ Build ALL_VIEW_STACK (all views in hierarchy)
|
|
232
|
+
└─ Return { html, superView, ultraView, needInsert }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Vòng lặp super views:**
|
|
236
|
+
```
|
|
237
|
+
PAGE_VIEW → LAYOUT1 → LAYOUT2 → ROOT_LAYOUT
|
|
238
|
+
↑ ↓
|
|
239
|
+
└────────── ALL_VIEW_STACK ──────────┘
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### **3. renderOrScanView(view, variableData, mode)**
|
|
245
|
+
|
|
246
|
+
**Input:** View, data, mode ('csr' or 'ssr')
|
|
247
|
+
**Output:** Rendered HTML (CSR) or Nothing (SSR)
|
|
248
|
+
|
|
249
|
+
**Logic:**
|
|
250
|
+
```
|
|
251
|
+
renderOrScanView()
|
|
252
|
+
├─ Determine mode:
|
|
253
|
+
│ ├─ CSR → use render(), prerender()
|
|
254
|
+
│ └─ SSR → use virtualRender(), virtualPrerender()
|
|
255
|
+
│
|
|
256
|
+
├─ CASE 1: No async data
|
|
257
|
+
│ └─ view.render() → return HTML
|
|
258
|
+
│
|
|
259
|
+
├─ CASE 2: Has @await directive
|
|
260
|
+
│ ├─ CSR: Load data from current URL
|
|
261
|
+
│ │ ├─ getURIData() → fetch from API
|
|
262
|
+
│ │ ├─ Store in this.store
|
|
263
|
+
│ │ └─ view.refresh(data)
|
|
264
|
+
│ └─ SSR: Just setup relationships
|
|
265
|
+
│
|
|
266
|
+
└─ CASE 3: Has @fetch directive
|
|
267
|
+
├─ CSR: Fetch data using config
|
|
268
|
+
│ ├─ this.App.Http.request(config)
|
|
269
|
+
│ └─ view.refresh(response.data)
|
|
270
|
+
└─ SSR: Just setup relationships
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
### **4. clearOldRendering()**
|
|
276
|
+
|
|
277
|
+
**Mục đích:** Reset state giữa renders
|
|
278
|
+
|
|
279
|
+
**Logic:**
|
|
280
|
+
```
|
|
281
|
+
clearOldRendering()
|
|
282
|
+
├─ 1. Clear templates cache
|
|
283
|
+
│ └─ Object.keys(templates).forEach(clear)
|
|
284
|
+
│
|
|
285
|
+
├─ 2. Clear cachedViews
|
|
286
|
+
│ └─ Delete old view instances
|
|
287
|
+
│
|
|
288
|
+
├─ 3. Clear stacks (GỮ LẠI cache info!)
|
|
289
|
+
│ ├─ ALL_VIEW_STACK = []
|
|
290
|
+
│ ├─ SUPER_VIEW_STACK = []
|
|
291
|
+
│ ├─ PAGE_VIEW = null
|
|
292
|
+
│ ├─ ⚠️ KHÔNG xóa: CURRENT_SUPER_VIEW_PATH
|
|
293
|
+
│ └─ ⚠️ KHÔNG xóa: CURRENT_SUPER_VIEW
|
|
294
|
+
│
|
|
295
|
+
└─ 4. Clear orphaned event data
|
|
296
|
+
└─ Prevent memory leaks
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Tại sao giữ cache info?**
|
|
300
|
+
- Để check layout cache ở `mountView()` PHASE 3
|
|
301
|
+
- Nếu xóa → sẽ luôn destroy layout (inefficient)
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
### **5. unmountView(view)**
|
|
306
|
+
|
|
307
|
+
**Mục đích:** Cleanup một view hoàn toàn
|
|
308
|
+
|
|
309
|
+
**Logic (5 bước):**
|
|
310
|
+
```
|
|
311
|
+
unmountView(view)
|
|
312
|
+
├─ 1. Call beforeUnmount() lifecycle
|
|
313
|
+
├─ 2. Call removeEvents() - remove listeners
|
|
314
|
+
├─ 3. Remove from viewMap
|
|
315
|
+
├─ 4. Call unmounted() lifecycle
|
|
316
|
+
└─ 5. Call destroy() if defined
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Khi nào gọi?**
|
|
320
|
+
- Layout thay đổi (PHASE 3)
|
|
321
|
+
- Navigation away from view
|
|
322
|
+
- Component cleanup
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 🔑 Key Concepts
|
|
327
|
+
|
|
328
|
+
### **1. View Hierarchy (Super Views)**
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
ROOT_LAYOUT (Master page)
|
|
332
|
+
↓
|
|
333
|
+
ADMIN_LAYOUT (Admin section)
|
|
334
|
+
↓
|
|
335
|
+
USER_LIST (Page view)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Scan order:** Bottom-up (USER_LIST → ADMIN_LAYOUT → ROOT_LAYOUT)
|
|
339
|
+
**Mount order:** Top-down (ROOT_LAYOUT → ADMIN_LAYOUT → USER_LIST)
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
### **2. Cache Strategy**
|
|
344
|
+
|
|
345
|
+
| Type | Key | TTL | Reuse |
|
|
346
|
+
|------|-----|-----|-------|
|
|
347
|
+
| Layout | `CURRENT_SUPER_VIEW_PATH` | Session | ✅ Reuse nếu giống |
|
|
348
|
+
| Page | `viewStoreKey` | 10 min | ✅ Reuse nếu không expired |
|
|
349
|
+
| Sections | `_sections[name]` | Session | ✅ Reuse |
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
### **3. CSR vs SSR Flow**
|
|
354
|
+
|
|
355
|
+
#### **CSR (Client-Side Rendering)**
|
|
356
|
+
```
|
|
357
|
+
mountView()
|
|
358
|
+
↓
|
|
359
|
+
loadView()
|
|
360
|
+
├─ render() → HTML
|
|
361
|
+
└─ Return { html, needInsert: true }
|
|
362
|
+
↓
|
|
363
|
+
OneDOM.setHTML(container, html)
|
|
364
|
+
↓
|
|
365
|
+
mounted() lifecycle
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### **SSR (Server-Side Rendering)**
|
|
369
|
+
```
|
|
370
|
+
mountViewScan()
|
|
371
|
+
↓
|
|
372
|
+
scanView()
|
|
373
|
+
├─ Get SSR HTML from server
|
|
374
|
+
├─ virtualRender() → setup relationships only
|
|
375
|
+
└─ Return { html, needInsert: false }
|
|
376
|
+
↓
|
|
377
|
+
OneDOM.setHTML(container, html) [optional]
|
|
378
|
+
↓
|
|
379
|
+
Scan DOM + attach events
|
|
380
|
+
↓
|
|
381
|
+
mounted() lifecycle
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
### **4. Data Flow: @await & @fetch**
|
|
387
|
+
|
|
388
|
+
#### **@await Directive**
|
|
389
|
+
```
|
|
390
|
+
View có @await
|
|
391
|
+
↓
|
|
392
|
+
renderOrScanView()
|
|
393
|
+
├─ CSR:
|
|
394
|
+
│ ├─ Check this.store[apiDataKey]
|
|
395
|
+
│ ├─ Nếu cache hit: view.refresh(cachedData)
|
|
396
|
+
│ └─ Nếu miss: this.App.Api.getURIData() → fetch → refresh
|
|
397
|
+
└─ SSR:
|
|
398
|
+
└─ Skip (data từ server)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### **@fetch Directive**
|
|
402
|
+
```
|
|
403
|
+
View có @fetch
|
|
404
|
+
↓
|
|
405
|
+
renderOrScanView()
|
|
406
|
+
├─ CSR:
|
|
407
|
+
│ ├─ Parse fetch config: { url, method, headers, ... }
|
|
408
|
+
│ ├─ this.App.Http.request(config)
|
|
409
|
+
│ └─ view.refresh(response.data)
|
|
410
|
+
└─ SSR:
|
|
411
|
+
└─ Skip (data từ server)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## ⚡ Performance Optimizations
|
|
417
|
+
|
|
418
|
+
### **1. Layout Cache**
|
|
419
|
+
```javascript
|
|
420
|
+
// ❌ BAD: Destroy layout mỗi lần navigate
|
|
421
|
+
if (layoutPath !== oldPath) {
|
|
422
|
+
destroy layout;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ✅ GOOD: Reuse layout nếu giống
|
|
426
|
+
if (newLayoutPath !== oldLayoutPath) {
|
|
427
|
+
destroy oldLayout;
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### **2. Page Cache**
|
|
432
|
+
```javascript
|
|
433
|
+
// Store page view đã render
|
|
434
|
+
this.cachedPageViews.set(viewStoreKey, viewInstance);
|
|
435
|
+
|
|
436
|
+
// Reuse nếu navigate lại
|
|
437
|
+
if (cachedPageView) {
|
|
438
|
+
return cachedView.html;
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### **3. Event Cleanup**
|
|
443
|
+
```javascript
|
|
444
|
+
// Avoid memory leaks
|
|
445
|
+
clearOldRendering() → clearOrphanedEventData();
|
|
446
|
+
|
|
447
|
+
// Clear only when needed
|
|
448
|
+
unmountView() → removeEvents();
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### **4. Scroll Position**
|
|
452
|
+
```javascript
|
|
453
|
+
// Remember old position
|
|
454
|
+
this.PAGE_VIEW?.__.scrollToOldPosition();
|
|
455
|
+
|
|
456
|
+
// Or go to top for new page
|
|
457
|
+
this.scrollToTop();
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 🐛 Potential Issues & Solutions
|
|
463
|
+
|
|
464
|
+
### **Issue 1: Layout cache mất khi refresh**
|
|
465
|
+
**Solution:** Store `CURRENT_SUPER_VIEW_PATH` persistent
|
|
466
|
+
|
|
467
|
+
### **Issue 2: Memory leak từ event listeners**
|
|
468
|
+
**Solution:** `clearOrphanedEventData()` + `removeEvents()`
|
|
469
|
+
|
|
470
|
+
### **Issue 3: Data stale trong @await**
|
|
471
|
+
**Solution:** Cache key includes `urlPath` → Cache per route
|
|
472
|
+
|
|
473
|
+
### **Issue 4: SSR hydration mismatch**
|
|
474
|
+
**Solution:** `virtualRender()` setup relationships before scan
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 📝 Summary Table
|
|
479
|
+
|
|
480
|
+
| Hàm | Mục đích | Input | Output | Khi gọi |
|
|
481
|
+
|-----|----------|-------|--------|---------|
|
|
482
|
+
| **mountView** | Main mount logic | viewName, params | HTML inserted | Navigation |
|
|
483
|
+
| **loadView** | Render view (CSR) | name, data, urlPath | {html, superView} | mountView PHASE 2 |
|
|
484
|
+
| **scanView** | Scan SSR HTML | name | {html, views} | mountViewScan |
|
|
485
|
+
| **renderOrScanView** | Render/scan with async | view, data, mode | HTML or None | loadView, scanView |
|
|
486
|
+
| **clearOldRendering** | Reset state | - | - | Before render |
|
|
487
|
+
| **unmountView** | Cleanup view | view | boolean | On destroy |
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## 🎓 Learning Points
|
|
492
|
+
|
|
493
|
+
1. **Cache optimization:** Layout reuse nếu path giống
|
|
494
|
+
2. **Lifecycle management:** unmounted → render → mounted
|
|
495
|
+
3. **Super view hierarchy:** Scan từ page up to root
|
|
496
|
+
4. **Async data handling:** @await vs @fetch directive
|
|
497
|
+
5. **Memory management:** Clear orphaned events, remove listeners
|
|
498
|
+
6. **CSR vs SSR:** Different rendering vs setup paths
|
|
499
|
+
7. **Event delegation:** Scan DOM and attach handlers
|
|
500
|
+
8. **Scroll position:** Restore position on back/forward
|
|
501
|
+
|
package/package.json
CHANGED
|
@@ -363,10 +363,6 @@ export class ViewController {
|
|
|
363
363
|
// Set basic properties (giữ nguyên tên từ code gốc)
|
|
364
364
|
this.id = config.viewId || uniqId();
|
|
365
365
|
deleteProp(config, 'viewId');
|
|
366
|
-
this.addCSS = config.addCSS || this._emptyFn;
|
|
367
|
-
deleteProp(config, 'addCSS');
|
|
368
|
-
this.removeCSS = config.removeCSS || this._emptyFn;
|
|
369
|
-
deleteProp(config, 'removeCSS');
|
|
370
366
|
this.superViewPath = config.superViewPath || config.superView;
|
|
371
367
|
deleteProp(config, 'superViewPath');
|
|
372
368
|
deleteProp(config, 'superView');
|
|
@@ -1354,6 +1350,7 @@ export class ViewController {
|
|
|
1354
1350
|
this.children.forEach(childCtrl => {
|
|
1355
1351
|
if (childCtrl && childCtrl instanceof ViewController) {
|
|
1356
1352
|
childCtrl.destroy();
|
|
1353
|
+
childCtrl._lifecycleManager.destroy();
|
|
1357
1354
|
}
|
|
1358
1355
|
});
|
|
1359
1356
|
this.children = [];
|
package/src/core/ViewManager.js
CHANGED
|
@@ -389,8 +389,8 @@ export class ViewManager {
|
|
|
389
389
|
const awaitData = null;
|
|
390
390
|
this.PAGE_VIEW = null;
|
|
391
391
|
try {
|
|
392
|
-
|
|
393
|
-
const viewStoreKey = name
|
|
392
|
+
|
|
393
|
+
const viewStoreKey = name + '__' + urlPath + '__ciew';
|
|
394
394
|
const cachedPageView = this.cachedPageViews.get(viewStoreKey);
|
|
395
395
|
if (cachedPageView && cachedPageView instanceof ViewEngine) {
|
|
396
396
|
// Sử dụng lại cached page view
|
|
@@ -398,7 +398,7 @@ export class ViewManager {
|
|
|
398
398
|
this.PAGE_VIEW = cachedPageView;
|
|
399
399
|
let ultraView = cachedPageView;
|
|
400
400
|
let superView = null;
|
|
401
|
-
if(cachedPageView.superView && cachedPageView.superView instanceof ViewEngine){
|
|
401
|
+
if (cachedPageView.superView && cachedPageView.superView instanceof ViewEngine) {
|
|
402
402
|
superView = cachedPageView.superView;
|
|
403
403
|
ultraView = superView;
|
|
404
404
|
this.CURRENT_SUPER_VIEW_MOUNTED = true;
|
|
@@ -407,7 +407,7 @@ export class ViewManager {
|
|
|
407
407
|
cachedPageView.__._templateManager.pushCachedSections();
|
|
408
408
|
html = cachedLayoutPath === this.CURRENT_SUPER_VIEW_PATH ? superView.__.renderedHtml : this.renderView(superView);
|
|
409
409
|
}
|
|
410
|
-
|
|
410
|
+
logger.debug(`🔍 App.View.loadView: Using cached view for '${name}' with URL path '${urlPath}'`);
|
|
411
411
|
return {
|
|
412
412
|
html: html,
|
|
413
413
|
isSuperView: superView ? true : false,
|
|
@@ -421,7 +421,7 @@ export class ViewManager {
|
|
|
421
421
|
|
|
422
422
|
let hasCache = false;
|
|
423
423
|
if (this.cachedTimes > 0) {
|
|
424
|
-
let cacheKey = name
|
|
424
|
+
let cacheKey = name + '__' + urlPath + '__ciew';
|
|
425
425
|
const cachedData = this.storageService.get(cacheKey);
|
|
426
426
|
if (cachedData) {
|
|
427
427
|
data = { ...data, ...cachedData };
|
|
@@ -447,16 +447,16 @@ export class ViewManager {
|
|
|
447
447
|
if (this.cachedTimes > 0) {
|
|
448
448
|
if (this.PAGE_VIEW instanceof ViewEngine) {
|
|
449
449
|
const oldCacheData = this.PAGE_VIEW.data;
|
|
450
|
-
let cacheKey = this.PAGE_VIEW.path
|
|
450
|
+
let cacheKey = this.PAGE_VIEW.path + '__' + this.PAGE_VIEW.urlPath + '__ciew';
|
|
451
451
|
this.storageService.set(cacheKey, oldCacheData, 3600); // cache trong 1 giờ
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
// Lưu view vào store để quản lý vòng đời (ttl mặc định 30 phút)
|
|
456
456
|
this.store.set(viewStoreKey, view, this.storeTTL); // lưu view vào store
|
|
457
|
-
this.store.onExpire(viewStoreKey, (
|
|
458
|
-
if (
|
|
459
|
-
|
|
457
|
+
this.store.onExpire(viewStoreKey, (viewStore) => {
|
|
458
|
+
if (viewStore && viewStore instanceof ViewEngine && viewStore != this.PAGE_VIEW) {
|
|
459
|
+
viewStore.__._lifecycleManager.destroy();
|
|
460
460
|
}
|
|
461
461
|
});
|
|
462
462
|
|
|
@@ -626,6 +626,13 @@ export class ViewManager {
|
|
|
626
626
|
if (route && route.$urlPath) {
|
|
627
627
|
view.__.urlPath = route.$urlPath;
|
|
628
628
|
}
|
|
629
|
+
const viewStoreKey = name.replace('.', '_') + '_' + (route?.$urlPath || '').replace(/[\/\:]/g, '_');
|
|
630
|
+
this.store.set(viewStoreKey, view, this.storeTTL); // lưu view vào store
|
|
631
|
+
this.store.onExpire(viewStoreKey, (viewStore) => {
|
|
632
|
+
if (viewStore && viewStore instanceof ViewEngine && viewStore != this.PAGE_VIEW) {
|
|
633
|
+
viewStore.__._lifecycleManager.destroy();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
629
636
|
|
|
630
637
|
|
|
631
638
|
// Store view in array for tracking
|
|
@@ -795,8 +802,7 @@ export class ViewManager {
|
|
|
795
802
|
oldSuperView.__._lifecycleManager.unmounted();
|
|
796
803
|
// oldSuperView.__._lifecycleManager.destroyOriginalView();
|
|
797
804
|
}
|
|
798
|
-
|
|
799
|
-
|
|
805
|
+
|
|
800
806
|
|
|
801
807
|
|
|
802
808
|
|
|
@@ -806,22 +812,20 @@ export class ViewManager {
|
|
|
806
812
|
console.error('View rendering error:', viewResult.error);
|
|
807
813
|
return;
|
|
808
814
|
}
|
|
815
|
+
const newPageView = this.PAGE_VIEW, newSuperView = this.CURRENT_SUPER_VIEW;
|
|
809
816
|
|
|
810
817
|
// ============================================================
|
|
811
818
|
// STEP 2: Check cache layout - nếu giống thì KHÔNG destroy
|
|
812
819
|
// ============================================================
|
|
813
|
-
const newSuperViewPath =
|
|
820
|
+
const newSuperViewPath = newSuperView?.__.path;
|
|
814
821
|
const isSameLayout = newSuperViewPath === oldSuperViewPath;
|
|
815
822
|
|
|
816
|
-
// Chỉ
|
|
817
|
-
if (!isSameLayout) {
|
|
818
|
-
|
|
819
|
-
// Call destroy() to remove CSS and scripts
|
|
820
|
-
oldSuperView.__._lifecycleManager.unmounted();
|
|
821
|
-
// oldSuperView.__._lifecycleManager.destroyOriginalView();
|
|
822
|
-
}
|
|
823
|
-
|
|
823
|
+
// Chỉ unmount layout cũ nếu layout KHÁC
|
|
824
|
+
if (!isSameLayout && oldSuperView && oldSuperView instanceof ViewEngine) {
|
|
825
|
+
oldSuperView.__.removeStyles();
|
|
824
826
|
}
|
|
827
|
+
oldPageView?.__.removeStyles();
|
|
828
|
+
|
|
825
829
|
|
|
826
830
|
// ============================================================
|
|
827
831
|
// STEP 3: Render và mount như bình thường
|
|
@@ -839,14 +843,21 @@ export class ViewManager {
|
|
|
839
843
|
this.emitChangedSections();
|
|
840
844
|
}
|
|
841
845
|
|
|
842
|
-
|
|
846
|
+
if (newPageView && newPageView instanceof ViewEngine) {
|
|
847
|
+
newPageView.__.insertStyles();
|
|
848
|
+
}
|
|
849
|
+
if (newSuperView && newSuperView instanceof ViewEngine) {
|
|
850
|
+
newSuperView.__.insertStyles();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
|
|
843
854
|
if (viewResult.ultraView && viewResult.ultraView instanceof ViewEngine) {
|
|
844
855
|
viewResult.ultraView.__._lifecycleManager.mounted();
|
|
845
856
|
}
|
|
846
857
|
|
|
847
858
|
this.CURRENT_SUPER_VIEW_MOUNTED = true; // set trang thái super view mounted = true
|
|
848
859
|
|
|
849
|
-
this.PAGE_VIEW? this.PAGE_VIEW.__.scrollToOldPosition() : this.scrollToTop();
|
|
860
|
+
this.PAGE_VIEW ? this.PAGE_VIEW.__.scrollToOldPosition() : this.scrollToTop();
|
|
850
861
|
|
|
851
862
|
} catch (error) {
|
|
852
863
|
console.error('Error rendering view:', error);
|
|
@@ -1351,7 +1362,7 @@ export class ViewManager {
|
|
|
1351
1362
|
}
|
|
1352
1363
|
result = view.__[renderMethod]();
|
|
1353
1364
|
}
|
|
1354
|
-
const apiDataKey = view.__.path
|
|
1365
|
+
const apiDataKey = view.__.path + '_' + view.__.urlPath + ':data';
|
|
1355
1366
|
|
|
1356
1367
|
// ====================================================================
|
|
1357
1368
|
// CASE 3A: Has @await - Load data by current URL
|
|
@@ -1642,35 +1653,10 @@ export class ViewManager {
|
|
|
1642
1653
|
}
|
|
1643
1654
|
|
|
1644
1655
|
try {
|
|
1645
|
-
logger.log(`🗑️ View.unmountView: Unmounting view ${view.id} (${view.path})`);
|
|
1646
|
-
|
|
1647
|
-
// Step 1: Call beforeUnmount lifecycle
|
|
1648
|
-
if (view.__ && typeof view.__._lifecycleManager.beforeUnmount === 'function') {
|
|
1649
|
-
view.__._lifecycleManager.beforeUnmount();
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
// Step 2: Remove event listeners
|
|
1653
|
-
if (typeof view.__._lifecycleManager.removeEvents === 'function') {
|
|
1654
|
-
view.__._lifecycleManager.removeEvents();
|
|
1655
|
-
}
|
|
1656
1656
|
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
this.viewMap.delete(view.id);
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
// Step 4: Call unmounted lifecycle
|
|
1663
|
-
if (view.__ && typeof view.__._lifecycleManager.unmounted === 'function') {
|
|
1664
|
-
view.__._lifecycleManager.unmounted();
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// Step 5: Call destroy if defined
|
|
1668
|
-
if (view.__ && typeof view.__._lifecycleManager.destroy === 'function') {
|
|
1669
|
-
view.__._lifecycleManager.destroy();
|
|
1670
|
-
}
|
|
1657
|
+
view.__._lifecycleManager.unmounted();
|
|
1658
|
+
view.__.removeStyles();
|
|
1671
1659
|
|
|
1672
|
-
logger.log(`✅ View.unmountView: View ${view.id} unmounted successfully`);
|
|
1673
|
-
return true;
|
|
1674
1660
|
|
|
1675
1661
|
} catch (error) {
|
|
1676
1662
|
logger.error(`❌ View.unmountView: Error unmounting view ${view.id}:`, error);
|
package/src/core/ViewState.js
CHANGED
|
@@ -216,9 +216,6 @@ export class StateManager {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
// Log để debug (có thể tắt trong production)
|
|
219
|
-
if (this.controller?.App?.env?.debug) {
|
|
220
|
-
logger.log(`[StateManager] Flushing ${changesToProcess.length} changes (flush #${this._flushCount}):`, changesToProcess);
|
|
221
|
-
}
|
|
222
219
|
|
|
223
220
|
// Reset cờ called của multi-key listeners
|
|
224
221
|
for (const listener of this.multiKeyListeners) {
|
|
@@ -256,7 +256,6 @@ export class LifecycleManager {
|
|
|
256
256
|
this.beforeUnmount();
|
|
257
257
|
this.unmounting();
|
|
258
258
|
|
|
259
|
-
// Remove scripts
|
|
260
259
|
ctrl.removeScripts();
|
|
261
260
|
|
|
262
261
|
// Stop event listeners
|
|
@@ -327,6 +326,7 @@ export class LifecycleManager {
|
|
|
327
326
|
|
|
328
327
|
// Remove styles (will use fallback if styles array is empty)
|
|
329
328
|
ctrl.removeStyles();
|
|
329
|
+
ctrl.removeScripts();
|
|
330
330
|
|
|
331
331
|
// Final cleanup: Remove all styles with this view path from DOM
|
|
332
332
|
// This ensures CSS is removed even if registry is out of sync
|
|
@@ -16,6 +16,12 @@ export class ResourceManager {
|
|
|
16
16
|
constructor(controller) {
|
|
17
17
|
this.controller = controller;
|
|
18
18
|
this.path = controller.path;
|
|
19
|
+
/**
|
|
20
|
+
* Flag to track if styles/scripts have been inserted
|
|
21
|
+
* @type {boolean}
|
|
22
|
+
*/
|
|
23
|
+
this.insertedStyles = false;
|
|
24
|
+
this.insertedScripts = false;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
updateController(newController) {
|
|
@@ -23,6 +29,47 @@ export class ResourceManager {
|
|
|
23
29
|
this.path = newController.path;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Check if this view's styles have been inserted into DOM
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
hasInsertedStyles() {
|
|
37
|
+
return this.insertedStyles && this.controller.insertedResourceKeys && this.controller.insertedResourceKeys.size > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if this view's scripts have been inserted into DOM
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
hasInsertedScripts() {
|
|
45
|
+
return this.insertedScripts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if specific resource key has been inserted
|
|
50
|
+
* @param {string} resourceKey
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
hasInsertedResource(resourceKey) {
|
|
54
|
+
return this.controller.insertedResourceKeys && this.controller.insertedResourceKeys.has(resourceKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the number of inserted styles for this view
|
|
59
|
+
* @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
getInsertedStylesCount() {
|
|
62
|
+
if (!this.controller.insertedResourceKeys) return 0;
|
|
63
|
+
let count = 0;
|
|
64
|
+
this.controller.insertedResourceKeys.forEach(key => {
|
|
65
|
+
const entry = this.controller.App.View.Engine.resourceRegistry.get(key);
|
|
66
|
+
if (entry && entry.resourceType === 'style') {
|
|
67
|
+
count++;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return count;
|
|
71
|
+
}
|
|
72
|
+
|
|
26
73
|
/**
|
|
27
74
|
* Insert all resources (styles + scripts)
|
|
28
75
|
*/
|
|
@@ -42,12 +89,25 @@ export class ResourceManager {
|
|
|
42
89
|
/**
|
|
43
90
|
* Insert styles into DOM with reference counting
|
|
44
91
|
* Extracted from ViewController.js line 428
|
|
92
|
+
* Will skip if already inserted to prevent duplicate execution
|
|
45
93
|
*/
|
|
46
94
|
insertStyles() {
|
|
95
|
+
this.controller.children?.forEach(childCtrl => {
|
|
96
|
+
childCtrl._resourceManager.insertStyles();
|
|
97
|
+
});
|
|
98
|
+
// Skip if already inserted
|
|
99
|
+
if (this.insertedStyles) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
47
103
|
if (!this.controller.styles || this.controller.styles.length === 0) {
|
|
104
|
+
this.insertedStyles = true;
|
|
48
105
|
return;
|
|
49
106
|
}
|
|
50
107
|
|
|
108
|
+
let insertedCount = 0;
|
|
109
|
+
let skippedCount = 0;
|
|
110
|
+
|
|
51
111
|
this.controller.styles.forEach(style => {
|
|
52
112
|
const resourceKey = this.controller.App.View.Engine.getResourceKey({
|
|
53
113
|
...style,
|
|
@@ -63,8 +123,11 @@ export class ResourceManager {
|
|
|
63
123
|
registryEntry.viewPaths.add(this.path);
|
|
64
124
|
registryEntry.referenceCount++;
|
|
65
125
|
this.controller.insertedResourceKeys.add(resourceKey);
|
|
126
|
+
skippedCount++;
|
|
66
127
|
return; // Don't insert again
|
|
67
128
|
}
|
|
129
|
+
|
|
130
|
+
insertedCount++;
|
|
68
131
|
|
|
69
132
|
// Create and insert new style element
|
|
70
133
|
let element;
|
|
@@ -131,18 +194,31 @@ export class ResourceManager {
|
|
|
131
194
|
});
|
|
132
195
|
|
|
133
196
|
this.controller.insertedResourceKeys.add(resourceKey);
|
|
197
|
+
logger.debug(` ✅ Style inserted (key: ${resourceKey}, type: ${style.type})`);
|
|
134
198
|
});
|
|
199
|
+
|
|
200
|
+
this.insertedStyles = true;
|
|
135
201
|
}
|
|
136
202
|
|
|
137
203
|
/**
|
|
138
204
|
* Insert scripts into DOM with reference counting
|
|
139
205
|
* Extracted from ViewController.js line 523
|
|
206
|
+
* Will skip if already inserted to prevent duplicate execution
|
|
140
207
|
*/
|
|
141
208
|
insertScripts() {
|
|
209
|
+
// Skip if already inserted
|
|
210
|
+
if (this.insertedScripts) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
142
214
|
if (!this.controller.scripts || this.controller.scripts.length === 0) {
|
|
215
|
+
this.insertedScripts = true;
|
|
143
216
|
return;
|
|
144
217
|
}
|
|
145
218
|
|
|
219
|
+
let insertedCount = 0;
|
|
220
|
+
let skippedCount = 0;
|
|
221
|
+
|
|
146
222
|
this.controller.scripts.forEach(script => {
|
|
147
223
|
const resourceKey = this.controller.App.View.Engine.getResourceKey({
|
|
148
224
|
...script,
|
|
@@ -158,6 +234,7 @@ export class ResourceManager {
|
|
|
158
234
|
registryEntry.viewPaths.add(this.path);
|
|
159
235
|
registryEntry.referenceCount++;
|
|
160
236
|
this.controller.insertedResourceKeys.add(resourceKey);
|
|
237
|
+
skippedCount++;
|
|
161
238
|
|
|
162
239
|
// If script already loaded, call onload callback (only for external scripts with element)
|
|
163
240
|
if (script.onload && registryEntry.element && registryEntry.element.readyState === 'complete') {
|
|
@@ -176,6 +253,8 @@ export class ResourceManager {
|
|
|
176
253
|
return; // Don't insert again
|
|
177
254
|
}
|
|
178
255
|
|
|
256
|
+
insertedCount++;
|
|
257
|
+
|
|
179
258
|
// Create and insert new script element
|
|
180
259
|
let element;
|
|
181
260
|
|
|
@@ -293,19 +372,33 @@ export class ResourceManager {
|
|
|
293
372
|
this.controller.insertedResourceKeys.add(resourceKey);
|
|
294
373
|
}
|
|
295
374
|
});
|
|
375
|
+
|
|
376
|
+
this.insertedScripts = true;
|
|
296
377
|
}
|
|
297
378
|
|
|
298
379
|
/**
|
|
299
380
|
* Remove styles from registry with reference counting
|
|
300
381
|
* Extracted from ViewController.js line 683
|
|
382
|
+
* Will skip if not inserted to prevent unnecessary operations
|
|
301
383
|
*/
|
|
302
384
|
removeStyles() {
|
|
385
|
+
this.controller.children?.forEach(childCtrl => {
|
|
386
|
+
childCtrl._resourceManager.removeStyles();
|
|
387
|
+
});
|
|
388
|
+
// Skip if not inserted
|
|
389
|
+
if (!this.insertedStyles) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
303
393
|
if (!this.controller.styles || this.controller.styles.length === 0) {
|
|
304
394
|
// Fallback: Remove all styles with this view path from DOM
|
|
305
395
|
this.removeStylesByViewPath();
|
|
396
|
+
this.insertedStyles = false;
|
|
306
397
|
return;
|
|
307
398
|
}
|
|
308
399
|
|
|
400
|
+
let removedCount = 0;
|
|
401
|
+
|
|
309
402
|
this.controller.styles.forEach(style => {
|
|
310
403
|
const resourceKey = this.controller.App.View.Engine.getResourceKey({
|
|
311
404
|
...style,
|
|
@@ -324,10 +417,13 @@ export class ResourceManager {
|
|
|
324
417
|
if (registryEntry.referenceCount <= 0) {
|
|
325
418
|
if (registryEntry.element && registryEntry.element.parentNode) {
|
|
326
419
|
registryEntry.element.remove();
|
|
420
|
+
removedCount++;
|
|
327
421
|
}
|
|
328
422
|
|
|
329
423
|
// Remove from registry
|
|
330
424
|
this.controller.App.View.Engine.resourceRegistry.delete(resourceKey);
|
|
425
|
+
} else {
|
|
426
|
+
logger.debug(` ⏭️ Style still in use by other views (key: ${resourceKey}, refCount: ${registryEntry.referenceCount})`);
|
|
331
427
|
}
|
|
332
428
|
|
|
333
429
|
this.controller.insertedResourceKeys.delete(resourceKey);
|
|
@@ -337,6 +433,7 @@ export class ResourceManager {
|
|
|
337
433
|
elements.forEach(element => {
|
|
338
434
|
if (element.parentNode) {
|
|
339
435
|
element.remove();
|
|
436
|
+
removedCount++;
|
|
340
437
|
}
|
|
341
438
|
});
|
|
342
439
|
}
|
|
@@ -344,6 +441,8 @@ export class ResourceManager {
|
|
|
344
441
|
|
|
345
442
|
// Final fallback: Remove any remaining styles with this view path
|
|
346
443
|
this.removeStylesByViewPath();
|
|
444
|
+
|
|
445
|
+
this.insertedStyles = false;
|
|
347
446
|
}
|
|
348
447
|
|
|
349
448
|
/**
|
|
@@ -402,12 +501,24 @@ export class ResourceManager {
|
|
|
402
501
|
/**
|
|
403
502
|
* Remove scripts from registry with reference counting
|
|
404
503
|
* Extracted from ViewController.js line 790
|
|
504
|
+
* Will skip if not inserted to prevent unnecessary operations
|
|
405
505
|
*/
|
|
406
506
|
removeScripts() {
|
|
507
|
+
// Skip if not inserted
|
|
508
|
+
this.controller.children?.forEach(childCtrl => {
|
|
509
|
+
childCtrl._resourceManager.removeScripts();
|
|
510
|
+
});
|
|
511
|
+
if (!this.insertedScripts) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
407
515
|
if (!this.controller.scripts || this.controller.scripts.length === 0) {
|
|
516
|
+
this.insertedScripts = false;
|
|
408
517
|
return;
|
|
409
518
|
}
|
|
410
519
|
|
|
520
|
+
let removedCount = 0;
|
|
521
|
+
|
|
411
522
|
this.controller.scripts.forEach(script => {
|
|
412
523
|
const resourceKey = this.controller.App.View.Engine.getResourceKey({
|
|
413
524
|
...script,
|
|
@@ -428,14 +539,19 @@ export class ResourceManager {
|
|
|
428
539
|
// Just remove from registry
|
|
429
540
|
if (registryEntry.element && registryEntry.element.parentNode) {
|
|
430
541
|
registryEntry.element.remove();
|
|
542
|
+
removedCount++;
|
|
431
543
|
}
|
|
432
544
|
|
|
433
545
|
// Remove from registry
|
|
434
546
|
this.controller.App.View.Engine.resourceRegistry.delete(resourceKey);
|
|
547
|
+
} else {
|
|
548
|
+
logger.debug(` ⏭️ Script still in use by other views (key: ${resourceKey}, refCount: ${registryEntry.referenceCount})`);
|
|
435
549
|
}
|
|
436
550
|
|
|
437
551
|
this.controller.insertedResourceKeys.delete(resourceKey);
|
|
438
552
|
}
|
|
439
553
|
});
|
|
554
|
+
|
|
555
|
+
this.insertedScripts = false;
|
|
440
556
|
}
|
|
441
557
|
}
|