Browser Live Reload with the upcoming Iris CLI :)

Former-commit-id: dd061ca2977d0d5456f72ac05b6a42d9bb0c2697
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-01-09 08:15:01 +02:00
parent 5ff2ed016d
commit 4cfd3c7a05
2 changed files with 111 additions and 4 deletions

View File

@ -706,7 +706,7 @@ func (api *APIBuilder) Use(handlers ...context.Handler) {
// It doesn't care about call order, it will prepend the handlers to all
// existing routes and the future routes that may being registered.
//
// The difference from `.DoneGLobal` is that this/or these Handler(s) are being always running first.
// The difference from `.DoneGlobal` is that this/or these Handler(s) are being always running first.
// Use of `ctx.Next()` of those handler(s) is necessary to call the main handler or the next middleware.
// It's always a good practise to call it right before the `Application#Run` function.
func (api *APIBuilder) UseGlobal(handlers ...context.Handler) {

113
iris.go
View File

@ -2,7 +2,7 @@ package iris
import (
// std packages
"bytes"
stdContext "context"
"errors"
"fmt"
@ -11,12 +11,11 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/kataras/golog"
// context for the handlers
"github.com/kataras/iris/v12/context"
// core packages, required to build the application
@ -36,6 +35,9 @@ import (
// handlers used in `Default` function
requestLogger "github.com/kataras/iris/v12/middleware/logger"
"github.com/kataras/iris/v12/middleware/recover"
"github.com/kataras/golog"
"gopkg.in/yaml.v3"
)
// Version is the current version number of the Iris Web Framework.
@ -875,6 +877,11 @@ func (app *Application) Build() error {
if !app.Router.Downgraded() {
// router
if err := app.tryInjectLiveReload(); err != nil {
rp.Errf("LiveReload: init: failed: %v", err)
}
// create the request handler, the default routing handler
routerHandler := router.NewDefaultHandler()
err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false)
@ -943,6 +950,106 @@ func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error {
return err
}
// tryInjectLiveReload tries to check if this application
// runs under https://github.com/kataras/iris-cli and if so
// then it checks if the livereload is enabled and then injects
// the watch listener (js script) on every HTML response.
// It has a slight performance cost but
// this (iris-cli with watch and livereload enabled)
// is meant to be used only in development mode.
// It does a full reload at the moment and if the port changed
// at runtime it will fire 404 instead of redirecting to the correct port (that's a TODO).
//
// tryInjectLiveReload runs right before Build -> BuildRouter.
func (app *Application) tryInjectLiveReload() error {
conf := struct {
Running bool `yaml:"Running,omitempty"`
LiveReload struct {
Disable bool `yaml:"Disable"`
Port int `yaml:"Port"`
} `yaml:"LiveReload"`
}{}
// defaults to disabled here.
conf.LiveReload.Disable = true
wd, err := os.Getwd()
if err != nil {
return err
}
for _, path := range []string{".iris.yml" /*, "../.iris.yml", "../../.iris.yml" */} {
path = filepath.Join(wd, path)
if _, err := os.Stat(path); err == nil {
inFile, err := os.OpenFile(path, os.O_RDONLY, 0644)
if err != nil {
return err
}
dec := yaml.NewDecoder(inFile)
err = dec.Decode(&conf)
inFile.Close()
if err != nil {
return err
}
break
}
}
if !conf.Running || conf.LiveReload.Disable {
return nil
}
scriptReloadJS := []byte(fmt.Sprintf(`<script>(function () {
const scheme = document.location.protocol == "https:" ? "wss" : "ws";
const endpoint = scheme + "://" + document.location.hostname + ":%d/livereload";
w = new WebSocket(endpoint);
w.onopen = function () {
console.info("LiveReload: initialization");
};
w.onclose = function () {
console.info("LiveReload: terminated");
};
w.onmessage = function (message) {
// NOTE: full-reload, at least for the moment. Also if backend changed its port then we will get 404 here.
window.location.reload();
};
}());</script>`, conf.LiveReload.Port))
bodyCloseTag := []byte("</body>")
app.WrapRouter(func(w http.ResponseWriter, r *http.Request, _ http.HandlerFunc) {
ctx := app.ContextPool.Acquire(w, r)
rec := ctx.Recorder() // Record everything and write all in once at the Context release.
app.ServeHTTPC(ctx) // We directly call request handler with Context.
if strings.HasPrefix(ctx.GetContentType(), "text/html") {
// delete(rec.Header(), context.ContentLengthHeaderKey)
body := rec.Body()
if idx := bytes.LastIndex(body, bodyCloseTag); idx > 0 {
// add the script right before last </body>.
body = append(body[:idx], bytes.Replace(body[idx:], bodyCloseTag, append(scriptReloadJS, bodyCloseTag...), 1)...)
rec.SetBody(body)
} else {
// Just append it.
rec.Write(scriptReloadJS)
}
if _, has := rec.Header()[context.ContentLengthHeaderKey]; has {
rec.Header().Set(context.ContentLengthHeaderKey, fmt.Sprintf("%d", len(rec.Body())))
}
}
app.ContextPool.Release(ctx)
})
return nil
}
// https://ngrok.com/docs
func (app *Application) tryStartTunneling() {
if !app.config.Tunneling.isEnabled() {