İçindekiler
GoUI üzerine inşa ederken en sık ortaya çıkan başarısızlık modları için kontrol-listesi tarzı bir referans. Her bölüm, bir olay sırasında gözden geçirilebilir olacak şekilde tasarlanmıştır — gördüğünüz belirtiye atlayın.
Bu belge boyunca kullanılan modül yolu: github.com/zatrano/goui.
1. WebSocket bağlanmıyor
Belirtiler: tarayıcı hiçbir zaman bir "session" frame'i almıyor,
onConnect hiç ateşlenmiyor, onError hemen ateşleniyor, veya soket
açıldıktan hemen sonra kapanıyor.
Bunları sırayla inceleyin:
-
/goui/wsgerçekten erişilebilir mi? Adapter'ınızGET /goui/ws(ws.Path) yolunu bağlar ve WebSocket yükseltmesi olmayan istekleri reddeder (Fiberfiber.ErrUpgradeRequireddöndürür; stdlib/Gin/Echo kendi yükseltme kontrollerini kullanır — bkz.adapters/). URL'yi düz bir tarayıcı navigasyonu/fetchile vurmak doğru bir şekilde başarısız olur — bu bir hata değildir. İstemcinin gerçekten birws:///wss://URL'si inşa ettiğini,http:///https://değil, onaylayın. - URL kendi-kaynaklı/relatif mi, yoksa sayfanın sunulma şekliyle
eşleşmeyen bir şema/host'u yanlışlıkla sabit kodladınız mı?
GoUIClient._buildUrl, yapılandırılmışwsUrl,wsile başlamadığında sadecews:/wss:'i otomatik olarak türetir — yanlışlıkla tam nitelikli birhttp://URL'si geçirirseniz, sizin için düzeltilmez. - Bir ters proxy'nin arkasında mı? Nginx (ve çoğu proxy),
açıkça
proxy_http_version 1.1;,proxy_set_header Upgrade $http_upgrade;, veproxy_set_header Connection $connection_upgrade;'i yapılandırmadığınız süreceUpgrade/Connection: Upgradebaşlıklarını yönlendirmez. Bunu kaçırmak, "yerelde çalışıyor ama üretimde çalışmıyor"un en yaygın dağıtım zamanı nedenidir. Tam, çalışan bir Nginx yapılandırması için 13-project-integration.md §6.3'e bakın. -
?session=olmayan taze bir bağlantıda?component=atlandı mı? Bilinen birsessionne de bircomponentsorgu parametresi sağlanmadıysa, sunucu bir hata frame'i (ws.ErrComponentRequired,"component query parameter is required") ile yanıt verir ve bağlantıyı kapatır.GoUIClient'in boş olmayan bircomponentNameile inşa edildiğini onaylayın. - Bileşen ismi gerçekten kayıtlı mı?
registry.Create, bilinmeyen bir isim içincore.ErrComponentNotRegistereddöndürür, ki bunu sunucu bir hata frame'ine çevirir ve ardından bir"session"frame'i hiç göndermeden bağlantıyı kapatır.registry.Register(...)'a geçirilen tam string'in istemcinin istediğiyle eşleştiğini iki kez kontrol edin — bu, 13-project-integration.md §2'de açıklanan kiracı-nitelikli isimlerle özellikle yanlış anlaşılması kolaydır. - Tarayıcının Network/WS incelemecisinde gerçek kapanış kodunu ve
yükseltmeden önceki herhangi bir HTTP yanıtını kontrol edin.
Hiç gelmeyen bir
101 Switching Protocols(pending'de takılı kalmış, veya4xx/5xxyerine) proxy/routing katmanına işaret eder; açılıp sonra hemen kapanan bir soket, yukarıdaki hata-frame'i yoluna işaret eder — kapanmadan önce, herhangi biri geldiyse,"error"frame'inin payload'ını okuyun. - Sunucu tarafı günlükler.
Session.readLoop, prefetch başarısızlıklarını günlükler (log.Printf("[goui] prefetch %q failed: %v", ...)) ama çoğu bağlantı seviyesi başarısızlık, sadece istemciye gönderilen hata frame'i olarak görünür — varsayılan olarak ayrı bir sunucu tarafı WS bağlantı günlüğü yoktur. Tekrarlanan başarısızlıklara sunucu tarafı görünürlük gerekiyorsa,registry.Create/hub.Getçağrılarının etrafına kendi günlüklemenizi ekleyin.
2. Bayat oturum (sessionStorage key'i goui.sessionId)
İstemci, oturum ID'sini sessionStorage'da goui.sessionId key'i
altında kalıcı hale getirir (client/goui.js'de SESSION_KEY), böylece
sadece bir ağ kesintisi değil, bir sayfa yeniden yükleme, baştan
başlamak yerine aynı sunucu tarafı Session'a (ve dolayısıyla aynı
mount edilmiş bileşen durumuna) yeniden bağlanabilir.
Belirti: bir sunucu yeniden başlatmasından (bkz. §6) veya bir Hub
yeniden oluşturmasından sonra, tarayıcı hâlâ sessionStorage'da eski
bir goui.sessionId'ye sahiptir, ?session=<old-id> ile yeniden
bağlanmayı dener, ve sunucu — o ID hakkında artık hiçbir hafızası
olmayan — "session not found" ile yanıt verir ve yeni bir oturum
kaydetmeden bağlantıyı kapatır.
İstemcinin tam olarak bu durum için kendi kendini iyileştiren bir yolu zaten vardır — kurulumunuzda gerçekten erişilebilir olduğunu doğrulayın:
// client/goui.js — _handleFrame, case 'error'
if (message === 'session not found' || message.includes('session not found')) {
this.sessionId = '';
sessionStorage.removeItem(SESSION_KEY);
this.componentRoots.clear();
if (this.ws) this.ws.close();
this.onError(message + ' — reconnecting fresh');
return;
}
Bu otomatik olarak kurtarılmıyorsa kontrol listesi:
-
onclose, gerçekten_scheduleReconnect'e bağlı mı?GoUIClient'in yerleşik yeniden bağlanma mantığını geçersiz kıldıysanız veya atladıysanız (özel bir taşıma sarmalayıcısı, elleWebSocketkullanımı vb.), yukarıdaki otomatik "bayat ID'yi temizle ve taze yeniden bağlan" yolu asla çalışmaz — bunu yeniden üretmeniz gerekir. - Eski oturum ID'sinin bir kopyasını tutan başka bir şey mi var
(yükleme zamanında onu gömen sunucu tarafında render edilmiş bir
sayfa, bir çerez, özel bir depolama) ve istemci
sessionStorage'ı zaten temizledikten sonra onu yeniden mi enjekte ediyor? Yeniden bağlanmada?session=için tek doğruluk kaynağının,_buildUrl()zamanında taze okunansessionStorage.getItem('goui.sessionId')olduğunu onaylayın. - "Çıkış yap" / "kiracı değiştir" akışı için elle durumu mu
temizliyorsunuz? Farklı bir bileşen/kiracı ismiyle yeniden
bağlanmadan önce
sessionStorage.removeItem('goui.sessionId')'yi (veyasessionStorage.clear()'ı) kendiniz çağırın — aksi halde tarayıcı, (doğru bir şekilde) istediğiniz yeni bağlamla hiçbir ilişkisi olmayan bir oturuma yeniden bağlanmayı deneyecektir. - Birden fazla sekme beklenmedik şekilde bir oturum ID'sini mi
paylaşıyor?
sessionStorage, spesifikasyona göre sekme başınadır, ama ID'yi kendi kodunuzda bir yerdelocalStorage'a veya bir çereze kopyaladıysanız, iki sekme aynı sunucuSession'ına yeniden bağlanmak için yarışabilir, ve yarışı kaybedenews.ErrSessionAlreadyActivedöndürülür (Session.Reattach, bir bağlantı zaten aktifse yeniden bağlanmayı reddeder). Bir oturumu sekmeler arasında paylaşmayı özellikle amaçlamadığınız sürece (GoUI bunu kutudan çıktığı gibi desteklemez), oturum ID'lerinisessionStorage'a kapsamlı tutun.
3. Yama yolu "bir kayma" gibi görünüyor / yamalar yanlış elemana iniyor
Belirti: bir set_attr/update_text/replace yaması bir kardeşi
hedefliyor gibi görünüyor, görünürde hiçbir şey olmuyor, veya eksik bir
hedef hakkında bir hata görünüyor.
- Yolların, ham DOM
childNodes'a değil, "anlamlı çocuklar" aracılığıyla çözülen, bileşen köküne göre relatif olduğunu unutmayın. Hem sunucu (diff.ParseHTML/convertHTMLNode) hem de istemci (client/goui.js'dekimeaningfulChildren()) çocukları indekslemeden önce sadece-boşluk metin düğümlerini düşürür. İndeksleri ham tarayıcı DevToolschildNodesçıktısına karşı karşılaştırıyorsanız (HTML string'inizdeki girintiden gelen boşluk metin düğümlerini dahil eder), indeksler eşleşmez — her zaman indeksleri elemanlar + boş-olmayan metin açısından düşünün, omeaningfulChildren'ın hesapladığıyla eşleşecek şekilde. -
Render(), bir kök elemandan fazlasını mı döndürdü? Öyleyse, GoUI çıktınızı sentetik bir<div data-goui-component="...">içine sarar (bkz. 10-diffing-internals.md §3), ki bu, kendiRender()çıktınızı yalıtılmış olarak okurken beklediğinizden bir seviye daha aşağı her yolu kaydırır.Render()'ın tam olarak bir kök eleman döndürmesini sağlayarak düzeltin — bu, hem ekstra iç içe geçmeyi kaldırır hem de istemcinin[data-goui-component]ile aradığı elemanla eşleşir. - Diff'lenen önceki render ağacı, gerçekten diff'lenen ağaç mı?
Session.renderTrees[componentID], sadece başarılı birRender()+ ayrıştırmadan sonrasendRender/sendFullRendertarafından ayarlanır. Önceki birRender()çağrısı kendi istek işlemenizin ortasında hata verdiyse (ve hatayı yuttunuzsa), saklanan ağaç, o an canlı DOM'da olan şeyden daha eski olabilir; bu, saklanan ağaca karşı doğru görünen ama kullanıcının gerçekten görmesine karşı yanlış görünen yamalar üretir. HerHandleEventyolunun ya bir hata döndürdüğünden (istemciye bir hata frame'i olarak yansıtılır — bkz.Session.handleEventFrame) ya da bileşeni, sonrakiRender()çıktısı gerçeklikle eşleşen bir durumda bıraktığından emin olun. -
data-goui-ignoreile işaretlenmemiş bir bileşen alt ağacı içinde GoUI'nin dışında DOM'u elle mi düzenlediniz (bir tarayıcı eklentisi, başka bir script, elle DevTools düzenlemesi)? GoUI'nin diff'lemesi, canlı DOM'un render ettiği son ağaçla eşleştiğini varsayar; uzlaştırılmış bir alt ağaç içindeki herhangi bir bant dışı mutasyon, sonraki yamalar için indeksleri senkron dışına çıkarabilir. GoUI'nin onun üzerine uzlaştırma yapmasına izin vermek yerine, kasıtlı olarak istemciye ait olan herhangi bir bölgeyidata-goui-ignoreiçine sarın (§5).
4. Bir yamadan sonra Checkbox/radio durumu görsel olarak eşleşmiyor
Belirti: sunucu açıkça set_attr checked="checked"'i gönderdi (veya
kaldırdı), ama checkbox/radio'nun tarayıcıdaki görsel durumu eşleşecek
şekilde değişmiyor — veya değişiyor, sonra sonraki ilgisiz bir
etkileşimde geri dönüyor.
Bu iyi bilinen bir DOM tuhaflığıdır, bir GoUI hatası değil, ama GoUI'nin
bunu nasıl ele aldığını tam olarak bilmeye değer, böylece işlemenin
kendisi atlandığında tanıyabilirsiniz: checked, selected,
disabled, ve readonly için, bir checkbox ile etkileşime girildikten
sonra HTML öznitelikleri ve DOM özellikleri iki farklı şeydir —
checked özniteliğini setAttribute aracılığıyla ayarlamak, canlı
.checked özelliğini, tarayıcının gerçekten render ettiği şeyi,
güvenilir bir şekilde güncellemez. client/goui.js'nin applyPatch'i,
bunu hem set_attr hem de remove_attr için açıkça ele alır:
case 'set_attr': {
const target = resolvePath(rootEl, path);
if (target && target.nodeType === Node.ELEMENT_NODE) {
const name = patch.attr;
const value = patch.value ?? '';
target.setAttribute(name, value);
// Boolean DOM özellikleri, yamalamadan sonra form kontrolleri için senkron tutulmalıdır.
if (name === 'checked' || name === 'selected' || name === 'disabled' || name === 'readOnly' || name === 'readonly') {
const prop = name === 'readonly' ? 'readOnly' : name;
target[prop] = true;
}
if (name === 'value' && 'value' in target) {
target.value = value;
}
}
break;
}
Hâlâ bir uyumsuzluk görüyorsanız kontrol listesi:
-
data-goui-ignoreile işaretlenmiş bir alt ağacı mı yamalıyorsunuz?applyPatch, hedef (veya insert/remove için, ebeveyni) yok sayılmış bir bölgenin içindeyse (§5) yukarıdaki koda ulaşmadan önce çıkar — kasıtlı olarak, böylece istemciye ait widget'lara dokunulmaz. Checkbox'ınız ilgisiz bir sebeple böyle bir bölgenin içindeyse,checkeddurumu GoUI tarafından asla senkronize edilmez; tamamen o bölgenin sahibi olan her ne ise ona bağlıdır. - Yama gerçekten tarayıcıya ulaştı mı? Bu olay için gerçekten
bir
checked/valueset_attr/remove_attryamasının gönderildiğini ağ incelemecisi (veya geçici bironError/console günlüğü) aracılığıyla onaylayın — sunucu tarafı durum mutasyona uğradıktan sonraMarkDirty()hiç çağrılmadıysa, hiç render (ve dolayısıyla hiç yama) üretilmez; bileşenin sonraki ilgisiz render'ı sonunda değişikliği yansıtır, ki bu "bir sonraki tıklamada kendi kendini düzeltti" gibi görünebilir.forms.ChoiceInput.HandleEvent, tanınan bir değişiklik olayında koşulsuz olarakMarkDirty()'yi çağırır — özel bir checkbox kontrolü inşa ettiyseniz, sizinkinin de öyle yaptığından emin olun. - Bir
value/checkeddeğeri, o özelliğin uygulanmadığı bir eleman türünde mi ayarlanıyor (örn. bir<input>yerine düz bir<div>'decheckedayarlamak)? Yukarıdaki özellik-senkron dalı sadecetarget[prop]'u atar — eleman türünün o özelliği desteklediğini doğrulamaz; boolean olmayan bir elemanacheckedatamak, DOM'un bakış açısından sessizce bir no-op'tur, ki bu "yama uygulanmadı"ya aynı görünebilir.
5. Rich text / kod editörü imleci atlıyor, veya düzenlemeler eziliyor
Belirti: bir Quill (forms.RichTextEditor) veya CodeMirror
(forms.CodeEditor) örneğinde yazmak imlecin alanın başına/sonuna
atlamasına, seçimlerin kaybolmasına, veya editörün tamamının yazarken
görünür bir şekilde yeniden mount edilmesine sebep oluyor.
Bu, GoUI'nin diff/patch uzlaştırmasının, üçüncü taraf bir editör kütüphanesinin sahip olduğu ve kendi başına mutasyona uğrattığı DOM'a dokunmasına izin verildiğinde olur. Düzeltme her zaman aynı üç mekanizmanın bir kombinasyonudur — kontrolünüz için hepsinin gerçekten yerinde olduğunu kontrol edin:
-
Editörün mount elemanında
data-goui-ignore. HemRichTextEditor.Render()hem deCodeEditor.Render(), kendi dış sarmalayıcılarındadata-goui-ignore="1"'i ayarlar.client/goui.js'ninisGoUIIgnored/applyPatch'i, çözülmüş hedefi (veya insert/remove için, çözülmüş ebeveyni) bir[data-goui-ignore]atasının içinde olan herhangi bir yamayı atlar — bu, GoUI'nin uzlaştırıcısının ilk render'dan sonra, sunucu tarafında ne değişirse değişsin, o elemanın içindeki hiçbir şeye asla dokunmayacağı anlamına gelir. Özel bir üçüncü-taraf-widget sarmalayıcısı yazıyorsanız, bu özniteliği onun dış elemanına kopyalayın. -
Stabil, "render'da-boş" bir senkronizasyon kanalı, canlı değer değil. Ne
RichTextEditorne deCodeEditor, mevcutValue'yu her render'da yok sayılmış alt ağaca render eder — senkronizasyon<textarea class="goui-editor-sync">'i kasıtlı olarak her zaman boş render edilir, başlangıç değeri sadece mount zamanında JS köprüsü tarafından (bir kez okunan)data-initialaracılığıyla mevcuttur — bkz.client/modules/richtext.js'ninmountQuill'i. Özel bir kontrol bunun yerine widget'ın mevcut metnini her sunucu gidiş-dönüşünde markup'a yeniden render ediyorsa,data-goui-ignoreyerinde olsa bile diff yamalayıcısıyla savaşırsınız, çünkü onun yukarısındaki şekli sürekli değişen birdata-goui-ignore'lu eleman, hâlâ ebeveynin yeniden anahtarlanmasına/değiştirilmesine sebep olabilir. -
Bir render başka türlü gerekli değilse, saf DOM-senkron olayları için
HandleEvent'tencore.ErrSkipRenderdöndürün.Session.handleEventFrame, bu sentinel'i açıkça kontrol eder:if err := component.HandleEvent(ctx, frame.Event, payload); err != nil { if errors.Is(err, core.ErrSkipRender) { return // olayı onayla, ama hiçbir şeyi render/yama etme } // ... }RichTextEditor/CodeEditor'ın kendiHandleEventuygulamalarının şu anda sadeceValue'yu güncelleyipErrSkipRender'ı doğrudan döndürmek yerineMarkDirty()'yi atladığını (üst formun ne zaman yeniden render edileceğine karar vermesine güvenerek) unutmayın — her iki yaklaşım da, hiçbir görünür fayda sağlamayan bir değer için gereksiz bir yama döngüsünden kaçınır. Bir olay tamamen sunucu tarafı durumu istemciye ait bir widget'la senkron tutmak için var olduğunda ve bir yeniden render hiçbir görünür fayda sağlamayacaksa (ve yok sayılan bölgenin atası ilgisiz bir sebeple değiştirilirse imleç/seçim durumunu bozma riski taşıyorsa) kendi olay handler'larınızdaErrSkipRender'ı açıkça kullanın. -
Senkron olayını debounce edin. Her iki editör de, senkronizasyon
<textarea>'sining-inputbağlamasındag-debounce'u (varsayılan 350ms) ayarlar — her tek tuş vuruşunda bir WS olayı (ve dolayısıyla birHandleEventçağrısı) göndermek, görünür bir render tetiklemese bile israftır; benzer herhangi bir serbest-yazma senkronizasyon kanalı için bir debounce'u yerinde tutun.
6. "Bileşen kodunu değiştirdim ama hiçbir şey değişmedi" (sunucu yeniden başlatma)
Belirti: bir bileşenin Render/HandleEvent/Mount'unu (veya başka
herhangi bir Go kaynağını) düzenlediniz, tarayıcı sekmesini yeniden
yüklediniz, ve uygulama hâlâ eski kod gibi davranıyor.
Bunun, GoUI'ye karşı geliştirme yaparken en yaygın yanlış alarm olduğu
için içselleştirilmeye değer tek bir nedeni vardır: GoUI bileşenleri
derlenmiş Go kodudur, şablonlar veya yorumlanmış script'ler değil.
client/*.js içindeki istemci tarafı JS'nin aksine (tarayıcının normal
bir sayfa yeniden yüklemesinde yeniden aldığı), herhangi bir .go
dosyasındaki bir değişiklik — bir bileşen, bir doğrulama kuralı, kayıtlı
bir factory, registry bağlantısının kendisi — etkili olmadan önce Go
process'inin yeniden inşa edilmesi ve yeniden başlatılması gerekir.
GoUI'nin kendi içinde hot-reload yoktur.
Kontrol listesi:
- Gerçekten yeniden inşa mı ettiniz?
go run ./cmd/myapp, her çağrıda yeniden inşa eder; uzun süre çalışan birgo build && ./bin/myappiş akışı bunu yapmaz — yineleme yapıyorsanız, her seferinde yago build'i yeniden çalıştırın ya da kaydettiğinizde binary'i yeniden inşa eden ve yeniden başlatan bir dosya izleyici (air,wgo,entr,reflexvb.) kullanın. - Yeniden başlatılan process, vurduğunuz portu gerçekten bağladı
mı (önceki bir örnekten
"address already in use"yok)? Eski bir process'i sessizce trafiği sunmaya devam ettiren başarısız bir yeniden başlatma, "değişikliğimin hiçbir etkisi olmadı"yla aynı görünür. - Mevcut tarayıcı sekmeleri, eski process tarafından barındırılan
bir oturuma yeniden bağlanıp, sonra yeni olandan bir
"session not found"mı aldı? Yeniden başlatmadan sonra, bellek içi*ws.Hubboştur — önceden bağlı her tarayıcı sekmesiningoui.sessionId'si şimdi artık var olmayan bir oturumu ifade eder. §2'ye göre, istemci bunu otomatik olarak zaten kurtarır (bayat ID'yi temizler, taze yeniden bağlanır, yeniden mount eder). Taze bir mount gerçekleştiğini görmüyorsanız, sunucu tarafı değişikliğin etkili olmadığı sonucuna varmak yerine sekmeyi sert-yeniden yükleyin (herhangi bir önbelleğe alınmış JS'yi atlayarak). -
go.mod'unuzun gerçekten import ettiğigouimodülünün vendor edilmiş/kopyalanmış bir checkout'unu değil, gerçekten import edileni mi düzenliyorsunuz? Uygulamanızgithub.com/zatrano/goui'ye bir versiyon pin'i aracılığıyla bağımlıysa (bir localreplaceyönergesi çalışma kopyanıza işaret etmiyorsa), ayrı bir yerel klondaki dosyaları düzenlemek,go.mod'unuza yerel geliştirme içinreplace github.com/zatrano/goui => ../path/to/gouieklemeden ya da bağımlılığı yükseltmeden/yeniden vendor etmeden binary üzerinde bir etkisi olmaz. - Sadece stilendirmeyi mi değiştirdiniz (
forms/style.css, bir tema geçersiz kılması) ve bir Go yeniden inşasını mı bekliyorsunuz? CSS veclient/altındaki istemci JS modülleri, olduğu gibi sunulan statik dosyalardır — bunlar düz bir tarayıcı yeniden yüklemesinde (veya tarayıcı onları agresif bir şekilde önbelleğe aldıysa sert bir yeniden yüklemede) gerçekten etkili olur. Bir CSS değişikliğini kovalarken Go process'ini yeniden başlatmayın; tersine, bir tarayıcı yeniden yüklemesinin tek başına bir Go tarafı değişikliği alacağını beklemeyin.