From 9640f59961c7f1d02268bcd726961e9ba1e87fad Mon Sep 17 00:00:00 2001 From: kataras Date: Fri, 10 Nov 2017 16:15:47 +0200 Subject: [PATCH] app. SPA from router wrapper to a simple handler, works faster now. Iris' router respects wildcards with other paths as well (unique) for almost a half year now... so we don't need a whole wrapper for those things anymore, fixes https://github.com/kataras/iris/issues/807 Former-commit-id: 5bd7f24997bb025a01bb92960a1bf255f073a228 --- .gitignore | 3 +- .../main.go | 3 +- .../embedded-single-page-application/main.go | 2 + context/context.go | 2 +- core/router/fs.go | 62 ++----------- core/router/router_spa_wrapper.go | 87 ++++++------------- iris.go | 13 +-- 7 files changed, 45 insertions(+), 127 deletions(-) diff --git a/.gitignore b/.gitignore index 73703f23..3d4d7d52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vscode -_authortools \ No newline at end of file +_authortools +/_examples/**/node_modules \ No newline at end of file diff --git a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go index 81d7c707..57b3f352 100644 --- a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go +++ b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go @@ -13,8 +13,7 @@ func newApp() *iris.Application { ctx.Writef("404 not found here") }) - assetHandler := app.StaticEmbeddedHandler("./public", Asset, AssetNames) - app.SPA(assetHandler) + app.StaticEmbedded("/", "./public", Asset, AssetNames) // Note: // if you want a dynamic index page then see the file-server/embedded-single-page-application diff --git a/_examples/file-server/single-page-application/embedded-single-page-application/main.go b/_examples/file-server/single-page-application/embedded-single-page-application/main.go index ec3d338b..fbf9618c 100644 --- a/_examples/file-server/single-page-application/embedded-single-page-application/main.go +++ b/_examples/file-server/single-page-application/embedded-single-page-application/main.go @@ -25,6 +25,8 @@ func newApp() *iris.Application { assetHandler := app.StaticEmbeddedHandler("./public", Asset, AssetNames) // as an alternative of SPA you can take a look at the /routing/dynamic-path/root-wildcard // example too + // or + // app.StaticEmbedded if you don't want to redirect on index.html and simple serve your SPA app (recommended). // public/index.html is a dynamic view, it's handlded by root, // and we don't want to be visible as a raw data, so we will diff --git a/context/context.go b/context/context.go index ee336936..ee09f300 100644 --- a/context/context.go +++ b/context/context.go @@ -2573,7 +2573,7 @@ var errTransactionInterrupted = errors.New("transaction interrupted, recovery fr // it's not covers all paths, // such as databases, this should be managed by the libraries you use to make your database connection, // this transaction scope is only for context's response. -// Transactions have their own middleware ecosystem also, look iris.go:UseTransaction. +// Transactions have their own middleware ecosystem also. // // See https://github.com/kataras/iris/tree/master/_examples/ for more func (ctx *context) BeginTransaction(pipe func(t *Transaction)) { diff --git a/core/router/fs.go b/core/router/fs.go index 3ab54947..02f65822 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -21,49 +21,6 @@ import ( "github.com/kataras/iris/context" ) -func calculateAssetValidator(vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) AssetValidator { - if len(vdir) > 0 { - if vdir[0] == '.' { - vdir = vdir[1:] - } - if vdir[0] == '/' || vdir[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed - vdir = vdir[1:] - } - } - - // collect the names we are care for, - // because not all Asset used here, we need the vdir's assets. - allNames := namesFn() - - var names []string - for _, path := range allNames { - // i.e: path = public/css/main.css - - // check if path is the path name we care for - if !strings.HasPrefix(path, vdir) { - continue - } - - names = append(names, path) - } - - return func(reqPath string) bool { - reqPath = strings.TrimPrefix(reqPath, "/"+vdir) - - for _, path := range names { - // in order to map "/" as "/index.html" - if path == "/index.html" && reqPath == "/" { - reqPath = "/index.html" - } - - if path == vdir+reqPath { - return true - } - } - return false - } -} - // StaticEmbeddedHandler returns a Handler which can serve // embedded into executable files. // @@ -809,7 +766,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo // can't use Redirect() because that would make the path absolute, // which would be a problem running under StripPrefix if strings.HasSuffix(ctx.Request().URL.Path, indexPage) { - localRedirect(ctx.ResponseWriter(), ctx.Request(), "./") + localRedirect(ctx, "./") return "", http.StatusMovedPermanently } @@ -830,12 +787,12 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo url := ctx.Request().URL.Path if d.IsDir() { if url[len(url)-1] != '/' { - localRedirect(ctx.ResponseWriter(), ctx.Request(), path.Base(url)+"/") + localRedirect(ctx, path.Base(url)+"/") return "", http.StatusMovedPermanently } } else { if url[len(url)-1] == '/' { - localRedirect(ctx.ResponseWriter(), ctx.Request(), "../"+path.Base(url)) + localRedirect(ctx, "../"+path.Base(url)) return "", http.StatusMovedPermanently } } @@ -845,7 +802,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo if d.IsDir() { url := ctx.Request().URL.Path if url[len(url)-1] != '/' { - localRedirect(ctx.ResponseWriter(), ctx.Request(), path.Base(url)+"/") + localRedirect(ctx, path.Base(url)+"/") return "", http.StatusMovedPermanently } } @@ -928,14 +885,13 @@ func toHTTPError(err error) (msg string, httpStatus int) { // localRedirect gives a Moved Permanently response. // It does not convert relative paths to absolute paths like Redirect does. -func localRedirect(w http.ResponseWriter, r *http.Request, newPath string) { - if q := r.URL.RawQuery; q != "" { +func localRedirect(ctx context.Context, newPath string) { + if q := ctx.Request().URL.RawQuery; q != "" { newPath += "?" + q } - w.Header().Set("Location", newPath) - w.WriteHeader(http.StatusMovedPermanently) - // ctx.Header("Location", newPath) - // ctx.StatusCode(http.StatusMovedPermanently) + + ctx.Header("Location", newPath) + ctx.StatusCode(http.StatusMovedPermanently) } func containsDotDot(v string) bool { diff --git a/core/router/router_spa_wrapper.go b/core/router/router_spa_wrapper.go index a43387bb..16b43e89 100644 --- a/core/router/router_spa_wrapper.go +++ b/core/router/router_spa_wrapper.go @@ -1,7 +1,6 @@ package router import ( - "net/http" "strings" "github.com/kataras/iris/context" @@ -29,7 +28,7 @@ func (s *SPABuilder) AddIndexName(filename string) *SPABuilder { } // NewSPABuilder returns a new Single Page Application builder -// It does what StaticWeb expected to do when serving files and routes at the same time +// It does what StaticWeb or StaticEmbedded expected to do when serving files and routes at the same time // from the root "/" path. // // Accepts a static asset handler, which can be an app.StaticHandler, app.StaticEmbeddedHandler... @@ -47,7 +46,7 @@ func NewSPABuilder(assetHandler context.Handler) *SPABuilder { AssetHandler: assetHandler, AssetValidators: []AssetValidator{ func(path string) bool { - return true // returns true by-default + return true // returns true by-default, if false then it fires 404. }, }, } @@ -62,65 +61,31 @@ func (s *SPABuilder) isAsset(reqPath string) bool { return true } -// BuildWrapper returns a wrapper which serves the single page application -// with the declared configuration. -// -// It should be passed to the router's `WrapRouter`: -// https://godoc.org/github.com/kataras/iris/core/router#Router.WrapRouter -// -// Example: https://github.com/kataras/iris/tree/master/_examples/file-server/single-page-application-builder -func (s *SPABuilder) BuildWrapper(cPool *context.Pool) WrapperFunc { +// Handler serves the asset handler but in addition, it makes some checks before that, +// based on the `AssetValidators` and `IndexNames`. +func (s *SPABuilder) Handler(ctx context.Context) { + path := ctx.Path() - fileServer := s.AssetHandler - - wrapper := func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { - path := r.URL.Path - - // make a validator call, by-default all paths are valid and this codeblock doesn't mean anything - // but for cases that users wants to bypass an asset she/he can do that by modifiying the `APIBuilder#AssetValidators` field. - // - // It's here for backwards compatibility as well, see #803. - if !s.isAsset(path) { - // it's not asset, execute the registered route's handlers - router(w, r) - return - } - - for _, index := range s.IndexNames { - if strings.HasSuffix(path, index) { - localRedirect(w, r, "./") - // "/" should be manually registered. - // We don't setup an index handler here, - // let full control to the user - // (use middleware, ctx.ServeFile or ctx.View and so on...) - return - } - } - - ctx := cPool.Acquire(w, r) - // convert to a recorder in order to not write the status and body directly but wait for a flush (EndRequest). - rec := ctx.Recorder() // rec and context.ResponseWriter() is the same thing now. - // execute the asset handler. - fileServer(ctx) - // check if body was written, if not then; - // 1. reset the whole response writer, its status code, headers and body - // 2. release only the object, - // so it doesn't fires the status code's handler to the client - // (we are eliminating the multiple response header calls this way) - // 3. execute the router itself, if route found then it will serve that, otherwise 404 or 405. - // - // we could also use the ctx.ResponseWriter().Written() > 0. - empty := len(rec.Body()) == 0 - if empty { - rec.Reset() - cPool.ReleaseLight(ctx) - router(w, r) - return - } - // if body was written from the file server then release the context as usual, - // it will send everything to the client and reset the context. - cPool.Release(ctx) + // make a validator call, by-default all paths are valid and this codeblock doesn't mean anything + // but for cases that users wants to bypass an asset she/he can do that by modifiying the `APIBuilder#AssetValidators` field. + // + // It's here for backwards compatibility as well, see #803. + if !s.isAsset(path) { + // it's not asset, execute the registered route's handlers + ctx.NotFound() + return } - return wrapper + for _, index := range s.IndexNames { + if strings.HasSuffix(path, index) { + localRedirect(ctx, "./") + // "/" should be manually registered. + // We don't setup an index handler here, + // let full control to the user + // (use middleware, ctx.ServeFile or ctx.View and so on...) + return + } + } + + s.AssetHandler(ctx) } diff --git a/iris.go b/iris.go index 10651ab7..7b4149ae 100644 --- a/iris.go +++ b/iris.go @@ -334,19 +334,14 @@ var ( // SPA accepts an "assetHandler" which can be the result of an // app.StaticHandler or app.StaticEmbeddedHandler. -// It wraps the router and checks: -// if it;s an asset, if the request contains "." (this behavior can be changed via /core/router.NewSPABuilder), -// if the request is index, redirects back to the "/" in order to let the root handler to be executed, -// if it's not an asset then it executes the router, so the rest of registered routes can be -// executed without conflicts with the file server ("assetHandler"). -// -// Use that instead of `StaticWeb` for root "/" file server. +// Use that when you want to navigate from /index.html to / automatically +// it's a helper function which just makes some checks based on the `IndexNames` and `AssetValidators` +// before the assetHandler call. // // Example: https://github.com/kataras/iris/tree/master/_examples/file-server/single-page-application func (app *Application) SPA(assetHandler context.Handler) *router.SPABuilder { s := router.NewSPABuilder(assetHandler) - wrapper := s.BuildWrapper(app.ContextPool) - app.Router.WrapRouter(wrapper) + app.APIBuilder.HandleMany("GET HEAD", "/{f:path}", s.Handler) return s }