jskelet 0.6.2 → 0.6.3
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.
- package/AGENTS.md +136 -136
- package/CHANGELOG.md +620 -596
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -309
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +661 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1443 -1423
- package/docs/07-yapilandirma.md +12 -6
- package/docs/08-build.md +429 -428
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +338 -338
- package/docs/12-panel-ve-oturum.md +478 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -328
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +669 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1453 -1431
- package/docs/en/07-configuration.md +1219 -1214
- package/docs/en/08-build.md +447 -446
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +340 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +488 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +17 -1
- package/src/config/index.js +13 -0
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +230 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -462
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -0
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1122
- package/src/server/image-optimizer.js +500 -407
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -66
- package/src/server/logs/pipeline.js +165 -158
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -100
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +356 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1058
- package/src/server/redis.js +588 -569
- package/src/server/render.js +4 -4
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +15 -1
- package/types/config/index.d.ts +8 -0
- package/types/server/cache-blob.d.ts +13 -0
- package/types/server/data-cache.d.ts +9 -0
- package/types/server/disk-cache.d.ts +36 -0
- package/types/server/html-cache.d.ts +26 -3
- package/types/server/logs/file-sink.d.ts +16 -5
- package/types/server/redis.d.ts +2 -1
package/docs/05-islands.md
CHANGED
|
@@ -1,486 +1,486 @@
|
|
|
1
|
-
# 05 — Island'lar
|
|
2
|
-
|
|
3
|
-
Bu belge etkileşimin nasıl eklendiğini anlatır: `data-island` sözleşmesi, props
|
|
4
|
-
geçişi, üç hidrasyon stratejisi ve IntersectionObserver mantığı,
|
|
5
|
-
`client/entries/*` yapısı ve sayfa başına ek entry yükleme, runtime API'si
|
|
6
|
-
(`register`, `registerAll`, `hydrate`, `observeDocument`, `start`), island'lar
|
|
7
|
-
arası durum paylaşımı için `createStore`, DOM yardımcıları, `startSafeImages` ve
|
|
8
|
-
ertelenmiş panel (fragment) deseni. Modelin *neden* böyle olduğu
|
|
9
|
-
[02-mimari.md](./02-mimari.md)'de, bundle'ın nasıl üretildiği
|
|
10
|
-
[08-build.md](./08-build.md)'de.
|
|
11
|
-
|
|
12
|
-
## Sözleşme
|
|
13
|
-
|
|
14
|
-
Sunucu HTML'i tamdır; island yalnızca davranış ekler. Üç parça var.
|
|
15
|
-
|
|
16
|
-
**1. Şablonda işaret.**
|
|
17
|
-
|
|
18
|
-
```ejs
|
|
19
|
-
<div data-island="counter" data-island-props='{"start":5}'></div>
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
**2. Island modülü — `mount` adlı named export.**
|
|
23
|
-
|
|
24
|
-
```js
|
|
25
|
-
// client/islands/counter.js
|
|
26
|
-
/**
|
|
27
|
-
* @param {HTMLElement} element
|
|
28
|
-
* @param {{ start?: number }} props
|
|
29
|
-
* @returns {void | (() => void)} temizlik fonksiyonu (opsiyonel)
|
|
30
|
-
*/
|
|
31
|
-
export function mount(element, props) {
|
|
32
|
-
let value = props.start ?? 0;
|
|
33
|
-
// …
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**3. Entry'de kayıt.**
|
|
38
|
-
|
|
39
|
-
```js
|
|
40
|
-
// client/entries/main.js
|
|
41
|
-
import { registerAll, start } from "jskelet/client";
|
|
42
|
-
|
|
43
|
-
registerAll({
|
|
44
|
-
counter: () => import("../islands/counter.js"),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
start();
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Loader'ın dinamik import olması modelin özü: modül yalnızca sayfada o island
|
|
51
|
-
gerçekten varsa **ve** bağlanma koşulu sağlandığında indirilir. Bu haritayı
|
|
52
|
-
büyütmek ilk yükü büyütmez.
|
|
53
|
-
|
|
54
|
-
## HTML attribute'ları
|
|
55
|
-
|
|
56
|
-
| Attribute | Anlamı |
|
|
57
|
-
| --- | --- |
|
|
58
|
-
| `data-island="ad"` | Bağlanacak island'ın kayıtlı adı. Zorunlu. |
|
|
59
|
-
| `data-island-props='{"…":…}'` | JSON props. Ayrıştırılamazsa konsola hata basılır ve `{}` geçilir. |
|
|
60
|
-
| `data-island-eager` | Görünürlükten bağımsız, hemen bağla. |
|
|
61
|
-
| `data-island-idle` | Görünür olsa bile `load` + boş zamana kadar bekle. |
|
|
62
|
-
| `data-island-ready="true"` | **Framework yazar.** `mount()` başarıyla döndükten sonra eklenir; CSS ve testler bunu okuyabilir. |
|
|
63
|
-
|
|
64
|
-
`data-island-props` içeriği HTML attribute'u olduğu için tek tırnakla sarmak en
|
|
65
|
-
kolay yoldur. Değerleri sunucuda üretiyorsanız `jsonScript()` ya da `attrs()`
|
|
66
|
-
kullanmak kaçış hatalarını önler:
|
|
67
|
-
|
|
68
|
-
```ejs
|
|
69
|
-
<div <%- attrs({ "data-island": "chart", "data-island-props": JSON.stringify({ symbol }) }) %>></div>
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Hidrasyon stratejileri
|
|
73
|
-
|
|
74
|
-
### Varsayılan: görünürlüğe bağlı
|
|
75
|
-
|
|
76
|
-
Her island bir `IntersectionObserver`'a verilir (`rootMargin: "200px 0px"`).
|
|
77
|
-
Ekranda olanlar zaten ilk gözlemde tetiklenir; ekran dışındakiler kaydırılana
|
|
78
|
-
kadar hiç indirilmez. Element bir kez görününce gözlemden çıkarılır.
|
|
79
|
-
|
|
80
|
-
Bağlama işi ayrıca boş zamana kaydırılır (`requestIdleCallback`,
|
|
81
|
-
`timeout: 500`; desteklenmiyorsa `setTimeout(fn, 0)`): aynı anda görünen çok
|
|
82
|
-
sayıda island tek bir uzun task'a dönüşürse TBT ve INP bozulur.
|
|
83
|
-
|
|
84
|
-
### `data-island-eager`
|
|
85
|
-
|
|
86
|
-
Görünürlük beklenmez, doğrudan bağlanır. Header davranışı, çerez bandı, tema
|
|
87
|
-
değiştirici gibi sayfa genelinde geçerli island'lar için.
|
|
88
|
-
|
|
89
|
-
```ejs
|
|
90
|
-
<header data-island="header" data-island-eager></header>
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### `data-island-idle`
|
|
94
|
-
|
|
95
|
-
Görünür olsa bile `load` olayı tamamlanıp ana iş parçacığı boşalana kadar
|
|
96
|
-
bekletilir. İlk ekranda görünen ama kritik olmayan ağır modüller — örneğin
|
|
97
|
-
grafik kütüphanesi çeken bir mini grafik — LCP ile yarışmasın diye.
|
|
98
|
-
|
|
99
|
-
```ejs
|
|
100
|
-
<div data-island="sparkline" data-island-idle></div>
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Sayfa yüklendiğinde `document.readyState` zaten `complete` ise bekleme atlanır
|
|
104
|
-
ve doğrudan boş zamana kaydırılır.
|
|
105
|
-
|
|
106
|
-
### Gizli elementler
|
|
107
|
-
|
|
108
|
-
`hidden` bir drawer ya da dialog'un düzen kutusu yoktur ve
|
|
109
|
-
`IntersectionObserver` onu **asla** bildirmez. Bu yüzden `hydrate()` ölçümleri
|
|
110
|
-
tek seferde okur (`getClientRects().length > 0`) ve kutusu olmayan elementleri
|
|
111
|
-
gözlemciye vermek yerine doğrudan bağlar. Ölçümlerin tek seferde okunması da
|
|
112
|
-
bilinçli: araya yazma girmediği için tek reflow olur.
|
|
113
|
-
|
|
114
|
-
Pratik sonucu: bir modal'ı `hidden` başlatabilirsiniz, island'ı yine bağlanır.
|
|
115
|
-
|
|
116
|
-
## `client/` dizini
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
client/
|
|
120
|
-
├── entries/
|
|
121
|
-
│ ├── main.js her sayfada yüklenen ortak bootstrap (veya main.ts)
|
|
122
|
-
│ └── chart.js yalnızca isteyen sayfalarda
|
|
123
|
-
└── islands/
|
|
124
|
-
├── counter.ts .js veya .ts
|
|
125
|
-
└── chart.js
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
`client/entries/*.{js,ts,mts}` içindeki **her dosya bir esbuild entry'sidir**.
|
|
129
|
-
`main.js` (veya `main.ts`) layout tarafından her sayfada yüklenir (manifest'te
|
|
130
|
-
varsa). Ek entry'ler yalnızca onları isteyen sayfalarda yüklenir. Aynı stem için
|
|
131
|
-
iki uzantı (`main.js` + `main.ts`) build hatasıdır.
|
|
132
|
-
|
|
133
|
-
```js
|
|
134
|
-
// controller — manifest anahtarı her zaman *.js kalır
|
|
135
|
-
return { view: "pages/markets", entries: ["chart.js"] };
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Layout `entries` dizisindeki her adı `asset(entry)` ile çözüp bir
|
|
139
|
-
`<script type="module">` basar. Ad manifest anahtarıdır (`chart.js`), kaynak
|
|
140
|
-
dosya `chart.ts` olsa bile hash'siz anahtar `.js` kalır.
|
|
141
|
-
|
|
142
|
-
Paylaşılan `@/lib` modülleri sunucuda da import ediliyorsa **`.js` kalsın** —
|
|
143
|
-
Node runtime `.ts` çözmez; `.ts` yalnızca esbuild client hattında derlenir.
|
|
144
|
-
|
|
145
|
-
Kod bölme (`splitting: true`) açık: iki entry'nin paylaştığı modüller ortak bir
|
|
146
|
-
chunk'a çıkar ve iki kez indirilmez.
|
|
147
|
-
|
|
148
|
-
`client/islands/` bir zorunluluk değil, yalnızca yaygın düzen; island modülleri
|
|
149
|
-
entry'den erişilebilen herhangi bir yerde olabilir. `@/` alias'ı hem sunucuda
|
|
150
|
-
hem bundle'da çalışır, böylece `lib/` altındaki paylaşılan modüller aynı import
|
|
151
|
-
stilini kullanabilir.
|
|
152
|
-
|
|
153
|
-
## Runtime API — `jskelet/client`
|
|
154
|
-
|
|
155
|
-
### `register(name, loader)`
|
|
156
|
-
|
|
157
|
-
Tek bir island kaydeder. `loader` `Promise<{ mount }>` döndüren bir fonksiyon
|
|
158
|
-
olmalıdır.
|
|
159
|
-
|
|
160
|
-
```js
|
|
161
|
-
import { register } from "jskelet/client";
|
|
162
|
-
|
|
163
|
-
register("counter", () => import("../islands/counter.js"));
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### `registerAll(entries)`
|
|
167
|
-
|
|
168
|
-
Nesne biçiminde toplu kayıt. Pratikte tercih edilen biçim.
|
|
169
|
-
|
|
170
|
-
```js
|
|
171
|
-
registerAll({
|
|
172
|
-
counter: () => import("../islands/counter.js"),
|
|
173
|
-
drawer: () => import("../islands/drawer.js"),
|
|
174
|
-
});
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### `hydrate(root?)`
|
|
178
|
-
|
|
179
|
-
`root` (varsayılan `document`) altındaki tüm `[data-island]` elementlerini tarar
|
|
180
|
-
ve bağlanma stratejisine göre işler. Zaten bağlanmış elementler atlanır.
|
|
181
|
-
|
|
182
|
-
Bir island'ı elle yeniden taramak gerektiğinde (ör. kendi kodunuzla DOM
|
|
183
|
-
eklediyseniz) doğrudan çağırabilirsiniz:
|
|
184
|
-
|
|
185
|
-
```js
|
|
186
|
-
container.innerHTML = html;
|
|
187
|
-
hydrate(container);
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### `observeDocument()`
|
|
191
|
-
|
|
192
|
-
`document.body` üzerine bir `MutationObserver` kurar ve sonradan DOM'a eklenen
|
|
193
|
-
island'ları da yakalar (infinite scroll, portal, fragment yükleme).
|
|
194
|
-
`MutationObserver` örneğini döndürür, böylece gerekirse `disconnect()`
|
|
195
|
-
edilebilir.
|
|
196
|
-
|
|
197
|
-
### `start()`
|
|
198
|
-
|
|
199
|
-
Tipik bootstrap: `DOMContentLoaded` beklenir (gerekiyorsa), sonra `hydrate()` ve
|
|
200
|
-
`observeDocument()` çağrılır.
|
|
201
|
-
|
|
202
|
-
```js
|
|
203
|
-
registerAll({ /* … */ });
|
|
204
|
-
start();
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Bağlanma davranışı ve hatalar
|
|
208
|
-
|
|
209
|
-
- Bir element aynı island adıyla **iki kez bağlanmaz**; kayıt element bazında
|
|
210
|
-
`WeakMap` içinde tutulur.
|
|
211
|
-
- Kayıtlı olmayan bir ad için konsola uyarı basılır:
|
|
212
|
-
`[island] not registered: <name>`.
|
|
213
|
-
- Modül import'u ya da `mount()` hata verirse konsola hata basılır
|
|
214
|
-
(`[island] <name> failed to load`) ve **sayfanın kalanı etkilenmez**.
|
|
215
|
-
- `mount()` başarıyla dönerse elemente `data-island-ready="true"` yazılır.
|
|
216
|
-
- `mount()` bir temizlik fonksiyonu döndürebilir; framework onu saklar ve
|
|
217
|
-
`unmount()` çağrıldığında işletir (aşağıya bakın).
|
|
218
|
-
|
|
219
|
-
### `unmount(root?)`
|
|
220
|
-
|
|
221
|
-
`root` altındaki island'ları söker: saklanan temizlik fonksiyonlarını çağırır,
|
|
222
|
-
`data-island-ready` işaretini kaldırır ve kaydı siler, böylece aynı düğüm
|
|
223
|
-
tekrar DOM'a girerse yeniden bağlanabilir. `root`'un kendisi de island olabilir.
|
|
224
|
-
|
|
225
|
-
DOM'un bir bölgesini değiştirirken çağrılması **zorunlu**:
|
|
226
|
-
|
|
227
|
-
```js
|
|
228
|
-
import { hydrate, unmount } from "jskelet/client";
|
|
229
|
-
|
|
230
|
-
unmount(container);
|
|
231
|
-
container.innerHTML = html;
|
|
232
|
-
hydrate(container);
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Atlanması en kolay gözden kaçan sızıntı biçimini üretiyor. `innerHTML` ile
|
|
236
|
-
değiştirilen bir bölgenin island'ları DOM'dan çıkar, ama `document`/`window`
|
|
237
|
-
üzerine kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder;
|
|
238
|
-
birkaç takastan sonra aynı iş onlarca kez çalışır.
|
|
239
|
-
|
|
240
|
-
```js
|
|
241
|
-
export function mount(element) {
|
|
242
|
-
const timer = setInterval(() => tick(element), 1000);
|
|
243
|
-
const onResize = () => layout(element);
|
|
244
|
-
window.addEventListener("resize", onResize);
|
|
245
|
-
|
|
246
|
-
return () => {
|
|
247
|
-
clearInterval(timer);
|
|
248
|
-
window.removeEventListener("resize", onResize);
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
`swap()` ve form yardımcıları `unmount()`u kendileri çağırıyor; elle DOM
|
|
254
|
-
değiştirdiğiniz yerlerde siz çağırıyorsunuz.
|
|
255
|
-
|
|
256
|
-
### `swap(target, url, options?)` ve `startSwapLinks(root?)`
|
|
257
|
-
|
|
258
|
-
Bir bölgeyi sunucudan gelen parçayla değiştirir: eski alt ağacı söker, içeriği
|
|
259
|
-
yazar, yeniden hidre eder ve odağı kaybolmuşsa geri getirir.
|
|
260
|
-
|
|
261
|
-
```html
|
|
262
|
-
<a href="/_fragment/satirlar?sayfa=2" data-swap="#satirlar">Sonraki</a>
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Sunucu tarafı ve tüm seçenekler
|
|
266
|
-
[12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
|
|
267
|
-
|
|
268
|
-
### `enhanceForm(form)` ve `startForms(root?)`
|
|
269
|
-
|
|
270
|
-
`data-enhance` taşıyan formları sayfa yenilemeden gönderir; JS kapalıyken
|
|
271
|
-
normal POST + yönlendirme akışı çalışmaya devam eder. Sözleşmenin tamamı
|
|
272
|
-
[12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
|
|
273
|
-
|
|
274
|
-
## Durum paylaşımı: `createStore`
|
|
275
|
-
|
|
276
|
-
React Context'in yerine kullanılan minimal pub/sub. `useSyncExternalStore`
|
|
277
|
-
köprüsünün yerini alır: doğrudan `subscribe`.
|
|
278
|
-
|
|
279
|
-
```js
|
|
280
|
-
// client/stores/theme.js
|
|
281
|
-
import { createStore } from "jskelet/client";
|
|
282
|
-
|
|
283
|
-
export const theme = createStore("light");
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
```js
|
|
287
|
-
// client/islands/theme-toggle.js
|
|
288
|
-
import { theme } from "../stores/theme.js";
|
|
289
|
-
|
|
290
|
-
export function mount(element) {
|
|
291
|
-
const paint = (value) => {
|
|
292
|
-
element.textContent = value === "light" ? "Koyu tema" : "Açık tema";
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const unsubscribe = theme.subscribe(paint);
|
|
296
|
-
paint(theme.get());
|
|
297
|
-
|
|
298
|
-
element.addEventListener("click", () => {
|
|
299
|
-
theme.set((prev) => (prev === "light" ? "dark" : "light"));
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
return unsubscribe;
|
|
303
|
-
}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
API:
|
|
307
|
-
|
|
308
|
-
| Üye | Davranış |
|
|
309
|
-
| --- | --- |
|
|
310
|
-
| `get()` | Anlık değer |
|
|
311
|
-
| `set(next)` | Değer ya da `(prev) => next` fonksiyonu. Değer **aynıysa** (`===`) dinleyiciler tetiklenmez. |
|
|
312
|
-
| `subscribe(listener)` | Dinleyici ekler, kaldıran fonksiyonu döndürür. Abone olurken mevcut değerle çağrılmaz — ilk boyamayı kendiniz yapın. |
|
|
313
|
-
|
|
314
|
-
## DOM yardımcıları
|
|
315
|
-
|
|
316
|
-
`jskelet/client` island'ların paylaştığı küçük bir yardımcı seti verir.
|
|
317
|
-
|
|
318
|
-
| Fonksiyon | İmza | Davranış |
|
|
319
|
-
| --- | --- | --- |
|
|
320
|
-
| `qs` | `(root, selector) => HTMLElement \| null` | `querySelector` |
|
|
321
|
-
| `qsa` | `(root, selector) => HTMLElement[]` | `querySelectorAll`, gerçek dizi olarak |
|
|
322
|
-
| `on` | `(target, type, handler, options?) => () => void` | Dinleyici ekler ve **kaldıran fonksiyonu döndürür** |
|
|
323
|
-
| `onClick` | `(root, selector, handler) => () => void` | Delege edilmiş click; `handler(event, target)` |
|
|
324
|
-
| `debounce` | `(ms, fn) => fn` | Son çağrıdan `ms` sonra çalışır |
|
|
325
|
-
| `raf` | `(fn) => fn` | Çağrıları tek bir `requestAnimationFrame`'de birleştirir |
|
|
326
|
-
| `toggleClass` | `(element, name, active) => void` | `classList.toggle` |
|
|
327
|
-
| `getOverlayRoot` | `() => HTMLElement` | `#jskelet-overlays` ya da `body` |
|
|
328
|
-
|
|
329
|
-
`on()` ve `onClick()`'in kaldırıcı döndürmesi, `mount()`'un temizlik
|
|
330
|
-
fonksiyonuyla doğal olarak eşleşir:
|
|
331
|
-
|
|
332
|
-
```js
|
|
333
|
-
import { on, onClick, raf } from "jskelet/client";
|
|
334
|
-
|
|
335
|
-
export function mount(element) {
|
|
336
|
-
const offClick = onClick(element, "[data-tab]", (event, target) => {
|
|
337
|
-
selectTab(target.dataset.tab);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
const offScroll = on(window, "scroll", raf(() => updateShadow(element)), {
|
|
341
|
-
passive: true,
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
return () => {
|
|
345
|
-
offClick();
|
|
346
|
-
offScroll();
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
`getOverlayRoot()` modal/drawer içeriğini taşımak için: layout'ta
|
|
352
|
-
`<div id="jskelet-overlays"></div>` varsa oraya, yoksa `body`ye. Portal,
|
|
353
|
-
`overflow` ya da `transform` taşıyan bir ata elementin `position: fixed`
|
|
354
|
-
overlay'i kırpmasını engeller.
|
|
355
|
-
|
|
356
|
-
## `startSafeImages()`
|
|
357
|
-
|
|
358
|
-
Yüklenemeyen görseller için tek bir belge dinleyicisi. **Bilinçli olarak island
|
|
359
|
-
değildir:** görsel ağırlıklı bir sayfada 80+ `<img>` olabiliyor ve her birine
|
|
360
|
-
ayrı island bağlamak (gözlemci + dinamik import + mount) sırf hata ihtimali için
|
|
361
|
-
ciddi bir hidrasyon yükü.
|
|
362
|
-
|
|
363
|
-
```js
|
|
364
|
-
// client/entries/main.js
|
|
365
|
-
import { registerAll, start, startSafeImages } from "jskelet/client";
|
|
366
|
-
|
|
367
|
-
registerAll({ /* … */ });
|
|
368
|
-
startSafeImages();
|
|
369
|
-
start();
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
Kullanım, şablon tarafında:
|
|
373
|
-
|
|
374
|
-
```ejs
|
|
375
|
-
<%# 1. Minimal: framework ölçüleri koruyan bir blokla değiştirir %>
|
|
376
|
-
<img src="/kapak.png" alt="Kapak" width="640" height="360" data-safe-image>
|
|
377
|
-
|
|
378
|
-
<%# 2. Kendi hata görünümü %>
|
|
379
|
-
<div data-safe-image-host>
|
|
380
|
-
<img src="/kapak.png" alt="Kapak" data-safe-image>
|
|
381
|
-
<template data-safe-image-fallback>
|
|
382
|
-
<div class="flex h-40 items-center justify-center bg-slate-100">Görsel yok</div>
|
|
383
|
-
</template>
|
|
384
|
-
</div>
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
Nasıl çalışır:
|
|
388
|
-
|
|
389
|
-
- Belgeye **yakalama fazında** tek bir `error` dinleyicisi kurulur. `error`
|
|
390
|
-
olayı kabarmaz ama yakalama fazında görülebilir; bu yüzden tek dinleyici tüm
|
|
391
|
-
görselleri karşılar ve sonradan DOM'a eklenenler de kendiliğinden kapsanır.
|
|
392
|
-
- `data-safe-image-host` sarmalayıcısı **ve** içinde
|
|
393
|
-
`<template data-safe-image-fallback>` varsa sarmalayıcının tamamı template
|
|
394
|
-
içeriğiyle değiştirilir. Framework hiçbir stil dayatmaz.
|
|
395
|
-
- Yoksa görselin yerine minimal bir blok konur: `role="img"`, `alt` (ya da
|
|
396
|
-
`data-fallback-label`) değeri `aria-label` olarak, görselin `className`i artı
|
|
397
|
-
`data-fallback-class`, ve `width`/`height` varsa aynı ölçüler inline style
|
|
398
|
-
olarak. Ölçülerin korunması değiştirme sırasında düzen kaymasını (CLS)
|
|
399
|
-
önler.
|
|
400
|
-
- JS çalışmadan önce başarısız olmuş görseller olay üretmez; bu yüzden bir kez
|
|
401
|
-
tarama yapılır (`requestIdleCallback`, `timeout: 2000`): `complete` olup
|
|
402
|
-
`naturalWidth === 0` olanlar değiştirilir.
|
|
403
|
-
|
|
404
|
-
## Ertelenmiş panel (fragment) deseni
|
|
405
|
-
|
|
406
|
-
Ağır ve ikincil bir bölümü (yorumlar, ilgili haberler, uzun bir tablo) ilk HTML
|
|
407
|
-
yanıtından tamamen çıkarmak istediğinizde island + layout'suz render birleşimi
|
|
408
|
-
kullanılır. Framework'te bunun için özel bir API yok; iki hazır parçanın
|
|
409
|
-
kombinasyonu:
|
|
410
|
-
|
|
411
|
-
**1. Sunucuda layout'suz bir fragment ucu** (`renderView`, bkz.
|
|
412
|
-
[03-routing.md](./03-routing.md)):
|
|
413
|
-
|
|
414
|
-
```js
|
|
415
|
-
// routes/80-fragments.mjs
|
|
416
|
-
export default function register(app, { renderView }) {
|
|
417
|
-
app.get("/_fragment/yorumlar/:id", async (req, res) => {
|
|
418
|
-
const comments = await getComments(req.params.id);
|
|
419
|
-
res.type("html").send(await renderView("fragments/comments", { comments }));
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
**2. Sayfada bir yer tutucu island.** Görünürlüğe bağlı bağlandığı için,
|
|
425
|
-
ziyaretçi o bölüme kaydırmazsa ne modül ne de fragment indirilir:
|
|
426
|
-
|
|
427
|
-
```ejs
|
|
428
|
-
<div data-island="deferred" data-island-props='{"src":"/_fragment/yorumlar/42"}'></div>
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
**3. Island fragment'ı çekip yerleştirir ve içindeki island'ları hidre eder:**
|
|
432
|
-
|
|
433
|
-
```js
|
|
434
|
-
// client/islands/deferred.js
|
|
435
|
-
import { hydrate } from "jskelet/client";
|
|
436
|
-
|
|
437
|
-
export async function mount(element, { src }) {
|
|
438
|
-
try {
|
|
439
|
-
const response = await fetch(src, { headers: { accept: "text/html" } });
|
|
440
|
-
if (!response.ok) return;
|
|
441
|
-
|
|
442
|
-
element.innerHTML = await response.text();
|
|
443
|
-
hydrate(element);
|
|
444
|
-
} catch {
|
|
445
|
-
// İkincil içerik: sessizce vazgeç, sayfanın kalanı etkilenmesin.
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
`observeDocument()` zaten çalışıyorsa son satırdaki `hydrate()` çağrısı
|
|
451
|
-
gereksizdir; yine de açıkça çağırmak, `start()` kullanmayan bir kurulumda da
|
|
452
|
-
doğru davranmasını sağlar.
|
|
453
|
-
|
|
454
|
-
Fragment yolları için `/_fragment/` öneki önerilir: varsayılan `prewarmSkip`
|
|
455
|
-
listesinde olduğu için ısıtma turu bu uçları taramaz
|
|
456
|
-
([06-cache.md](./06-cache.md)).
|
|
457
|
-
|
|
458
|
-
## Ortam değişkenleri ve `clientEnv`
|
|
459
|
-
|
|
460
|
-
Tarayıcıda `process` yoktur, ama sunucuyla paylaşılan modüller yine de
|
|
461
|
-
`process.env` okuyabilir. `jskelet.config.mjs` → `clientEnv` ile bildirilen
|
|
462
|
-
anahtarlar build zamanında bundle'a gömülür:
|
|
463
|
-
|
|
464
|
-
```js
|
|
465
|
-
export default {
|
|
466
|
-
clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN"],
|
|
467
|
-
};
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
Next'teki `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın herkese açık
|
|
471
|
-
olduğu isimden değil config'ten belli. `process.env`in tamamı tek nesne olarak
|
|
472
|
-
define edilir, yani listede olmayan bir anahtar okunduğunda çökme yerine
|
|
473
|
-
`undefined` döner. `NODE_ENV` her zaman gömülür.
|
|
474
|
-
|
|
475
|
-
## Tarayıcı desteği
|
|
476
|
-
|
|
477
|
-
Bundle hedefi sabittir: `chrome111`, `edge111`, `firefox111`, `safari16.4`. ESM
|
|
478
|
-
+ dinamik import + `IntersectionObserver` island modelinin zaten alt sınırı;
|
|
479
|
-
daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor.
|
|
480
|
-
JS hiç çalışmasa da sunucu HTML'i tam olduğu için sayfa okunur kalır.
|
|
481
|
-
|
|
482
|
-
## Sırada ne var
|
|
483
|
-
|
|
484
|
-
- Bundle, hash'ler ve `entries` manifest'i: [08-build.md](./08-build.md)
|
|
485
|
-
- `entries` alanının controller tarafı: [03-routing.md](./03-routing.md)
|
|
486
|
-
- Island durumunu dev panelinden izlemek: [09-dev-araclari.md](./09-dev-araclari.md)
|
|
1
|
+
# 05 — Island'lar
|
|
2
|
+
|
|
3
|
+
Bu belge etkileşimin nasıl eklendiğini anlatır: `data-island` sözleşmesi, props
|
|
4
|
+
geçişi, üç hidrasyon stratejisi ve IntersectionObserver mantığı,
|
|
5
|
+
`client/entries/*` yapısı ve sayfa başına ek entry yükleme, runtime API'si
|
|
6
|
+
(`register`, `registerAll`, `hydrate`, `observeDocument`, `start`), island'lar
|
|
7
|
+
arası durum paylaşımı için `createStore`, DOM yardımcıları, `startSafeImages` ve
|
|
8
|
+
ertelenmiş panel (fragment) deseni. Modelin *neden* böyle olduğu
|
|
9
|
+
[02-mimari.md](./02-mimari.md)'de, bundle'ın nasıl üretildiği
|
|
10
|
+
[08-build.md](./08-build.md)'de.
|
|
11
|
+
|
|
12
|
+
## Sözleşme
|
|
13
|
+
|
|
14
|
+
Sunucu HTML'i tamdır; island yalnızca davranış ekler. Üç parça var.
|
|
15
|
+
|
|
16
|
+
**1. Şablonda işaret.**
|
|
17
|
+
|
|
18
|
+
```ejs
|
|
19
|
+
<div data-island="counter" data-island-props='{"start":5}'></div>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**2. Island modülü — `mount` adlı named export.**
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// client/islands/counter.js
|
|
26
|
+
/**
|
|
27
|
+
* @param {HTMLElement} element
|
|
28
|
+
* @param {{ start?: number }} props
|
|
29
|
+
* @returns {void | (() => void)} temizlik fonksiyonu (opsiyonel)
|
|
30
|
+
*/
|
|
31
|
+
export function mount(element, props) {
|
|
32
|
+
let value = props.start ?? 0;
|
|
33
|
+
// …
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**3. Entry'de kayıt.**
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// client/entries/main.js
|
|
41
|
+
import { registerAll, start } from "jskelet/client";
|
|
42
|
+
|
|
43
|
+
registerAll({
|
|
44
|
+
counter: () => import("../islands/counter.js"),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
start();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Loader'ın dinamik import olması modelin özü: modül yalnızca sayfada o island
|
|
51
|
+
gerçekten varsa **ve** bağlanma koşulu sağlandığında indirilir. Bu haritayı
|
|
52
|
+
büyütmek ilk yükü büyütmez.
|
|
53
|
+
|
|
54
|
+
## HTML attribute'ları
|
|
55
|
+
|
|
56
|
+
| Attribute | Anlamı |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `data-island="ad"` | Bağlanacak island'ın kayıtlı adı. Zorunlu. |
|
|
59
|
+
| `data-island-props='{"…":…}'` | JSON props. Ayrıştırılamazsa konsola hata basılır ve `{}` geçilir. |
|
|
60
|
+
| `data-island-eager` | Görünürlükten bağımsız, hemen bağla. |
|
|
61
|
+
| `data-island-idle` | Görünür olsa bile `load` + boş zamana kadar bekle. |
|
|
62
|
+
| `data-island-ready="true"` | **Framework yazar.** `mount()` başarıyla döndükten sonra eklenir; CSS ve testler bunu okuyabilir. |
|
|
63
|
+
|
|
64
|
+
`data-island-props` içeriği HTML attribute'u olduğu için tek tırnakla sarmak en
|
|
65
|
+
kolay yoldur. Değerleri sunucuda üretiyorsanız `jsonScript()` ya da `attrs()`
|
|
66
|
+
kullanmak kaçış hatalarını önler:
|
|
67
|
+
|
|
68
|
+
```ejs
|
|
69
|
+
<div <%- attrs({ "data-island": "chart", "data-island-props": JSON.stringify({ symbol }) }) %>></div>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Hidrasyon stratejileri
|
|
73
|
+
|
|
74
|
+
### Varsayılan: görünürlüğe bağlı
|
|
75
|
+
|
|
76
|
+
Her island bir `IntersectionObserver`'a verilir (`rootMargin: "200px 0px"`).
|
|
77
|
+
Ekranda olanlar zaten ilk gözlemde tetiklenir; ekran dışındakiler kaydırılana
|
|
78
|
+
kadar hiç indirilmez. Element bir kez görününce gözlemden çıkarılır.
|
|
79
|
+
|
|
80
|
+
Bağlama işi ayrıca boş zamana kaydırılır (`requestIdleCallback`,
|
|
81
|
+
`timeout: 500`; desteklenmiyorsa `setTimeout(fn, 0)`): aynı anda görünen çok
|
|
82
|
+
sayıda island tek bir uzun task'a dönüşürse TBT ve INP bozulur.
|
|
83
|
+
|
|
84
|
+
### `data-island-eager`
|
|
85
|
+
|
|
86
|
+
Görünürlük beklenmez, doğrudan bağlanır. Header davranışı, çerez bandı, tema
|
|
87
|
+
değiştirici gibi sayfa genelinde geçerli island'lar için.
|
|
88
|
+
|
|
89
|
+
```ejs
|
|
90
|
+
<header data-island="header" data-island-eager></header>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `data-island-idle`
|
|
94
|
+
|
|
95
|
+
Görünür olsa bile `load` olayı tamamlanıp ana iş parçacığı boşalana kadar
|
|
96
|
+
bekletilir. İlk ekranda görünen ama kritik olmayan ağır modüller — örneğin
|
|
97
|
+
grafik kütüphanesi çeken bir mini grafik — LCP ile yarışmasın diye.
|
|
98
|
+
|
|
99
|
+
```ejs
|
|
100
|
+
<div data-island="sparkline" data-island-idle></div>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Sayfa yüklendiğinde `document.readyState` zaten `complete` ise bekleme atlanır
|
|
104
|
+
ve doğrudan boş zamana kaydırılır.
|
|
105
|
+
|
|
106
|
+
### Gizli elementler
|
|
107
|
+
|
|
108
|
+
`hidden` bir drawer ya da dialog'un düzen kutusu yoktur ve
|
|
109
|
+
`IntersectionObserver` onu **asla** bildirmez. Bu yüzden `hydrate()` ölçümleri
|
|
110
|
+
tek seferde okur (`getClientRects().length > 0`) ve kutusu olmayan elementleri
|
|
111
|
+
gözlemciye vermek yerine doğrudan bağlar. Ölçümlerin tek seferde okunması da
|
|
112
|
+
bilinçli: araya yazma girmediği için tek reflow olur.
|
|
113
|
+
|
|
114
|
+
Pratik sonucu: bir modal'ı `hidden` başlatabilirsiniz, island'ı yine bağlanır.
|
|
115
|
+
|
|
116
|
+
## `client/` dizini
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
client/
|
|
120
|
+
├── entries/
|
|
121
|
+
│ ├── main.js her sayfada yüklenen ortak bootstrap (veya main.ts)
|
|
122
|
+
│ └── chart.js yalnızca isteyen sayfalarda
|
|
123
|
+
└── islands/
|
|
124
|
+
├── counter.ts .js veya .ts
|
|
125
|
+
└── chart.js
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`client/entries/*.{js,ts,mts}` içindeki **her dosya bir esbuild entry'sidir**.
|
|
129
|
+
`main.js` (veya `main.ts`) layout tarafından her sayfada yüklenir (manifest'te
|
|
130
|
+
varsa). Ek entry'ler yalnızca onları isteyen sayfalarda yüklenir. Aynı stem için
|
|
131
|
+
iki uzantı (`main.js` + `main.ts`) build hatasıdır.
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
// controller — manifest anahtarı her zaman *.js kalır
|
|
135
|
+
return { view: "pages/markets", entries: ["chart.js"] };
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Layout `entries` dizisindeki her adı `asset(entry)` ile çözüp bir
|
|
139
|
+
`<script type="module">` basar. Ad manifest anahtarıdır (`chart.js`), kaynak
|
|
140
|
+
dosya `chart.ts` olsa bile hash'siz anahtar `.js` kalır.
|
|
141
|
+
|
|
142
|
+
Paylaşılan `@/lib` modülleri sunucuda da import ediliyorsa **`.js` kalsın** —
|
|
143
|
+
Node runtime `.ts` çözmez; `.ts` yalnızca esbuild client hattında derlenir.
|
|
144
|
+
|
|
145
|
+
Kod bölme (`splitting: true`) açık: iki entry'nin paylaştığı modüller ortak bir
|
|
146
|
+
chunk'a çıkar ve iki kez indirilmez.
|
|
147
|
+
|
|
148
|
+
`client/islands/` bir zorunluluk değil, yalnızca yaygın düzen; island modülleri
|
|
149
|
+
entry'den erişilebilen herhangi bir yerde olabilir. `@/` alias'ı hem sunucuda
|
|
150
|
+
hem bundle'da çalışır, böylece `lib/` altındaki paylaşılan modüller aynı import
|
|
151
|
+
stilini kullanabilir.
|
|
152
|
+
|
|
153
|
+
## Runtime API — `jskelet/client`
|
|
154
|
+
|
|
155
|
+
### `register(name, loader)`
|
|
156
|
+
|
|
157
|
+
Tek bir island kaydeder. `loader` `Promise<{ mount }>` döndüren bir fonksiyon
|
|
158
|
+
olmalıdır.
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
import { register } from "jskelet/client";
|
|
162
|
+
|
|
163
|
+
register("counter", () => import("../islands/counter.js"));
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `registerAll(entries)`
|
|
167
|
+
|
|
168
|
+
Nesne biçiminde toplu kayıt. Pratikte tercih edilen biçim.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
registerAll({
|
|
172
|
+
counter: () => import("../islands/counter.js"),
|
|
173
|
+
drawer: () => import("../islands/drawer.js"),
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `hydrate(root?)`
|
|
178
|
+
|
|
179
|
+
`root` (varsayılan `document`) altındaki tüm `[data-island]` elementlerini tarar
|
|
180
|
+
ve bağlanma stratejisine göre işler. Zaten bağlanmış elementler atlanır.
|
|
181
|
+
|
|
182
|
+
Bir island'ı elle yeniden taramak gerektiğinde (ör. kendi kodunuzla DOM
|
|
183
|
+
eklediyseniz) doğrudan çağırabilirsiniz:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
container.innerHTML = html;
|
|
187
|
+
hydrate(container);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `observeDocument()`
|
|
191
|
+
|
|
192
|
+
`document.body` üzerine bir `MutationObserver` kurar ve sonradan DOM'a eklenen
|
|
193
|
+
island'ları da yakalar (infinite scroll, portal, fragment yükleme).
|
|
194
|
+
`MutationObserver` örneğini döndürür, böylece gerekirse `disconnect()`
|
|
195
|
+
edilebilir.
|
|
196
|
+
|
|
197
|
+
### `start()`
|
|
198
|
+
|
|
199
|
+
Tipik bootstrap: `DOMContentLoaded` beklenir (gerekiyorsa), sonra `hydrate()` ve
|
|
200
|
+
`observeDocument()` çağrılır.
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
registerAll({ /* … */ });
|
|
204
|
+
start();
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Bağlanma davranışı ve hatalar
|
|
208
|
+
|
|
209
|
+
- Bir element aynı island adıyla **iki kez bağlanmaz**; kayıt element bazında
|
|
210
|
+
`WeakMap` içinde tutulur.
|
|
211
|
+
- Kayıtlı olmayan bir ad için konsola uyarı basılır:
|
|
212
|
+
`[island] not registered: <name>`.
|
|
213
|
+
- Modül import'u ya da `mount()` hata verirse konsola hata basılır
|
|
214
|
+
(`[island] <name> failed to load`) ve **sayfanın kalanı etkilenmez**.
|
|
215
|
+
- `mount()` başarıyla dönerse elemente `data-island-ready="true"` yazılır.
|
|
216
|
+
- `mount()` bir temizlik fonksiyonu döndürebilir; framework onu saklar ve
|
|
217
|
+
`unmount()` çağrıldığında işletir (aşağıya bakın).
|
|
218
|
+
|
|
219
|
+
### `unmount(root?)`
|
|
220
|
+
|
|
221
|
+
`root` altındaki island'ları söker: saklanan temizlik fonksiyonlarını çağırır,
|
|
222
|
+
`data-island-ready` işaretini kaldırır ve kaydı siler, böylece aynı düğüm
|
|
223
|
+
tekrar DOM'a girerse yeniden bağlanabilir. `root`'un kendisi de island olabilir.
|
|
224
|
+
|
|
225
|
+
DOM'un bir bölgesini değiştirirken çağrılması **zorunlu**:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
import { hydrate, unmount } from "jskelet/client";
|
|
229
|
+
|
|
230
|
+
unmount(container);
|
|
231
|
+
container.innerHTML = html;
|
|
232
|
+
hydrate(container);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Atlanması en kolay gözden kaçan sızıntı biçimini üretiyor. `innerHTML` ile
|
|
236
|
+
değiştirilen bir bölgenin island'ları DOM'dan çıkar, ama `document`/`window`
|
|
237
|
+
üzerine kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder;
|
|
238
|
+
birkaç takastan sonra aynı iş onlarca kez çalışır.
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
export function mount(element) {
|
|
242
|
+
const timer = setInterval(() => tick(element), 1000);
|
|
243
|
+
const onResize = () => layout(element);
|
|
244
|
+
window.addEventListener("resize", onResize);
|
|
245
|
+
|
|
246
|
+
return () => {
|
|
247
|
+
clearInterval(timer);
|
|
248
|
+
window.removeEventListener("resize", onResize);
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`swap()` ve form yardımcıları `unmount()`u kendileri çağırıyor; elle DOM
|
|
254
|
+
değiştirdiğiniz yerlerde siz çağırıyorsunuz.
|
|
255
|
+
|
|
256
|
+
### `swap(target, url, options?)` ve `startSwapLinks(root?)`
|
|
257
|
+
|
|
258
|
+
Bir bölgeyi sunucudan gelen parçayla değiştirir: eski alt ağacı söker, içeriği
|
|
259
|
+
yazar, yeniden hidre eder ve odağı kaybolmuşsa geri getirir.
|
|
260
|
+
|
|
261
|
+
```html
|
|
262
|
+
<a href="/_fragment/satirlar?sayfa=2" data-swap="#satirlar">Sonraki</a>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Sunucu tarafı ve tüm seçenekler
|
|
266
|
+
[12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
|
|
267
|
+
|
|
268
|
+
### `enhanceForm(form)` ve `startForms(root?)`
|
|
269
|
+
|
|
270
|
+
`data-enhance` taşıyan formları sayfa yenilemeden gönderir; JS kapalıyken
|
|
271
|
+
normal POST + yönlendirme akışı çalışmaya devam eder. Sözleşmenin tamamı
|
|
272
|
+
[12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
|
|
273
|
+
|
|
274
|
+
## Durum paylaşımı: `createStore`
|
|
275
|
+
|
|
276
|
+
React Context'in yerine kullanılan minimal pub/sub. `useSyncExternalStore`
|
|
277
|
+
köprüsünün yerini alır: doğrudan `subscribe`.
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
// client/stores/theme.js
|
|
281
|
+
import { createStore } from "jskelet/client";
|
|
282
|
+
|
|
283
|
+
export const theme = createStore("light");
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
// client/islands/theme-toggle.js
|
|
288
|
+
import { theme } from "../stores/theme.js";
|
|
289
|
+
|
|
290
|
+
export function mount(element) {
|
|
291
|
+
const paint = (value) => {
|
|
292
|
+
element.textContent = value === "light" ? "Koyu tema" : "Açık tema";
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const unsubscribe = theme.subscribe(paint);
|
|
296
|
+
paint(theme.get());
|
|
297
|
+
|
|
298
|
+
element.addEventListener("click", () => {
|
|
299
|
+
theme.set((prev) => (prev === "light" ? "dark" : "light"));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return unsubscribe;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
API:
|
|
307
|
+
|
|
308
|
+
| Üye | Davranış |
|
|
309
|
+
| --- | --- |
|
|
310
|
+
| `get()` | Anlık değer |
|
|
311
|
+
| `set(next)` | Değer ya da `(prev) => next` fonksiyonu. Değer **aynıysa** (`===`) dinleyiciler tetiklenmez. |
|
|
312
|
+
| `subscribe(listener)` | Dinleyici ekler, kaldıran fonksiyonu döndürür. Abone olurken mevcut değerle çağrılmaz — ilk boyamayı kendiniz yapın. |
|
|
313
|
+
|
|
314
|
+
## DOM yardımcıları
|
|
315
|
+
|
|
316
|
+
`jskelet/client` island'ların paylaştığı küçük bir yardımcı seti verir.
|
|
317
|
+
|
|
318
|
+
| Fonksiyon | İmza | Davranış |
|
|
319
|
+
| --- | --- | --- |
|
|
320
|
+
| `qs` | `(root, selector) => HTMLElement \| null` | `querySelector` |
|
|
321
|
+
| `qsa` | `(root, selector) => HTMLElement[]` | `querySelectorAll`, gerçek dizi olarak |
|
|
322
|
+
| `on` | `(target, type, handler, options?) => () => void` | Dinleyici ekler ve **kaldıran fonksiyonu döndürür** |
|
|
323
|
+
| `onClick` | `(root, selector, handler) => () => void` | Delege edilmiş click; `handler(event, target)` |
|
|
324
|
+
| `debounce` | `(ms, fn) => fn` | Son çağrıdan `ms` sonra çalışır |
|
|
325
|
+
| `raf` | `(fn) => fn` | Çağrıları tek bir `requestAnimationFrame`'de birleştirir |
|
|
326
|
+
| `toggleClass` | `(element, name, active) => void` | `classList.toggle` |
|
|
327
|
+
| `getOverlayRoot` | `() => HTMLElement` | `#jskelet-overlays` ya da `body` |
|
|
328
|
+
|
|
329
|
+
`on()` ve `onClick()`'in kaldırıcı döndürmesi, `mount()`'un temizlik
|
|
330
|
+
fonksiyonuyla doğal olarak eşleşir:
|
|
331
|
+
|
|
332
|
+
```js
|
|
333
|
+
import { on, onClick, raf } from "jskelet/client";
|
|
334
|
+
|
|
335
|
+
export function mount(element) {
|
|
336
|
+
const offClick = onClick(element, "[data-tab]", (event, target) => {
|
|
337
|
+
selectTab(target.dataset.tab);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const offScroll = on(window, "scroll", raf(() => updateShadow(element)), {
|
|
341
|
+
passive: true,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return () => {
|
|
345
|
+
offClick();
|
|
346
|
+
offScroll();
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`getOverlayRoot()` modal/drawer içeriğini taşımak için: layout'ta
|
|
352
|
+
`<div id="jskelet-overlays"></div>` varsa oraya, yoksa `body`ye. Portal,
|
|
353
|
+
`overflow` ya da `transform` taşıyan bir ata elementin `position: fixed`
|
|
354
|
+
overlay'i kırpmasını engeller.
|
|
355
|
+
|
|
356
|
+
## `startSafeImages()`
|
|
357
|
+
|
|
358
|
+
Yüklenemeyen görseller için tek bir belge dinleyicisi. **Bilinçli olarak island
|
|
359
|
+
değildir:** görsel ağırlıklı bir sayfada 80+ `<img>` olabiliyor ve her birine
|
|
360
|
+
ayrı island bağlamak (gözlemci + dinamik import + mount) sırf hata ihtimali için
|
|
361
|
+
ciddi bir hidrasyon yükü.
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
// client/entries/main.js
|
|
365
|
+
import { registerAll, start, startSafeImages } from "jskelet/client";
|
|
366
|
+
|
|
367
|
+
registerAll({ /* … */ });
|
|
368
|
+
startSafeImages();
|
|
369
|
+
start();
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Kullanım, şablon tarafında:
|
|
373
|
+
|
|
374
|
+
```ejs
|
|
375
|
+
<%# 1. Minimal: framework ölçüleri koruyan bir blokla değiştirir %>
|
|
376
|
+
<img src="/kapak.png" alt="Kapak" width="640" height="360" data-safe-image>
|
|
377
|
+
|
|
378
|
+
<%# 2. Kendi hata görünümü %>
|
|
379
|
+
<div data-safe-image-host>
|
|
380
|
+
<img src="/kapak.png" alt="Kapak" data-safe-image>
|
|
381
|
+
<template data-safe-image-fallback>
|
|
382
|
+
<div class="flex h-40 items-center justify-center bg-slate-100">Görsel yok</div>
|
|
383
|
+
</template>
|
|
384
|
+
</div>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Nasıl çalışır:
|
|
388
|
+
|
|
389
|
+
- Belgeye **yakalama fazında** tek bir `error` dinleyicisi kurulur. `error`
|
|
390
|
+
olayı kabarmaz ama yakalama fazında görülebilir; bu yüzden tek dinleyici tüm
|
|
391
|
+
görselleri karşılar ve sonradan DOM'a eklenenler de kendiliğinden kapsanır.
|
|
392
|
+
- `data-safe-image-host` sarmalayıcısı **ve** içinde
|
|
393
|
+
`<template data-safe-image-fallback>` varsa sarmalayıcının tamamı template
|
|
394
|
+
içeriğiyle değiştirilir. Framework hiçbir stil dayatmaz.
|
|
395
|
+
- Yoksa görselin yerine minimal bir blok konur: `role="img"`, `alt` (ya da
|
|
396
|
+
`data-fallback-label`) değeri `aria-label` olarak, görselin `className`i artı
|
|
397
|
+
`data-fallback-class`, ve `width`/`height` varsa aynı ölçüler inline style
|
|
398
|
+
olarak. Ölçülerin korunması değiştirme sırasında düzen kaymasını (CLS)
|
|
399
|
+
önler.
|
|
400
|
+
- JS çalışmadan önce başarısız olmuş görseller olay üretmez; bu yüzden bir kez
|
|
401
|
+
tarama yapılır (`requestIdleCallback`, `timeout: 2000`): `complete` olup
|
|
402
|
+
`naturalWidth === 0` olanlar değiştirilir.
|
|
403
|
+
|
|
404
|
+
## Ertelenmiş panel (fragment) deseni
|
|
405
|
+
|
|
406
|
+
Ağır ve ikincil bir bölümü (yorumlar, ilgili haberler, uzun bir tablo) ilk HTML
|
|
407
|
+
yanıtından tamamen çıkarmak istediğinizde island + layout'suz render birleşimi
|
|
408
|
+
kullanılır. Framework'te bunun için özel bir API yok; iki hazır parçanın
|
|
409
|
+
kombinasyonu:
|
|
410
|
+
|
|
411
|
+
**1. Sunucuda layout'suz bir fragment ucu** (`renderView`, bkz.
|
|
412
|
+
[03-routing.md](./03-routing.md)):
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
// routes/80-fragments.mjs
|
|
416
|
+
export default function register(app, { renderView }) {
|
|
417
|
+
app.get("/_fragment/yorumlar/:id", async (req, res) => {
|
|
418
|
+
const comments = await getComments(req.params.id);
|
|
419
|
+
res.type("html").send(await renderView("fragments/comments", { comments }));
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**2. Sayfada bir yer tutucu island.** Görünürlüğe bağlı bağlandığı için,
|
|
425
|
+
ziyaretçi o bölüme kaydırmazsa ne modül ne de fragment indirilir:
|
|
426
|
+
|
|
427
|
+
```ejs
|
|
428
|
+
<div data-island="deferred" data-island-props='{"src":"/_fragment/yorumlar/42"}'></div>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**3. Island fragment'ı çekip yerleştirir ve içindeki island'ları hidre eder:**
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
// client/islands/deferred.js
|
|
435
|
+
import { hydrate } from "jskelet/client";
|
|
436
|
+
|
|
437
|
+
export async function mount(element, { src }) {
|
|
438
|
+
try {
|
|
439
|
+
const response = await fetch(src, { headers: { accept: "text/html" } });
|
|
440
|
+
if (!response.ok) return;
|
|
441
|
+
|
|
442
|
+
element.innerHTML = await response.text();
|
|
443
|
+
hydrate(element);
|
|
444
|
+
} catch {
|
|
445
|
+
// İkincil içerik: sessizce vazgeç, sayfanın kalanı etkilenmesin.
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`observeDocument()` zaten çalışıyorsa son satırdaki `hydrate()` çağrısı
|
|
451
|
+
gereksizdir; yine de açıkça çağırmak, `start()` kullanmayan bir kurulumda da
|
|
452
|
+
doğru davranmasını sağlar.
|
|
453
|
+
|
|
454
|
+
Fragment yolları için `/_fragment/` öneki önerilir: varsayılan `prewarmSkip`
|
|
455
|
+
listesinde olduğu için ısıtma turu bu uçları taramaz
|
|
456
|
+
([06-cache.md](./06-cache.md)).
|
|
457
|
+
|
|
458
|
+
## Ortam değişkenleri ve `clientEnv`
|
|
459
|
+
|
|
460
|
+
Tarayıcıda `process` yoktur, ama sunucuyla paylaşılan modüller yine de
|
|
461
|
+
`process.env` okuyabilir. `jskelet.config.mjs` → `clientEnv` ile bildirilen
|
|
462
|
+
anahtarlar build zamanında bundle'a gömülür:
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
export default {
|
|
466
|
+
clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN"],
|
|
467
|
+
};
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Next'teki `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın herkese açık
|
|
471
|
+
olduğu isimden değil config'ten belli. `process.env`in tamamı tek nesne olarak
|
|
472
|
+
define edilir, yani listede olmayan bir anahtar okunduğunda çökme yerine
|
|
473
|
+
`undefined` döner. `NODE_ENV` her zaman gömülür.
|
|
474
|
+
|
|
475
|
+
## Tarayıcı desteği
|
|
476
|
+
|
|
477
|
+
Bundle hedefi sabittir: `chrome111`, `edge111`, `firefox111`, `safari16.4`. ESM
|
|
478
|
+
+ dinamik import + `IntersectionObserver` island modelinin zaten alt sınırı;
|
|
479
|
+
daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor.
|
|
480
|
+
JS hiç çalışmasa da sunucu HTML'i tam olduğu için sayfa okunur kalır.
|
|
481
|
+
|
|
482
|
+
## Sırada ne var
|
|
483
|
+
|
|
484
|
+
- Bundle, hash'ler ve `entries` manifest'i: [08-build.md](./08-build.md)
|
|
485
|
+
- `entries` alanının controller tarafı: [03-routing.md](./03-routing.md)
|
|
486
|
+
- Island durumunu dev panelinden izlemek: [09-dev-araclari.md](./09-dev-araclari.md)
|