PAYATHON 2026

SvelteKit 使用 use:enhance 提交表单后仍刷新页面

排障赵师傅

结论

db.em.flush() 不会让 SvelteKit 的 <form use:enhance> 退化成普通表单提交。根据这些现象,项目很可能使用了 SQLite 一类的本地文件数据库,而数据库文件又处在 Vite 的监听范围内。

字段没有变化时,MikroORM 通常不会真正写入数据库。字段发生变化后,flush() 会修改数据库文件,开发环境中的 Vite 检测到文件变化,随即触发整页刷新,于是看起来像是 use:enhance 失效了。

如果生产环境也能复现,需要继续检查 action 的响应、重定向、异常和前端代码。生产服务器通常没有 Vite 文件监听,刷新可能由其他原因引起。

为什么修改字段后才会刷新

整个过程通常是这样的:

  1. 字段没有变化时,MikroORM 的 Unit of Work 中没有需要持久化的内容。
  2. flush() 虽然执行了,数据库文件却不一定发生变化。
  3. 字段修改后,flush() 会执行 UPDATE,从而写入 SQLite 主文件、WAL 文件或 journal 文件。
  4. 如果这些文件位于项目目录中,Vite 的文件监听器可能会把它们当作项目资源。
  5. 这类文件无法通过 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 监听到数据库文件变化仍然是最可能的原因。

备注:内容仅供参考。