LiveReload

This commit is contained in:
Jeff Winkler
2025-11-08 13:05:23 -05:00
parent c1ddb6441e
commit 3b658ed7d0
7 changed files with 185 additions and 1 deletions

View File

@@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@@ -0,0 +1,2 @@
tmp/
build-errors.log

View File

@@ -0,0 +1,28 @@
# Live Reload with Air
Hot-reloads your Go code and web page.
## Setup
If you don't have Air yet:
```bash
go install github.com/air-verse/air@latest
```
## Run
```bash
air
```
Then open `http://localhost:3000` in your browser.
## How It Works
Air watches your Go files and rebuilds when you make changes.
LiveReloadPlugin handles browser refresh through a SSE connection at `/dev/reload`. When Air restarts the server, the connection drops, triggering an automatic page reload after 100ms. This only runs on localhost.
## Files
- `.air.toml` - Air config
- `livereload.go` - Via plugin for browser auto-reload

View File

@@ -0,0 +1,57 @@
package main
import (
"net/http"
"github.com/go-via/via"
"github.com/go-via/via/h"
)
func LiveReloadPlugin(v *via.V) {
v.HandleFunc("GET /dev/reload", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
<-r.Context().Done()
})
}
func liveReloadScript() h.H {
return h.Script(h.Raw(`
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
const evtSource = new EventSource('/dev/reload');
let overlay = null;
let showTimer = null;
evtSource.onerror = () => {
evtSource.close();
showTimer = setTimeout(() => {
if (!overlay) {
overlay = document.createElement('div');
overlay.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(200, 200, 200, 0.95); padding: 20px 40px; border-radius: 8px; color: #333; font-size: 24px; z-index: 999999; font-family: -apple-system, sans-serif;';
overlay.textContent = '🔌 Reconnecting...';
document.body.appendChild(overlay);
}
}, 1000);
(async function poll() {
for (let i = 0; i < 100; i++) {
try {
const res = await fetch('/', { method: 'HEAD', signal: AbortSignal.timeout(1000) });
if (res.ok) {
clearTimeout(showTimer);
if (overlay) overlay.remove();
location.reload();
return;
}
} catch (e) {}
await new Promise(r => setTimeout(r, 50));
}
})();
};
}
`))
}

View File

@@ -0,0 +1,46 @@
package main
import (
"github.com/go-via/via"
"github.com/go-via/via/h"
)
type Counter struct{ Count int }
func main() {
v := via.New()
LiveReloadPlugin(v)
v.Config(via.Options{
DocumentTitle: "Live Reload",
DocumentHeadIncludes: []h.H{
liveReloadScript(),
},
})
v.Page("/", func(c *via.Context) {
data := Counter{Count: 0}
step := c.Signal(1)
increment := c.Action(func() {
data.Count += step.Int()
c.Sync()
})
c.View(func() h.H {
return h.Div(h.Class("container"),
h.H1(h.Text("Live Reload")),
h.P(h.Textf("Count: %d", data.Count)),
h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())),
h.Label(
h.Text("Update Step: "),
h.Input(h.Type("number"), step.Bind()),
),
h.Button(h.Text("Increment"), increment.OnClick()),
)
})
})
v.Start(":3000")
}

View File

@@ -35,7 +35,7 @@ func (s *signal) Err() error {
return s.err
}
// Bind binds this signal to an imput element. When the imput changes
// Bind binds this signal to an input element. When the input changes
// its value the signal updates in real-time in the browser.
//
// Example:

5
via.go
View File

@@ -75,6 +75,9 @@ func (v *V) Config(cfg Options) {
if cfg.LogLvl != v.cfg.LogLvl {
v.cfg.LogLvl = cfg.LogLvl
}
if cfg.DocumentTitle != "" {
v.cfg.DocumentTitle = cfg.DocumentTitle
}
if cfg.DocumentHeadIncludes != nil {
v.cfg.DocumentHeadIncludes = cfg.DocumentHeadIncludes
}
@@ -142,6 +145,8 @@ func (v *V) registerCtx(id string, c *Context) {
// }
func (v *V) getCtx(id string) (*Context, error) {
v.contextRegistryMutex.RLock()
defer v.contextRegistryMutex.RUnlock()
if c, ok := v.contextRegistry[id]; ok {
return c, nil
}