Golang与SvelteKit对比Svelte在SSR方面的探讨
Golang与SvelteKit对比Svelte在SSR方面的探讨 大家好,
过去一个月,我一直在用 Go 语言构建我的项目(运输管理系统+CRM)的后端,同时每天都在纠结前端该用什么。我推崇任何强调简洁性的方案,这也是我知道自己应该使用 HTMX 的原因,但它确实会在设计上带来一些长远的限制。我的 JavaScript/TypeScript 经验很少,我认为它是一种必要的“恶”。仅仅通过比较 React、Vue 和 Svelte 的语法,似乎 Svelte 就是我想要的简洁解决方案。
我知道你们中有些人可能不爱听这个,但我确实会与 LLM 聊天来获取想法和学习(总是会验证,所以才发此帖)。当我询问将 Svelte 与 Go 一起使用时,技术栈和文件结构会是什么样子时,它推荐了 SvelteKit 而不是 Svelte,理由是服务器端渲染。请原谅我的无知(我在 1995 年至 2002 年间用 VB 6.0 和 C++ 编程,然后在 2023 年学习了 Apex 和 Python),但我的理解是,在后端使用 Go 的目的就是为了让 JavaScript 留在客户端;尽可能远离服务器,我认为它就应该待在那里(感谢 Ryan Dahl)。
有人能花几分钟时间就这个话题给我讲解一下吗?
更多关于Golang与SvelteKit对比Svelte在SSR方面的探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html
哈哈,这对我来说简直是瞬间焦虑发作。
更多关于Golang与SvelteKit对比Svelte在SSR方面的探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
@Dean_Davidson 抱歉帖子间隔时间这么长,过去45天我们正在进行数据迁移。
大家的帮助都非常大,但作为一个视觉学习者,这个帖子真的让我理解了。非常感谢。
如果沿用那个假设的例子,后端文件结构会是什么样子的?
啊,是的,我已经给这个仓库点了星,并且一直在研究它。
无论你做什么,绝对不要把那个仓库作为起点。哈哈。赶紧取消星标!
// 代码示例:一个简单的Go程序
func main() {
fmt.Println("hello world")
}
你的Svelte应用没有理由必须是单页应用。多年来,我一直使用Go后端和Svelte前端进行编码——我通常采用多页面方案。如果你需要多个页面,直接提供多个HTML文件即可;如果你需要在客户端保持一些持久状态,可以轻松地通过会话存储在不同页面间共享。
出于好奇,你是如何处理服务器端渲染的?假设你有一个页面,需要显示来自API调用的数据,如下所示:
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
你是如何将其作为HTML返回,并让标题和内容在服务器端渲染时就被填充好的?或者,你使用的是我称之为混合方法的方式,即只返回一个空的标题/内容,然后由客户端使用fetch API来设置标题/内容?这是Svelte唯一让我困惑的部分,因为要真正在服务器端渲染内容,你需要在服务器上有某种运行时环境。
拆分为更小的独立模块:使用单页应用时,内存中通常会有大量状态,许多组件会变得相互交织。最终你会得到一个由相互连接的代码构成的大网,在一端进行修改可能会破坏另一端的某些功能。而使用独立的页面则有一个清晰的边界。因此,多个开发者可以并行处理不同的页面,你的测试也不需要跨越页面边界。我见过一些单页应用,通过不同页面的特定路径会导致一些难以发现的细微错误。
是的,这也是我对类似 React 的语言的担忧。自从重新开始编程以来,我唯一的经验就是用 NextJS 创建我们公司的网站,这个过程对我来说就像拔牙一样痛苦。做一个小小的改动,整个网站就崩溃了,但我想这就是“类型安全”语言的魅力所在?我不知道,这是我去年才学到的一个术语。
即使是在处理高度动态的数据驱动型应用时,我也会采用静态渲染技术,以最大限度地减少页面加载过程中的闪烁或布局偏移。采用静态渲染方法时,像 Astro 这样的软件会为加载数据预渲染视图。我通常使用简单的骨架占位符,这样当数据实际到达时,页面上的变动会非常小。
如果一切运行完美,用户会看到加载主要 HTML 和 CSS 所需的极短阻塞时间,然后直接渲染出 UI 的静态版本。接着,水合过程将启动 JavaScript 并开始加载数据。当数据到达时,只有 UI 中显示数据的部分会被更新(例如分页表格中单元格的内容)。通过良好的缓存机制,初始加载状态的显示几乎可以瞬间完成。
如果完全不使用服务器端渲染,页面将不得不加载并解析 JavaScript,用组件创建虚拟 DOM,并将其渲染为 HTML 供浏览器显示。这可能导致在用户等待期间出现闪烁或布局偏移,因为浏览器在执行大量 JavaScript 代码的同时,需要多次更新渲染树。
我倾向于采用多页面方案,主要有以下几个原因:
- 拆分为更小的独立模块:在单页面应用(SPA)中,通常会有大量状态保存在内存中,许多组件会变得相互交织。最终,你会得到一个由相互关联的代码构成的大网,在一端进行修改可能会在别处引发问题。而使用独立的页面则能提供清晰的边界。这样,多名开发者可以并行处理不同的页面,你的测试也无需跨越页面边界。我曾见过一些SPA,在页面间按特定路径操作时,会导致一些难以发现的细微错误。
- 保持简单(KISS):浏览器在处理页面方面表现良好。你不需要模拟历史记录,链接/书签本身就指向实际的页面。你可以直接跳转到某个页面,而无需加载或运行任何路由逻辑。
- 首次加载性能:Astro 会将每个单独的页面静态构建为预渲染的 HTML。因此,用户通过链接跳转到特定页面时,将获得更快的首次加载体验。
但我的理解是,在后端使用 Go 的目的是将 JavaScript 保留在客户端;尽可能远离服务器。
嗯,这完全取决于你如何使用 Go。如果你愿意,你可以用 Go 在服务器端生成 100% 的网站内容(使用模板等),完全不需要使用前端框架或 JavaScript。你也可以采用混合方法,即用 Go 生成 HTML,然后使用 JS/前端框架进行差异加载。你还可以使用像 React 这样的传统 SPA 框架,将 JavaScript 发送到客户端,然后由客户端操作 DOM。
我对 Sveltekit 以及他们如何处理 SSR 不是很熟悉。但是——对于一个客户端库来处理 SSR,你必须在服务器上有某种运行时。看起来他们有面向特定目标的“适配器”概念:
![]()
适配器 • Svelte 文档
看起来大多数将 svelte 和 go 结合使用的人都在使用“静态”适配器,然后用 Go 来提供这些内容。我不知道这如何能够使用来自 RESTful API 的动态数据来预渲染某些内容(为此你将需要一个服务器端运行时)。但也许可以尝试一下静态适配器,看看它是否适合你。
是的,我主要使用 fetch API,因为它能很好地分离数据和表示层。对于像下面这样的简单应用:
{#await (await fetch('/data')).json()}
Loading...
{:then data}
<span>{data.name}</span>
{:catch err}
<span class="bg-red">{err}</span>
{/await}
但数据通常会按需动态重新加载,并带有不同的查询(例如分页表格数据),因此由 JavaScript 负责加载,而 Svelte 则动态更新前端表格。
我偶尔会为了提升内部页面首次加载性能而做的是:使用 Go 的模板引擎在服务器端插入数据。但我将数据以 JSON 格式直接插入到 JavaScript 中,然后让 Svelte 将其渲染到前端组件中。
// go 将数据插入到这个字符串字面量中:
const serverData=`{{data}}`
...
//Svelte 将数据传递给组件,以安全地渲染复杂的结构化数据
<Table data={data.rows}>
对于大多数公开网站,我使用 Atro 作为 Svelte 的构建工具,它在构建时使用静态渲染和水合技术。这提供了完美的 SEO 性能,且无需任何服务器端要求,因为只有用户特定的动态内容是通过 fetch API 加载的,从而实现了静态数据的快速缓存和水合技术带来的快速渲染的完美结合。
与LLM聊天是可以的。它们能给你一些好点子。但它们往往只是中等偏上水平。 (但并非真正的专家)
首先,前端是困难的。并非完全从技术角度,尽管如果你没有妥善规划,确实可能搞砸,然后背上沉重的技术债。
问题在于用户,以及你必须直观地“解释”你的应用程序是做什么的。用户是懒惰、愚蠢、友善、邪恶,同时又超级聪明的。如今你不能指望他们去读手册。
选择什么真正取决于你需要构建什么。这会是公开的吗?我的意思是,你的动态内容需要SEO吗?如果不需要,我强烈推荐CSR。(即使LLM说SSR)
个人观点:
- 我的技术栈是React Vite,我对此非常满意。(但对Svelte没有经验)
- 我不推荐HTMX。我发过关于这个的帖子。我认为这只是一阵炒作。
- 由于遗留原因,SSR很多时候只是停留在人们的观念里。我将前端视为一个真正的应用程序,因为每个浏览器都有很棒的功能,每台设备实际上都是一台超级计算机。后端则是一个API。
在后端,Go是完美的,每次我有机会使用它时都感到非常高兴。你会做得很好的。 我交谈过的每个人,一旦认真尝试过Go,都会感到满意。 希望能帮到你。
在SSR架构中,Go和SvelteKit可以明确分工:Go作为API服务层,SvelteKit作为SSR渲染层。以下是典型的技术栈结构:
// Go后端API示例 (main.go)
package main
import (
"encoding/json"
"net/http"
)
type Shipment struct {
ID string `json:"id"`
Status string `json:"status"`
}
func main() {
http.HandleFunc("/api/shipments", func(w http.ResponseWriter, r *http.Request) {
shipments := []Shipment{
{ID: "SHP001", Status: "In Transit"},
{ID: "SHP002", Status: "Delivered"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(shipments)
})
http.ListenAndServe(":8080", nil)
}
// SvelteKit页面加载数据示例 (+page.server.js)
export async function load({ fetch }) {
const response = await fetch('http://localhost:8080/api/shipments')
const shipments = await response.json()
return { shipments }
}
<!-- SvelteKit页面组件示例 (+page.svelte) -->
<script>
export let data
let shipments = data.shipments
</script>
<h1>运输管理</h1>
{#each shipments as shipment}
<div>
<strong>{shipment.id}</strong>: {shipment.status}
</div>
{/each}
文件结构:
project/
├── backend/ # Go服务
│ ├── main.go
│ └── go.mod
└── frontend/ # SvelteKit应用
├── src/
│ ├── routes/
│ │ ├── +page.server.js
│ │ └── +page.svelte
│ └── app.html
├── package.json
└── svelte.config.js
在这种架构中,Go处理业务逻辑和数据持久化,通过REST API提供数据。SvelteKit负责:
- 服务器端渲染初始页面
- 客户端路由和交互
- 调用Go API获取数据
部署时,可以将SvelteKit构建为独立的Node.js服务,或者使用SvelteKit的adapter-node适配器与Go服务部署在同一服务器上。这种分离确保了Go专注于高性能后端处理,而SvelteKit处理UI渲染和客户端交互。
Dean_Davidson:
我通常的做法是保持简单,让其自然发展!我认为这与RSC的这条评论是一致的:
啊,是的,我已经给这个仓库点了星标,并且一直在研究它。这个也很有趣,同样适用于我的用例:https://github.com/Melkeydev/go-blueprint
Dean_Davidson:
大多数时候,当我选择使用SPA时,是针对那些数据驱动性强、公开界面很少的应用(除了登录/忘记密码等)。在某些情况下,SSR可以提供帮助,但服务器必须知道如何从数据库渲染更复杂的内容(比如报告之类的)。
这正是TMS(运输管理系统)的样子,它的大部分内容由实时变化的货运数据组成,并且需要在不刷新页面的情况下实时更新(我假设Websockets可以解决这个问题?)。另一半则是与其他第三方货运软件(卡车远程信息处理、从托运人ERP接收货物招标、与拍卖板集成)的API集成。我喜欢Astro,我一直在用它为我的不同家庭成员创建小型商业网站,但我还没有看到它能扩展到满足我们需求的实例。最终,我将把这个平台开源,我的计划是通过在我构建的服务器基础设施上提供托管服务来将其货币化。到今年年底,用户数量可能超过一千。
Dean_Davidson:
在贸易展上展出(今年在CES上展出过)。
太棒了!感谢分享,我会去看看这个。
Dean_Davidson:
httprouter 是Go的默认路由器,这个说法准确吗?也许我搞混了。经过大量研究并且不知道自己在做什么之后,我将其范围缩小到httprouter、Gin或Echo;但决定选择Echo,因为我一直被告知它简单且兼容性好(与我听说人们很喜欢的Fiber相比,Fiber是基于fasthttp而不是net/http构建的)。
我不知道,我重复了很多我读到的东西,而且我为自己写的代码越多,我就越发现自己喜欢什么;但有一个真理永远不会改变,那就是我更喜欢尽可能简单,同时不牺牲用户体验和生产力。
还有一个问题——作为90年代过来的人,我喜欢一个好的桌面程序(我拒绝称它们为应用程序,《黑客帝国》里不这么叫,你也不应该!)。这不是优先事项,但最终,我想做一个这个的桌面版本(不是Electron)。这不能用那个叫Wails的东西来完成吗?如果可以的话,我想知道这里是否有人对这个包有任何经验。
再次感谢大家。
引用自 fakebizprez:
@Dean_Davidson – 关于你的帖子,我能否保留我的 Go + Supabase 后端,而仅仅用 Svelte 以客户端渲染(CSR)的方式编写前端?我的理解是 Svelte 是一个编译器,会直接将代码转换为 JavaScript;从而绕过了虚拟 DOM 的概念。
是的。可能最简单的方法是:使用 Svelte 构建你的应用(这会生成 JS/HTML 文件),然后通过你的 Go 可执行程序来提供这些静态内容。我经常使用类似的方法。在部署时,让你的前端框架编译到 ./public 或类似的目录,然后像这样提供静态内容:
// 提供我们的静态 js/html/图片:
http.Handle("/", http.FileServer(http.Dir("public")))
客户端渲染(CSR)唯一需要注意的“陷阱”是:用户可能直接访问你应用中的任何页面,或者点击刷新按钮,因此你需要有一个后备方案,在“未找到”时提供 index.html,或者跟踪路由,当用户访问像 yourapp.com/users/1: 这样的路由时也提供 index.html:
// 对于所有 SPA 路由,我们也提供 /index。
// 在此注册任何前端路由
spaRoutes := []string{
"/home",
"/faq",
"/login",
"/users/{id}"}
for _, route := range spaRoutes {
http.HandleFunc(fmt.Sprintf("GET %v", route), indexHandler)
}
其中 indexHandler 只是提供 index 文件:
func indexHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./public/index.html")
}
当然也有其他方法可以实现这一点,但这可能是最简单的。
关于你的 API 以及它如何与你的 Web 应用交互:你只需要创建返回 JSON 的路由,你的 Svelte 应用就会根据你的 RESTful API 更新 UI。重申一下——我不是 Svelte 开发者,但大概是这样的:
// main.go 中设置路由的地方
http.HandleFunc("POST /api/items", getItemsHandler)
//...
func getItemsHandler(w http.ResponseWriter, r *http.Request) {
// 从某处获取项目
items := getItems()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items)
}
在 Svelte 中创建一个加载函数(或类似功能):
// page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, params }) => {
const res = await fetch(`/api/items`);
const items = await res.json();
return { items: items };
};
然后在你的 .svelte 文件中:
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<h1>Items</h1>
<ul>
{#each data.items as item}
<li>{item}</li>
{/each}
</ul>
我不太了解 Svelte 世界中的虚拟 DOM。框架使用虚拟 DOM 是因为直接操作 DOM 开销很大。通过跟踪变化,你可以在需要时才更新 DOM。所以,我不完全确定 Svelte 是如何管理其 DOM 操作的,但我觉得不必过于担心这一点。
falco467:
对于大多数公共网站,我使用 Atro 作为 Svelte 的构建工具。
我喜欢 Astro。用它做网站既有趣又简单。
falco467:
你的 Svelte 应用没有理由必须是单页应用。我使用 Go 后端和 Svelte 前端进行开发已经多年了——我通常采用多页面方案。如果你需要多个页面,直接提供多个 HTML 文件即可;如果需要在客户端保持一些持久状态,可以很容易地通过 session-storage 在页面间共享。
你的方法看起来很棒,但你选择在 Svelte 中使用多页面方案而非单页应用的理由是什么?
多页面方案
├── backend/
│ ├── cmd/server/main.go # Go API 服务器
│ ├── internal/
│ │ ├── handlers/ # HTTP 处理器
│ │ ├── models/ # 数据库模型
│ │ └── services/ # 业务逻辑
│ └── go.mod
├── frontend/
│ ├── src/
│ │ ├── pages/ # 每个页面一个目录
│ │ │ ├── loads/
│ │ │ │ ├── index.ts # 货运页面的入口
│ │ │ │ ├── LoadBoard.svelte # 主组件
│ │ │ │ ├── components/ # 页面组件
│ │ │ │ └── api.ts # 货运页面的 API 客户端
│ │ │ └── customers/
│ │ ├── components/ # 共享组件
│ │ └── lib/ # 共享工具
│ ├── public/ # 静态资源
│ ├── vite.config.ts # Vite 配置
│ ├── tsconfig.json # TypeScript 配置
│ └── package.json
└── docker-compose.yml # 开发环境
对比
单页应用
├── backend/
│ ├── cmd/server/main.go # Go API 服务器
│ ├── internal/
│ │ ├── handlers/ # HTTP/API 处理器
│ │ ├── models/ # 数据库模型
│ │ └── services/ # 业务逻辑
│ └── go.mod
├── frontend/
│ ├── src/
│ │ ├── App.svelte # 应用容器
│ │ ├── main.ts # 应用入口
│ │ ├── routes/ # 客户端路由
│ │ │ ├── Loads.svelte # 货运看板页面
│ │ │ └── Customers.svelte # 客户页面
│ │ ├── components/ # 共享组件
│ │ ├── stores/ # 全局状态
│ │ │ ├── loads.ts # 货运状态
│ │ │ └── auth.ts # 认证状态
│ │ └── lib/ # 共享工具
│ ├── public/ # 静态资源
│ ├── vite.config.ts # Vite 配置
│ ├── tsconfig.json # TypeScript 配置
│ └── package.json
└── docker-compose.yml # 开发环境
这个结构看起来准确吗?目前,我的主页/主仪表板是用 Svelte、HTMX 和 React 编写的。我正在尝试推进这个项目,同时不违背“保持简单,傻瓜”的原则。
我见过一些单页应用,在页面间跳转时会导致一些难以发现的细微错误。
有道理。这主要是一个设计缺陷,因为共享状态通常不是一个好主意,除非它作为不可变值随时间推移而发出。单页应用可能发生的另一个糟糕问题是内存泄漏,但大多数现代框架都让这种情况变得不那么容易发生。不过,这确实是一个完全合理的批评。
首次加载性能:Astro 会将每个单独的页面静态构建成预渲染的 HTML。
我认为我们可能是在构建不同类型的应用,所以需要不同的工具。大多数情况下,我选择单页应用是为了构建数据驱动性强、公开页面很少(除了登录/忘记密码等)的应用。在某些情况下,服务器端渲染可能有所帮助,但服务器必须知道如何从数据库渲染更复杂的内容(比如报告之类的),而且通常我处理的数据量很大,所以无论如何我都需要对用户界面进行虚拟化。
总之,我还没用过 Astro,但看了一下,它看起来非常酷。
后端文件结构会是什么样子,还是用那个假设的例子来说?
我通常的做法是保持简单,让它自然发展!我认为这与RSC的这条评论是一致的:
例如,Go 生态系统中绝大多数包不会将可导入的包放在
pkg子目录中。更一般地说,这里描述的结构非常复杂,而 Go 仓库往往要简单得多。
根据职责分离来考虑你的包,通常能带来清晰的设计。如果我有多个可执行文件,我会创建一个 /cmd 目录;如果没有,我就把单一可执行文件的逻辑放在顶层的 main.go 里。
老实说,对于私有仓库,我并不太担心别人导入我的包。我做的很多工作都只由内部团队处理;所以我通常只对开源项目和/或我希望其他开发者会使用我的包的情况(比如其他团队使用的内部工具)才考虑使用 /internal。
考虑到这一点,以下是我最近做的一个项目的简化版结构。这是一个非常小/简单的项目,所以我采用了简单的布局:
# 我喜欢将数据访问层主要包含在一个数据文件夹中,
# 但我通常喜欢将模型和查询分开。
# 这使得导入时能清楚知道你在使用什么。
# 很明显这是一个 User 类型的模型:
# loggedInUser := models.User{}
# 很明显这是一个查询:
# resp := queries.GetReportData(myParams)
/data
/models
user.go
user_test.go
other.go #...
/queries
queries.go # 通用查询和包文档
reporting.go # 根据需要拆分
reporting_test.go
/mail # 这个应用通过 SMTP 发送邮件
mail.go
mail_test.go
/github # 这个应用集成了 GitHub
github.go
github_test.go
/slack # 这个应用也发送 Slack 通知
slack.go
slack_test.go
auth.go # 认证逻辑
config.go # 应用配置
main.go # 我们的服务器
总之,就是这样。这个项目最初是为一个新客户做的一个一次性概念验证(我一开始真的只有一个 main.go,作为唯一的开发者),现在它已经投入生产,并在贸易展会上展出(今年就在 CES 上)。这完美吗?当然不。但我会根据需要不断重构它,如果包变得过于庞大,就拆分它们。
所有这些,兜了一圈,是想说:从简单开始,并尝试根据包将如何被使用来考虑好的包名!mail.SendLoginNotication 很容易理解。github.CreateIssue、slack.NotifyBugReport 等也是如此。如果你还没读过,Effective Go 的“包名”部分很棒:
Effective Go - The Go Programming Language
也看看 Go 生态系统中这个非常流行的项目:
GitHub - julienschmidt/httprouter: A high performance HTTP request router that scales…
没有比这更简单的了。保持简单,直到你无法再简单为止!不过这只是我的一点浅见。


