SvelteKit 使用 use:enhance 提交表单后仍刷新页面
结论
db.em.flush() 不会让 SvelteKit 的 <form use:enhance> 退化成普通表单提交。根据这些现象,项目很可能使用了 SQLite 一类的本地文件数据库,而数据库文件又处在 Vite 的监听范围内。
字段没有变化时,MikroORM 通常不会真正写入数据库。字段发生变化后,flush() 会修改数据库文件,开发环境中的 Vite 检测到文件变化,随即触发整页刷新,于是看起来像是 use:enhance 失效了。
如果生产环境也能复现,需要继续检查 action 的响应、重定向、异常和前端代码。生产服务器通常没有 Vite 文件监听,刷新可能由其他原因引起。
为什么修改字段后才会刷新
整个过程通常是这样的:
- 字段没有变化时,MikroORM 的 Unit of Work 中没有需要持久化的内容。
flush()虽然执行了,数据库文件却不一定发生变化。- 字段修改后,
flush()会执行UPDATE,从而写入 SQLite 主文件、WAL 文件或 journal 文件。 - 如果这些文件位于项目目录中,Vite 的文件监听器可能会把它们当作项目资源。
- 这类文件无法通过 HMR 局部更新,因此 Vite 会整页刷新。
触发刷新的通常是数据库文件变化。浏览器并不会直接响应 flush()。
另外,flush() 的作用是把 MikroORM Unit of Work 中累积的变化同步到数据库。它会不会创建或提交事务,要看当前的事务上下文和 MikroORM 配置,不能简单等同于一次独立的 “commit transaction” 操作。
确认是否由 Vite 文件监听触发
可以依次检查下面几项。
1. 查看浏览器控制台
在开发环境中提交表单,观察控制台是否出现类似信息:
[vite] page reload ...
如果出现这条信息,基本可以确定刷新来自 Vite。
2. 查看 Network 面板
打开浏览器开发者工具,在 Network 面板启用 Preserve log,然后再次提交表单:
- action 请求结束后又出现新的
document请求,说明浏览器确实发生了页面导航或刷新。 - 没有新的
document请求,只重新请求了页面数据,通常是 SvelteKit 的数据失效更新,不是整页刷新。
3. 检查数据库文件的位置和变化
检查项目目录中是否存在以下文件:
*.db
*.sqlite
*.sqlite3
*.db-wal
*.db-shm
*.db-journal
比较提交前后的文件修改时间。如果字段修改并执行 flush() 后,文件时间发生变化,页面也同时刷新,基本就能确定问题来自 Vite 的文件监听。
推荐修复:把数据库文件移出项目监听目录
运行时数据库最好放在项目目录之外,至少不要放进 src、static 或其他源码目录。
例如,可以通过环境变量配置绝对路径:
DATABASE_URL=file:/var/lib/my-app/app.sqlite
本地开发时也可以使用项目外的专用数据目录。
尤其不要把数据库放在 static 中。该目录下的文件可能会被当作公开静态资源处理,既会触发刷新,也有数据泄露风险。
备选修复:让 Vite 忽略数据库文件
如果开发时必须把数据库保存在项目目录中,可以在 vite.config.ts 中排除相关文件:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {
watch: {
ignored: [
'**/*.db',
'**/*.db-*',
'**/*.sqlite',
'**/*.sqlite-*',
'**/*.sqlite3',
'**/*.sqlite3-*'
]
}
}
});
修改配置后,需要重启开发服务器。
如果数据库统一保存在单独的目录中,也可以直接忽略整个目录:
export default defineConfig({
plugins: [sveltekit()],
server: {
watch: {
ignored: ['**/.data/**']
}
}
});
只把数据库文件加入 .gitignore 通常没有作用,因为 Git 的忽略规则不会改变 Vite 的监听规则。
use:enhance 可以保留原有写法
客户端代码仍然可以使用 update({ reset: false }):
<script lang="ts">
import { enhance } from '$app/forms';
</script>
<form
method="POST"
action="?/updateThings"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}
>
<input
type="text"
name="thingsJsonString"
bind:value={thingsJsonString}
/>
<button type="submit">Submit</button>
</form>
reset: false 只决定 action 成功后是否重置表单,无法阻止 Vite 因文件变化而触发整页刷新。
update() 成功后,默认还会让页面数据失效,并重新运行相关的 load。如果界面只是重新渲染,滚动位置和浏览器文档都没有重新加载,看到的可能是数据失效更新。可以临时关闭自动失效来验证:
use:enhance={() => {
return async ({ update }) => {
await update({
reset: false,
invalidateAll: false
});
};
}}
不要为了掩盖刷新问题而长期关闭必要的数据失效。确认数据库文件触发了 Vite 刷新后,还是应该调整数据库位置或 Vite 的监听配置。
服务端 action 的建议写法
服务端应从 FormData 中读取字段,并在修改实体后执行 flush():
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
updateThings: async ({ request }) => {
try {
const data = await request.formData();
const formValue = data.get('thingsJsonString');
if (typeof formValue !== 'string') {
return fail(400, {
success: false,
error: 'Missing thingsJsonString'
});
}
const things = JSON.parse(formValue) as ThingInfo[];
for (const thing of things) {
const thingEntity = await db.thing.findOne(thing.id);
if (thingEntity) {
thingEntity.infoString = JSON.stringify(thing);
}
}
await db.em.flush();
return { success: true };
} catch (error) {
return fail(500, {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
} satisfies Actions;
删除 flush() 后页面可能不再刷新,因为数据库没有发生写入,但修改的数据也不会真正持久化,所以这不能解决问题。
如果生产环境也会刷新
部署后仍然发生整页刷新时,需要检查以下情况:
- action 是否返回了
redirect(...)。 flush()是否抛出异常,导致代理或错误处理程序改写响应。- action 响应是否仍是 SvelteKit 能识别的 action result。
- 浏览器控制台是否出现 JavaScript 异常。
- 成功后是否有代码调用
location.reload()、goto()或修改window.location。 - 反向代理是否把 action 请求重定向到了登录页或错误页。
- 表单是否在 hydration 完成前提交,或者客户端增强代码没有成功加载。
可以在 action 中记录 flush() 执行前后的位置:
console.log('before flush');
await db.em.flush();
console.log('after flush');
再结合 Network 面板判断是 action 请求失败、SvelteKit 重新运行了 load,还是 Vite 发起了整页刷新。按照题目描述的条件差异,Vite 监听到数据库文件变化仍然是最可能的原因。
备注:内容仅供参考。
