-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Minimal Integration Option #8
Comments
I'm not against it per-se, but I'm thinking about responsibilities and flexibility here. Maybe we're pushing too much into the library. To pick up the thought I had in the other issue... What if To make it more concrete, I'm thinking of application code that uses it something like this: func Frontend(next http.Handler) http.Handler {
// Setup the vite.Handler (middleware) here
v := vite.NewHandler(...)
// or
v := vite.Middleware(...)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if v can handle the requested asset by consulting the manifest.
// If so, return early.
...
// Switch case based on r.URL.Path and e.g. initialize a Go template.
...
// Consult v about the necessary things to inject into the template,
// like the <script>...</script> and <link>...</link> in the HTML head.
// Inject into the Go template and render.
...
next.ServeHTTP(w, r)
})
} In your app, with, e.g., gorilla/mux router, you'd use it something like this ... r := mux.NewRouter()
r.Use(Frontend)
... By doing so, this library would not have any opinions on how your app renders HTML, which router it uses, and what else you want to do within your templating. It would simply handle the Vite backend part and help you add the required HTML snippets to your HTML templating logic. I haven't thought this through in every detail. So think of it as a thought experiment only. Maybe I've overlooked dependencies between the app and the library. Happy to get feedback about it. |
I really like this idea! The middleware-style approach seems promising and could lead to a cleaner separation of concerns. How would this affect the developer experience for simpler use cases? Would there be a way to provide both this flexible approach and a simpler "batteries-included" option for those who prefer it? While this might involve some breaking changes, the benefits in flexibility could be worth it. Overall, I'm excited about the potential of this direction and would be happy to help out if needed. What do you think of something like this? // Custom ResponseWriter to capture the output
type customResponseWriter struct {
http.ResponseWriter
body []byte
}
func (crw *customResponseWriter) Write(b []byte) (int, error) {
crw.body = append(crw.body, b...)
return len(b), nil
}
// Helper function to inject a block into the HTML
func insertViteBlock(content []byte, placeholder, injection string) string {
return strings.Replace(string(content), placeholder, injection, 1)
}
func (h *Handler) Middleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
crw := &customResponseWriter{ResponseWriter: w}
next.ServeHTTP(crw, r)
viteBlock := `
{{- if .IsDev }}
{{ .PluginReactPreamble }}
<script type="module" src="{{ .ViteURL }}/@vite/client"></script>
{{- if ne .ViteEntry "" }}
<script type="module" src="{{ .ViteURL }}/{{ .ViteEntry }}"></script>
{{- else }}
<script type="module" src="{{ .ViteURL }}/src/main.tsx"></script>
{{- end }}
{{- else }}
{{- if .StyleSheets }}
{{ .StyleSheets }}
{{- end }}
{{- if .Modules }}
{{ .Modules }}
{{- end }}
{{- if .PreloadModules }}
{{ .PreloadModules }}
{{- end }}
{{- end }}
`
tmpl, err := template.New("vite").Parse(viteBlock)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
page := pageData{
IsDev: h.isDev,
ViteEntry: h.viteEntry,
ViteURL: h.viteURL,
}
// Handle both development and production modes.
if h.isDev {
page.PluginReactPreamble = template.HTML(PluginReactPreamble(h.viteURL))
} else {
var chunk *Chunk
if page.ViteEntry == "" {
chunk = h.manifest.GetEntryPoint()
} else {
entries := h.manifest.GetEntryPoints()
for _, entry := range entries {
if page.ViteEntry == entry.Src {
chunk = entry
break
}
}
if chunk == nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
page.StyleSheets = template.HTML(h.manifest.GenerateCSS(chunk.Src))
page.Modules = template.HTML(h.manifest.GenerateModules(chunk.Src))
page.PreloadModules = template.HTML(h.manifest.GeneratePreloadModules(chunk.Src))
}
var block strings.Builder
err = tmpl.Execute(&block, page)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
content := insertViteBlock(crw.body, "<meta name=\"vite\">", block.String())
w.Write([]byte(content))
}
} |
The snippet provided above feels a bit ad hoc, so I wanted to share the progress I've made in implementing this concept more systematically. I've decoupled // This block demonstrates the setup and usage of `vite.Middleware` in a Go
// web application.
//
// The Middleware is then applied to the root route ("/") of an HTTP mux,
// where it processes incoming requests before rendering a "index.tmpl"
// template.
//
// This setup allows a minimal integration of Vite with the Go web server,
// handling both development and production environments.
viteMiddleware, err := vite.NewMiddleware(vite.Config{
FS: viteFS,
IsDev: *isDev,
ViteURL: "http://localhost:5173",
})
if err != nil {
panic(err)
}
mux.HandleFunc("/", viteMiddleware.Use(func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.New("index.tmpl").ParseFS(goIndex, "index.tmpl")
if err != nil {
panic(err)
}
if err = tmpl.Execute(w, nil); err != nil {
panic(err)
}
})) I've created a pull request that demonstrates this approach. The PR includes:
You can find the PR here: Insert PR link I'd love to get your thoughts on this implementation. Does this align with what you had in mind? Are there any areas you think could be improved or refined further? |
Thank you so much. The issue is the better forum to discuss this, I agree (I shared my first thoughts on the PR). My first gut feeling is that with your approach, the But don't worry, your idea makes sense. I have to test and feel it from the application developer's perspective. I'm busy during the weeks currently, so it might take some time to complete for me. Thank you very much for pushing this forward though. |
Hi, I have been working out how this library works to integrate vite into my golang app using Inertia.js. I have not set it up so far because I wanted to render my HTML separately rather than let this library handle templates. I very much agree with what you have said @olivere:
Personally I would prefer to use this library as a helper function, so I can write my own template, and call a function to generate my HTML elements for vite such as:
Perhaps this function could call similar code that @ge3224 has written above but rather than injecting it with the response writer, we can just generate it as a string and let the user of this library render the HTML themselves. I think it would be nice to offer both this level of flexibility and a 'batteries-included' approach you are talking about. The most complex part of using Vite that would stop me from implementing it manually is generating the HTML tags from the Vite manifest.json, which this library can already do well but is hidden inside the other templating functionality. For me the use case with using Inertia.js, is that I only have 1 HTML template, so it is not difficult to call a function for the vite block, and I would rather avoid as much complexity as possible. I am happy to contribute with a PR - thanks for all your work so far :) |
Hi @danclaytondev. Nice to have you on board. Your approach is what I had in mind. I think the library should be told how to work (production/development environment etc.) and then act like a library. When asked, return the specifics of what should be injected into your templating library of choice. Something like this: type PageData struct {
Scripts template.HTML
...
} The app calls this library when rendering HTML: func Index(w http.ResponseWriter, r *http.Request) {
vite, err := v.Page(... <entrypoint> and other options ...)
if err != nil { ... }
// Now you can inject the "vite" into e.g. the Go HTML templating
data := struct {
Title string
Vite *vite.PageData
}{
Title: "My homepage",
Vite: vite,
}
err = tmpl.Execute(w, data)
if err != nil { ... }
}
var tmpl = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
{{ .Vite.Scripts }} <-- injecting the Vite script data here
</head>
<body>
...
</body>
</html>`)) The app is then free to use the Of course, we should also handle the part that serves the Vite assets, probably best in the form of a simple file server that can be added to the router of the app. I need something like this for an app I'm building myself next. But it may take me another few days to start. So, if you have the time and feel confident to approach this, feel free to do so. EDIT 1: Just changed |
I've started a basic implementation, I will post a draft PR once it is a little better and then we can discuss :) |
I really liked @danclaytondev's idea for a helper function approach. In fact, I was so excited about it that I went ahead and implemented it into a project I'm working on today. In my enthusiasm, I also created a PR implementing this helper function feature. I apologize for submitting this PR before @danclaytondev had a chance to contribute their implementation. @danclaytondev, I'm more than happy to step back if you'd like to take the lead on this feature. Alternatively, if you're open to it, perhaps we could collaborate on refining the implementation. I'm genuinely excited about this approach and would love to see it integrated into the library in the best possible way. @olivere, I welcome any feedback or suggestions on the implementation. The PR is open, and I'm more than willing to make adjustments based on the community's input. |
Hey! Thanks @danclaytondev and @ge3224. Yes, that's a good starter. That helper injects the necessary fragment into the application-provided HTML template. Then we're just missing a I like the approach. |
Hi @ge3224, no need to apologize at all, it's great to see this progress! It was worth me starting a solution to learn a bit more about Vite, and the code I wrote is very similar to how you have set it up. I hope that means its a good solution :) @olivere, what do you think is the best way to serve the files for production? I think we want to be unopinionated about what setup they are using (just For production mode, if the library user needs to add the Finally, shall we setup a feature branch so we can contribute to this before it all goes into main. I can write some tests. |
@danclaytondev Regarding your questions... I think we should always rely on the standard library and not provide anything specific to a Go web framework. A What I do today is to embed the generated React app into my Go binary (with So I think we have to solve two problems in both development and production mode:
Development modeIn development mode, we don't have to do anything for step 2 as the Vite dev server would do the serving for us. And step 1 is as simple as returning the preamble to the application. Vite docs describe this in step 2. Production modeIn production mode, it's a bit more complex. I think step 2 could be solved by providing a Step 1 is harder: Here we need the "relative src path from project root" (as stated in the Vite docs in step 3) to know what to render. With that key, we can consult the My implementation (or should I say: interpretation) of that can be found in the ProblemNow, this is where I think I'm overcomplicating things and maybe you two could help me prove that and find a simple solution ;-) When working with more complex React apps, you typically have something like a client-side router (e.g. React Router). Those routers typically want you to set up a routing table like What my Go backends typically do is to delegate to the React app when the request doesn't match any of its registered handlers. Here's an example:
I wonder if this library can handle step 2 and still continue all the way to the fallback should the request not actually be a Vite asset. How can we do that with a simple What am I missing? Did I explain it in a understandable way? Is there even a problem? Please prove me wrong. |
I've modified the example in the PR to more clearly demonstrate how static files, vite-managed assets, and Vite build files for prod could be served using the Go standard library. (See snippet below.) The example demonstrates three distinct handlers for related requests:
Admittedly, the example may be verbose, but it serves to illustrate how Vite can be integrated into a larger Go application. This approach could easily be adapted to different use cases and other libraries like Gorilla or Echo. Additionally, the PR as it stands preserves the existing //go:embed all:static
var static embed.FS // embedding static files as @olivere mentions
// ....
mux := http.NewServeMux() // This is the HTTP multiplexer in the standard library, not Gorilla Mux
// Serve assets that Vite would normally treat as 'public' assets.
//
// In this example, the 'static' directory is used as a replacement for
// Vite's default 'public' folder. (The 'publicDir' property has been set to `false`
// in vite.config.ts.) We're using the 'static' directory to achieve similar
// functionality, but available to the Go backend and Vite.
//
// To use a static asset in our Vite frontend application, we import it in JS like this:
//
// ```js
// import viteLogo from '/static/vite.svg' // (instead of '/vite.svg')
// ```
var staticFileServer http.Handler
if *isDev {
staticFileServer = http.FileServer(http.Dir("static"))
} else {
staticFS, err := fs.Sub(static, "static")
if err != nil {
panic(err)
}
staticFileServer = http.FileServer(http.FS(staticFS))
}
mux.Handle("/static/", http.StripPrefix("/static/", staticFileServer))
// Serve Vite-managed assets from the Go backend, accommodating both
// development and production environments.
//
// Usage in Vite remains the same as in a standard Vite setup. The Go backend
// will serve the assets from the correct location based on the environment.
var viteAssetsFileServer http.Handler
var viteAssetsSrcAttribute string // vite changes this path, depending on dev or prod
if *isDev {
viteAssetsFileServer = http.FileServer(http.Dir("src/assets"))
viteAssetsSrcAttribute = "/src/assets/"
} else {
viteAssetsFS, err := fs.Sub(static, "static/dist/assets")
if err != nil {
panic(err)
}
viteAssetsFileServer = http.FileServer(http.FS(viteAssetsFS))
viteAssetsSrcAttribute = "/assets/"
}
mux.Handle(viteAssetsSrcAttribute, http.StripPrefix(viteAssetsSrcAttribute, viteAssetsFileServer))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// The filesystem used to access the build manifest.
var viteFS fs.FS
if *isDev {
viteFS = os.DirFS(".")
} else {
fs, err := fs.Sub(static, "static/dist")
if err != nil {
panic(err)
}
viteFS = fs
}
// viteFragment generates the necessary HTML for Vite integration.
//
// It calls vite.HTMLFragment with a Config struct to create an HTML fragment
// that includes all required Vite assets and scripts. This fragment is of type
// `template.HTML` and is intended to be applied as data, replacing a `{{ .Vite }}`
// placeholder in the <head> section of the primary handler's executed HTML template.
viteFragment, err := vite.HTMLFragment(vite.Config{
FS: viteFS,
IsDev: *isDev,
ViteURL: "http://localhost:5173",
})
if err != nil {
panic(err)
}
tmpl, err := template.New("index.tmpl").ParseFS(goIndex, "index.tmpl")
if err != nil {
panic(err)
}
if err = tmpl.Execute(w, map[string]interface{}{
"Vite": viteFragment,
}); err != nil {
panic(err)
}
}) |
@ge3224 Thanks for the examples. I find it very helpful to have code to look at; for me, it's the best documentation I can find. |
Add Support for Registering Pre-Parsed Templates
Description
Currently, only template strings can be integrated with the library because
RegisterTemplate
accepts string arguments for templates and handles its own parsing.This limitation poses an issue when a Go backend is responsible for its own template parsing. For instance, an application might compose templates with partials to facilitate the reuse of common elements, such as a navigation bar.
There is no way to register these pre-parsed templates with the Vite handler.
Proposed Solution
To address this limitation while maintaining backwards compatibility, I propose adding a new method that allows the registration of pre-parsed templates. This method provides additional flexibility for applications that handle their own template parsing without disrupting existing functionality that relies on
RegisterTemplate
.This following example demonstrates how the method might be used to integrate embedded templates with the Vite handler while leveraging the new RegisterParsedTemplate method:
Conclusion
This change would boost the Vite handler’s flexibility and support different ways of handling templates. Looking forward to hearing your thoughts—thanks for considering it!
The text was updated successfully, but these errors were encountered: