如意 Django CRM 前端架构复盘:SvelteKit 如何重塑会话、路由与交付边界

OK,OK,大家好,欢迎大家来到大鹏 AI 教育,我是张大鹏。

给 Django 项目增加一个 SvelteKit 前端,很容易被描述成“换了更现代的界面”。

但对 CRM 这类长期演进的系统来说,界面技术只是表面,真正影响维护成本的是工程边界。

请求先经过谁、会话由谁续期、组织上下文在哪里补齐、前端以什么形态交付,才是这次架构复盘要回答的问题。

如意 Django CRM 已经把 SvelteKit 放到了这些边界上。

这篇文章不讨论框架热度,只沿着当前实现判断它接管了什么、带来了什么,又增加了哪些长期治理成本。

一、为什么要把前端变成独立工程边界

独立前端首先改变职责分配,然后才改变页面开发体验。

1.1 独立前端改变的是职责,不只是页面技术

在这套架构里,浏览器不直接承担全部会话判断,Django 也不负责输出最终页面。

三者形成了清晰的协作关系。

先沿着图中的箭头观察一次请求。

哪些请求可以由 SvelteKit 提前处理,哪些决定必须继续交给 Django?

图中可以直接辨认三层职责。

  • 🌐浏览器交互:浏览器负责交互、导航和客户端状态,不负责裁决业务数据权限。
  • 🧭页面交付:SvelteKit 负责服务端渲染、请求入口、会话衔接和页面交付。
  • 🛡️业务授权:Django API 负责身份认证、组织隔离、业务规则和数据权限。

我在判断前后端分离是否健康时,首先检查的不是框架名称,而是同一个决定有没有被两层同时负责。

顺着这张图,可以形成三个边界判断。

  • 🧱边界独立:前端的构建和运行过程已经不再依附 Django 模板系统。
  • 🔀责任分流:页面跳转可以在前端入口处理,资源授权必须回到 Django 完成。
  • 结论可验:package.jsonadapter-node和 Compose 服务定义共同证明前端是独立服务。

前端的package.json使用 Svelte 5、SvelteKit 和adapter-node

docker-compose.yml又把前端与 Django 后端定义为两个独立服务。

因此,“独立前端”的核心价值不是把 HTML 移到另一个目录,而是让页面交付和业务授权各自拥有明确负责人。

1.2 handle 钩子成为每次请求的统一入口

SvelteKit 官方把 hooks 定义为应用级函数,其中服务端handle会在服务器收到动态请求时运行。

项目正是利用这个入口,把三段处理按顺序组合起来。

exportconsthandle=sequence(Sentry.sentryHandle(),paraglideHandle,authHandle);

这段代码的处理顺序可以拆成三个入口职责。

  • 🚪异常入口:Sentry 包住动态请求,让服务端异常进入统一采集链路。
  • 🌍语言入口:Paraglide 确定请求语言和文字方向,避免页面各自判断语言环境。
  • 🔐会话入口:authHandle读取 cookie、整理会话并决定当前请求是否需要重定向。

页面因此不需要各自复制同一套入口判断。

框架能力仍有边界。

静态资源和已经预渲染的页面不会像普通动态请求一样经过这条链路,入口判断也不能代替 Django 的资源授权。

完整行为可以查看 https://svelte.dev/docs/kit/hooks。

二、会话与组织上下文如何进入统一请求管道

多组织 CRM 的会话不是一个布尔值,而是一组需要持续保持一致的身份状态。

2.1 本地读取 JWT 只能判断声明与过期时间

hooks.server.js中的decodeJwtPayload会拆分 JWT,对 payload 做 Base64URL 解码,再读取exporg_id等声明。

这样可以快速判断令牌是否具备当前请求需要的上下文,避免每个页面重复解析。

本地读取能够提供的信息只有三类。

  • 🪪声明读取:前端可以读取令牌携带的用户和组织声明,用于组织页面流程。
  • 时间判断:前端可以比较exp与当前时间,决定是否尝试刷新令牌。
  • ⚠️信任限制:没有签名校验的 payload 不能成为可信身份或数据授权依据。

文件中的verifyTokenLocally这个名字容易让人误以为它完成了完整认证。

实际上,它只确认格式可解析并且令牌没有超过过期时间。

攻击者可以自行构造 payload,因此 SvelteKit 只能据此组织页面流程,Django 仍必须验证令牌并执行权限检查。

2.2 单飞刷新避免同一令牌并发续期

一个页面加载时往往会并发请求多组数据。

如果这些请求同时发现 access token 过期,又分别使用同一个 refresh token 刷新,就可能重复消费一次性令牌。

项目使用refreshesInFlightMap 收敛这个窗口。

先看图中三条请求怎样汇入同一个刷新任务。

为什么刷新完成以后还要重新分流,而不是只返回一个统一响应?

图中的主路径可以拆成三个动作。

  • 🧵请求汇合:第一个过期请求创建刷新 Promise,后续相同令牌的请求复用它。
  • ♻️单次刷新:同一个 refresh token 在当前进程中只进入一次后端刷新调用。
  • 📤结果分发:刷新完成后,等待者取得同一个新令牌结果,再继续各自原来的请求。

我在处理令牌轮换时,会同时检查“只刷新一次”和“每个原请求都能继续”,缺少任何一半都不算完整单飞。

这张图还能给出三个适用边界。

  • 🔒作用范围:Map 减少的是当前 Node 进程内部的重复刷新窗口。
  • 📥证据范围:当前源码能够证明 Promise 复用,但还没有并发压力测试证明竞态已经消失。
  • 🧹清理责任:请求结束后必须移除 Map 项,否则旧刷新结果会变成长期脏状态。

因此,本文只把它表述为代码层面的单飞设计,不扩大成“并发竞态已经被运行验证”。

2.3 组织切换同时更新令牌并退休旧会话

多租户 CRM 的会话还要回答“当前代表哪个组织”。

当 org cookie 与 access token 中的组织声明不一致时,服务端会调用/auth/switch-org/

这个过程包含三个需要保持一致的动作。

  • 🏢组织确认:SvelteKit 根据当前 cookie 和令牌声明判断是否需要切换组织上下文。
  • 🔄令牌替换:后端返回新令牌后,服务端同时更新jwt_accessjwt_refreshorgcookie。
  • 🗑️旧会话退休:请求体携带即将被替换的 refresh token,让旧组织上下文进入失效流程。

前端代码只能证明它发起切换并提交旧令牌。

真正的成员关系校验和令牌失效仍由后端完成。

切换失败、组织不存在或用户尚未选择组织时,请求必须进入登录或组织选择分支。

三、路由与 API 边界如何变得可预测

统一入口的价值,在于新页面默认进入可解释的访问规则,而不是复制旧页面的偶然判断。

3.1 三类路由形成默认拒绝的访问矩阵

项目没有在每个页面里分别判断身份,而是在authHandle中把入口分成三类。

从左到右比较图中的三列。

为什么没有被显式列出的业务页面,反而会进入最严格的组织路由?

矩阵中的三类入口分别承担不同规则。

  • 🟢公共路由:/login/logout/bounce/portal/csat不要求已有会话。
  • 🟡登录路由:/org只要求有效会话,用于继续选择或确认组织。
  • 🔴组织路由:其余页面默认同时要求有效 JWT 和组织上下文。

我更倾向这种“显式放宽、默认收紧”的写法,因为新增页面不会因为忘记加入保护名单而自动公开。

不过,矩阵只能管理页面入口,不能替代资源权限。

  • 🔎默认分支:新增业务页面天然落入组织上下文要求更严格的路径。
  • 🧩职责边界:SvelteKit 决定页面能否进入,Django 决定具体资源能否访问。
  • 🚧失败边界:页面成功打开不代表用户已经获得某条客户或商机数据的操作权。

因此,这套矩阵提升的是入口行为的可预测性,而不是把后端权限迁移到前端。

3.2 服务端和浏览器不能共用同一个 API 地址

同一段前端代码可能运行在两个网络世界里。

SvelteKit 服务端位于 Docker 网络中,可以通过服务名访问 Django。

浏览器位于用户设备上,只能访问对外暴露的地址。

两套地址分别服务不同运行位置。

  • 🐳容器地址:DJANGO_API_BASE_URL供 server hooks 和 server load 在内部网络中访问后端。
  • 🖥️浏览器地址:PUBLIC_DJANGO_API_URL会进入客户端构建产物,必须能从用户设备访问。
  • 🔗契约一致:两个地址可以不同,但必须指向语义一致的 Django API 契约。

把二者都配置成http://backend:8000或都配置成localhost,必然有一侧无法工作。

这不是配置重复,而是运行位置不同的结果。

同类边界可以参考 https://svelte.dev/docs/kit/hooks#handleFetch。

3.3 一个 Django API 可以服务多个客户端,但契约必须稳定

仓库同时存在 Web 前端、Flutter 移动端和 MCP 服务边界。

它们可以复用 Django API 的认证与业务能力,但这项收益属于后端契约,不属于 SvelteKit 的自动能力。

多客户端复用至少依赖三个条件。

  • 📱移动客户端:Flutter 可以适配移动交互,但不应重新发明组织与权限语义。
  • 🤖智能体入口:MCP 可以复用业务能力,但必须服从同一套身份和资源授权规则。
  • 📜稳定契约:状态码、错误结构、组织上下文和资源权限需要由后端统一维护。

SvelteKit 让 Web 端更容易集中处理这些契约。

如果后端错误模型频繁漂移,多客户端只会同时承受更高的适配成本。

四、交付链路如何从页面构建升级为服务治理

独立前端交付的不再是一组页面文件,而是一项需要构建、运行、监控和持续治理的服务。

4.1 国际化检查进入 check 和 build 命令

项目没有把国际化停留在“页面能切换语言”。

package.json将翻译检查、静态文案审计、Paraglide 编译和 Svelte 类型检查组合进标准命令。

这条交付链路包含三个层次。

  • 🌏翻译资源:Paraglide 编译把消息资源转换成前端能够使用的生成物。
  • 🧪交付检查:翻译审计和 Svelte 检查在构建前暴露缺失资源与类型问题。
  • 🏗️生产构建:build先编译消息,再交给 Vite 生成 Node 服务产物。

命令通过只能证明规则和构建在当前环境中完成。

它不能证明每种语言的业务文案都经过人工校对,也不能代替真实浏览器中的布局检查。

具体集成机制可以查看 https://paraglidejs.com/sveltekit。

4.2 可观测性增强也扩大了个人信息治理责任

客户端和服务端 hooks 都接入了 Sentry,异常可以沿 SvelteKit 生命周期统一采集。

客户端还启用了 Replay,并设置了sendDefaultPii: true

这类配置必须同时检查价值和数据责任。

  • 👁️采集范围:用户标识、请求头、页面输入和回放内容都可能进入监控链路。
  • 🧼发送过滤:敏感字段需要在发送前脱敏或删除,而不是事后依赖人工判断。
  • 🕵️环境策略:开发、测试和生产环境应该分别配置采样率、保留时间和访问权限。

当前代码能够证明 SDK 与配置已经存在。

它不能证明告警通知、脱敏策略或真实排障效果已经完成验收。

数据范围说明可以继续核对 https://docs.sentry.io/platforms/javascript/data-management/data-collected/。

4.3 adapter-node 和 Docker 把前端交付为独立服务

SvelteKit 使用adapter-node后,构建结果是可由 Node 启动的独立服务。

前端 Dockerfile 负责安装依赖、执行构建并运行产物,Compose 再把它与 Django、PostgreSQL 和 Redis 放进同一套编排。

沿着图中的顺时针闭环检查一次交付过程。

为什么“本地页面能够打开”不能证明这条交付链路已经完整?

图中的五个节点可以归纳成三个治理阶段。

  • 📦构建输入:翻译资源先进入编译与审计,Svelte 检查随后确认代码和类型状态。
  • ⚙️运行产物:Vite 与adapter-node形成 Node 产物,Docker 负责提供一致运行环境。
  • 📡运行反馈:Sentry 把服务异常带回治理入口,让下一轮修复拥有可追踪信号。

我在验收独立前端时,会把构建、容器和监控看成同一条链路,而不是三个互不相关的配置文件。

这张图同时提醒我们三项未完成责任。

  • 🔁发布责任:滚动发布、回滚和前后端兼容仍需要部署平台负责。
  • 🧯运行责任:健康检查、资源限制和故障恢复不能由adapter-node自动提供。
  • 📊告警责任:接入 Sentry 不等于告警有效,通知路径和处置结果还要单独验证。

官方运行参数和部署约束可以查看 https://svelte.dev/docs/kit/adapter-node。

五、真实收益、复杂度代价与适用条件

最后回到选择问题:独立前端的收益和成本必须放在同一张决策清单里。

5.1 可确认的收益是集中、复用与可检查

当前代码和验证能够支持三类工程收益。

  • 🎯入口集中:会话、组织和路由入口收敛到服务端 hooks,不再散落到每个页面。
  • 🧰能力复用:刷新、切换组织和 API 访问形成公共能力,多页面可以复用同一实现。
  • 📏结果可查:国际化、类型检查和生产构建进入统一命令,失败能够在交付前暴露。

这些结论都能够从文件、调用关系和命令结果中复核。

至于“性能提升多少”“漏洞减少多少”“交付速度提升多少”,当前没有对照数据,不能因为采用了 SvelteKit 就自动成立。

5.2 代价是多一套服务、状态与故障面

边界更清晰并不等于系统更简单。

独立前端至少新增三类长期成本。

  • 💸服务成本:团队需要维护 Node 运行时、容器构建和前后端联合发布。
  • 🌀状态成本:cookie、access token、refresh token 与组织上下文必须保持一致。
  • 🧠认知成本:团队需要理解服务端地址、浏览器地址、入口授权和后端权限之间的关系。

典型故障也会随之改变。

这些问题未必已经发生,却都是从当前架构可以推导出的长期检查项。

5.3 是否采用取决于业务边界而不是框架热度

是否采用独立 SvelteKit 前端,可以从三个信号判断。

  • 🏁采用信号:项目具有复杂会话、多组织上下文、多个客户端和独立发布节奏。
  • ⚖️放弃信号:少量内部表单已经能由 Django Admin 或服务端模板稳定覆盖。
  • 🧑‍💻团队前提:团队愿意长期维护 API 契约、服务端 hooks、部署链路和数据治理。

如意 Django CRM 选择 SvelteKit,不是因为 Svelte 5 更新,也不是因为页面更“现代”。

真正的理由是项目需要一个明确的 Web 交付边界。

这次复盘最终留下的也不是某个框架名称,而是一条可迁移的判断原则。

先确认系统需要谁来承担边界,再决定用什么技术实现它。