svelte5:一个更糟糕的 vue3
本文最后更新于:2025年3月9日 凌晨
背景
svelte5 在去年 10 月发布,据说是 svelte 发布以来最好的版本。其中,他们主要为之自豪的是 runes,这是一个基于 proxy 实现的一个反应式状态系统。但经过 vue3 composition api 和 solidjs signals,吾辈并未感到兴奋。相反,这篇博客将主要针对吾辈在实际项目中遇到的 svelte5 问题进行说明,如果你非常喜欢 svelte5,现在就可以关闭页面了。
runes 只能在 svelte 组件或者 .svelte.ts 文件中
例如,当吾辈像 react/vue 一样用 runes 编写一个 hook,例如 useCounter
1 |
|
但这个函数不能放在普通的 .ts 文件,必须使用 .svelte.ts 后缀并让 @sveltejs/vite-plugin-svelte 编译这个文件中的 runes 代码,否则你会得到 $state is not defined
。这也包括 unit test,如果想要使用 runes(通常是测试 hooks/svelte component),文件名也必须以 .svelte.test.ts 结尾,真是糟糕的代码感染。
使用 runes 编写 hooks 传递或返回 runes 状态须用函数包裹
看到上面的 useCounter 中返回的值被放在 get value 里面了吗?那是因为必须这样做,例如如果尝试编写一个 hooks 并直接返回 runes 的状态,不管是 $state
还是 $derived
,都必须用函数包裹传递来“保持 reaction”,否则你会得到一个错误指向 https://svelte.dev/docs/svelte/compiler-warnings#state_referenced_locally。当然这也包括函数参数,看到其中的讽刺了吗?
1 |
|
当然,你不能直接返回 { now }
而必须使用 get/set 包裹,svelte5 喜欢让人写更多模版代码。
1 |
|
- https://github.com/sveltejs/svelte/discussions/15264
- https://github.com/TanStack/query/pull/6981#issuecomment-2002611355
class 是 runes 一等公民,或许不是?
哦,当吾辈说必须使用函数包裹 runes 状态时,显然 svelte 团队为自己留了逃生舱口,那就是 class。检查下面这段代码,它直接返回了 class 的实例,而且正常工作!如果你去查看 sveltekit 的官方代码,他们甚至将 class 的声明和创建放在了一起:https://github.com/sveltejs/kit/blob/3bab7e3eea4dda6ec485d671803709b70852f28b/packages/kit/src/runtime/client/state.svelte.js#L31-L40
1 |
|
显然,它不能应用于普通的 js object 上,不需要等到运行,在编译阶段就会爆炸。
1 |
|
最后,让我们看看 $state 是否可以将整个 class 变成响应式的?
1 |
|
当然不行,像 mobx 一样检测字段 class field 并将其变成响应式的显然太难了。然而,有趣的是,在这里你可以使用普通的 js 对象了。当然,当然。。。
1 |
|
印象中这几种写法在 vue3 中都可以正常工作,看起来怪癖更少一点。
svelte 模板包含一些无法使用 js 实现特定功能
就像 svelte 官方文档中说明的一样,在测试中无法使用 bindable props,因为它是一个模版的专用功能,无法在 js 中使用,必须通过额外的组件将 bindable props 转换为 svelte/store writable props,因为它可以在 svelte 组件测试中使用。
1 |
|
当想要测试这个包含 bindable props 的组件时,必须编写一个包装组件,类似这样。
1 |
|
单元测试代码
1 |
|
是说,有办法像是 vue3 一样动态绑定多个 bindable props 吗?
1 |
|
不,它甚至没有提供逃生舱口,所以无法实现某种通用的 Bindable2Writable 高阶组件来将 bindable props 自动转换为 writable props,这是非常愚蠢的,尤其是 vue3 已经珠玉在前的前提下,svelte5 的实现如此糟糕简直难以理解。
表单组件默认是非受控的,有时候会带来麻烦
对于下面这样一个组件,它只是一个双向绑定的 checkbox,很简单。
1 |
|
那么,如果去掉 bind 呢?单向绑定?不,它只是设置了初始值,然后就由 input 的内部状态控制了,而不是预期中的不再改变。观看 3s 演示
https://x.com/rxliuli/status/1896856626050855298/video/3
当然,这不是 svelte 的问题,除了 react 之外,其他 web 框架的单向数据流似乎在遇到表单时都会出现例外。
生态系统太小
这点是所有新框架都避免不了的,但在 svelte 却特别严重,包括
- router: 支持 memory spa 的 router,在 1 月初的时候找不到适配了 svelte5 的 https://github.com/ItalyPaleAle/svelte-spa-router/issues/318
- query: tanstack query 也支持 svelte,但截至今日 svelte5 支持仍未发布 https://github.com/TanStack/query/discussions/7413
- shadcn/ui: 同样的,svelte 中也有 shadcn 实现,但对 shadow dom 的支持很糟糕,不得不修改了内部引用的 svelte-toolbelt 库 https://github.com/huntabyte/bits-ui/issues/828
- table/form: 两个最复杂的 ui 组件没有找到合适的,tanstack table 的 api 很糟糕
- virtual list: 找不到支持可变行高/列宽的 list/grid 的库
- chart: 自行引入 echarts 集成,给一个现有的 svelte-echarts 包提 pr 支持了 svelte5,但作者还未正式发布 https://github.com/bherbruck/svelte-echarts/pull/34
社区反应
每当有人抱怨 svelte5 变得更复杂时,社区总有人说你是用 svelte5 编写 hello world 的新手、或者说你可以继续锚定到 svelte4。首先,第一点,像吾辈这样曾经使用过 react/vue 的人而言,svelte4 看起来很简单,吾辈已经用 svelte4 构建了一些程序,它们并不是 hello world,事实上,可能有 10k+ 行的纯代码。其次,锚定到旧版本对于个人是不可能的,当你开始一个新项目的时候,几乎没有办法锚定到旧版本,因为生态系统中的一切都在追求新版本,旧版本的资源很难找到。
就在吾辈发布完这篇博客之后,立刻有人以 “Svelte’s reactivity doesn’t exist at runtime” 进行辩护,而在 svelte5 中,这甚至不是一个站得住脚的论点。当然,他获得了 5 个 👍,而吾辈得到了一个 👎。
https://www.reddit.com/r/sveltejs/comments/1j6ayaf/comment/mgnctgm/
总结
svelte5 变得更好了吗?显然,runes 让它与 react/vue 看起来更像了一点,但目前仍然有非常多的边界情况和怪癖,下个项目可能会考虑认真使用 solidjs,吾辈已经使用 react/vue 太久了,还是想要找点新的东西看看。