client/web: clean up assets handling

A #cleanup that moves all frontend asset handling into assets.go
(formerly dev.go), and stores a single assetsHandler field back
to web.Server that manages when to serve the dev vite proxy versus
static files itself.

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy 2023-09-08 15:30:07 -04:00 committed by Sonia Appasamy
parent 4a38d8d372
commit 1eadb2b608
3 changed files with 49 additions and 47 deletions

View File

@ -4,6 +4,8 @@
package web
import (
"embed"
"io/fs"
"log"
"net/http"
"net/http/httputil"
@ -12,11 +14,42 @@
"os/exec"
"path/filepath"
"strings"
"tailscale.com/util/must"
)
// This contains all files needed to build the frontend assets.
// Because we assign this to the blank identifier, it does not actually embed the files.
// However, this does cause `go mod vendor` to include the files when vendoring the package.
// External packages that use the web client can `go mod vendor`, run `yarn build` to
// build the assets, then those asset bundles will be embedded.
//
//go:embed yarn.lock index.html *.js *.json src/*
var _ embed.FS
//go:embed build/*
var embeddedFS embed.FS
// staticfiles serves static files from the build directory.
var staticfiles http.Handler
func init() {
buildFiles := must.Get(fs.Sub(embeddedFS, "build"))
staticfiles = http.FileServer(http.FS(buildFiles))
}
func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) {
if devMode {
// When in dev mode, proxy asset requests to the Vite dev server.
cleanup := startDevServer()
return devServerProxy(), cleanup
}
return staticfiles, nil
}
// startDevServer starts the JS dev server that does on-demand rebuilding
// and serving of web client JS and CSS resources.
func (s *Server) startDevServer() (cleanup func()) {
func startDevServer() (cleanup func()) {
root := gitRootDir()
webClientPath := filepath.Join(root, "client", "web")
@ -45,10 +78,8 @@ func (s *Server) startDevServer() (cleanup func()) {
}
}
func (s *Server) addProxyToDevServer() {
if !s.devMode {
return // only using Vite proxy in dev mode
}
// devServerProxy returns a reverse proxy to the vite dev server.
func devServerProxy() *httputil.ReverseProxy {
// We use Vite to develop on the web client.
// Vite starts up its own local server for development,
// which we proxy requests to from Server.ServeHTTP.
@ -62,8 +93,9 @@ func (s *Server) addProxyToDevServer() {
w.Write([]byte("\n\nError: " + err.Error()))
}
viteTarget, _ := url.Parse("http://127.0.0.1:4000")
s.devProxy = httputil.NewSingleHostReverseProxy(viteTarget)
s.devProxy.ErrorHandler = handleErr
devProxy := httputil.NewSingleHostReverseProxy(viteTarget)
devProxy.ErrorHandler = handleErr
return devProxy
}
func gitRootDir() string {

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
<script type="module" crossorigin src="./assets/index-f8beba53.js"></script>
<script type="module" crossorigin src="./assets/index-4d1f45ea.js"></script>
<link rel="stylesheet" href="./assets/index-8612dca6.css">
</head>
<body>

View File

@ -7,14 +7,11 @@
import (
"context"
"crypto/rand"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"net/http/httputil"
"net/netip"
"os"
"path/filepath"
@ -31,35 +28,20 @@
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/util/httpm"
"tailscale.com/util/must"
"tailscale.com/version/distro"
)
// This contains all files needed to build the frontend assets.
// Because we assign this to the blank identifier, it does not actually embed the files.
// However, this does cause `go mod vendor` to include the files when vendoring the package.
// External packages that use the web client can `go mod vendor`, run `yarn build` to
// build the assets, then those asset bundles will be embedded.
//
//go:embed yarn.lock index.html *.js *.json src/*
var _ embed.FS
//go:embed build/*
var embeddedFS embed.FS
// staticfiles serves static files from the build directory.
var staticfiles http.Handler
// Server is the backend server for a Tailscale web client.
type Server struct {
lc *tailscale.LocalClient
devMode bool
devProxy *httputil.ReverseProxy // only filled when devMode is on
devMode bool
cgiMode bool
pathPrefix string
apiHandler http.Handler // csrf-protected api handler
assetsHandler http.Handler // serves frontend assets
apiHandler http.Handler // serves api endpoints; csrf-protected
}
// ServerOpts contains options for constructing a new Server.
@ -89,11 +71,7 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
cgiMode: opts.CGIMode,
pathPrefix: opts.PathPrefix,
}
cleanup = func() {}
if s.devMode {
cleanup = s.startDevServer()
s.addProxyToDevServer()
}
s.assetsHandler, cleanup = assetsHandler(opts.DevMode)
// Create handler for "/api" requests with CSRF protection.
// We don't require secure cookies, since the web client is regularly used
@ -107,11 +85,6 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
return s, cleanup
}
func init() {
buildFiles := must.Get(fs.Sub(embeddedFS, "build"))
staticfiles = http.FileServer(http.FS(buildFiles))
}
// ServeHTTP processes all requests for the Tailscale web client.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := s.serve
@ -151,14 +124,11 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
// Pass API requests through to the API handler.
s.apiHandler.ServeHTTP(w, r)
return
case s.devMode:
// When in dev mode, proxy non-api requests to the Vite dev server.
s.devProxy.ServeHTTP(w, r)
return
default:
// Otherwise, serve static files from the embedded filesystem.
s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
staticfiles.ServeHTTP(w, r)
if !s.devMode {
s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
}
s.assetsHandler.ServeHTTP(w, r)
return
}
}