Svelte SSR/CSR通用Storage API优化方案咨询
关于Svelte中SSR/CSR通用Storage API的优化方案探讨
需求与当前实现
我正在Svelte中构建一套SSR与CSR通用的Storage API:
- localStorage在SSR时直接做空操作(NO-OP),这个逻辑已经实现
- Cookie的处理需要访问服务端的响应流,目前只能通过
server-load或hooks.server.ts实现,当前用hooks.server.ts注入Svelte提供的Cookie管理器,但觉得这个方案不够优雅,想知道是否有更优实现,或者当前方案是否足够可用。
当前代码实现
1. StorageTypes 类
import { browser } from '$app/environment'; import Cookies from 'js-cookie'; type CookieManager = { set: (key: string, value: string, opts: { expires?: Date; path: string }) => void; get: (key: string) => string | undefined; }; /** * 实现SSR兼容的Storage类型封装 */ export class StorageTypes { static _cookieManager: CookieManager | null = null; /** * 不支持httpOnly,因为是SSR/CSR共享模块 */ static get cookies() { if (!this._cookieManager && browser) { this._cookieManager = { get: Cookies.get, set: Cookies.set }; } if (!this._cookieManager) { throw new Error('accessing StorageTypes.cookies before assigning it a value'); } return this._cookieManager; } static set cookieManager(cookieManager: CookieManager) { this._cookieManager = cookieManager; } static get localStorage() { return browser ? window.localStorage : { // eslint-disable-next-line @typescript-eslint/no-unused-vars getItem(key: string) { return null; }, // eslint-disable-next-line @typescript-eslint/no-unused-vars setItem(key: string, value: string | null) {} }; } }
2. hooks.server.ts 注入Cookie管理器
export const setServerSideCookieManager: Handle = async ({ event, resolve }) => { StorageTypes.cookieManager = { set: event.cookies.set, get: event.cookies.get }; return await resolve(event); }; export const handle: Handle = sequence(setServerSideCookieManager, ...otherstuff);
3. 使用场景:响应式Cookie封装函数
function cookie$<T extends ObjectStorageTypes>(key: string, options?: Options<T>) { const storage = StorageTypes.cookies.get(key); const parsed: T = storage ? JSON.parse(storage) : options?.default; let reactiveValue = $state<T>(parsed ?? options?.initializer); $effect.root(() => { $effect(() => { const expirationDate = new Date(Date.now()); expirationDate.setSeconds( expirationDate.getSeconds() + parseInt(PUBLIC_COOKIES_EXPIRATION_SPAN_SECONDS) ); StorageTypes.cookies.set(key, JSON.stringify(reactiveValue), { expires: expirationDate, path: '/' }); }); }); return { get value$(): T { return reactiveValue; }, set value$(newValue: T) { reactiveValue = newValue; } }; }
调用示例
Persisted.cookie$<{ value: Theme }>(THEME_COOKIE_KEY, { initializer: { value: 'dark' } });
方案分析与优化建议
当前方案的可用性
当前方案是完全可用的:SvelteKit的服务端运行环境中,每个请求是完全隔离的,静态类StorageTypes的_cookieManager只会在当前请求生命周期内生效,不会出现跨请求污染的问题。这个实现简单直接,适合中小型项目使用。
更贴合Svelte风格的优化方向
如果想要更符合Svelte的设计理念,可以尝试以下两种方案:
1. 封装为Svelte Store(推荐)
利用Svelte的Store特性,把Cookie操作封装成响应式Store,自动处理SSR/CSR差异:
import { browser } from '$app/environment'; import Cookies from 'js-cookie'; import { writable, type Writable } from 'svelte/store'; export function cookieStore<T>(key: string, initialValue: T, cookieOptions?: { expires?: Date; path: string }): Writable<T> { let store: Writable<T>; if (browser) { // 客户端从Cookie读取初始值 const storedValue = Cookies.get(key); const initial = storedValue ? JSON.parse(storedValue) : initialValue; store = writable(initial); // 订阅变化,自动更新Cookie store.subscribe(value => { Cookies.set(key, JSON.stringify(value), cookieOptions); }); } else { // 服务端创建空Store,避免报错 store = writable(initialValue); // 服务端如果需要设置Cookie,需要结合load函数或hooks,这里可以留空或做NO-OP store.subscribe(() => {}); } return store; }
服务端补充处理:如果需要在服务端初始化或修改Cookie,可以在load函数中直接使用event.cookies,然后把值传递给组件的Store:
// +page.server.ts export async function load({ cookies }) { const theme = cookies.get(THEME_COOKIE_KEY) || 'dark'; return { initialTheme: theme }; } // +page.svelte <script> import { cookieStore } from '$lib/stores'; export let data; const themeStore = cookieStore('theme', data.initialTheme, { path: '/' }); </script> <button on:click={() => $themeStore = $themeStore === 'dark' ? 'light' : 'dark'}> 切换主题:{$themeStore} </button>
这种方式完全贴合Svelte的响应式开发模式,使用起来更自然,也避免了全局静态类的依赖。
2. 使用Svelte Context传递Cookie管理器
通过Context API在请求上下文传递Cookie管理器,避免全局静态类的使用:
// +layout.server.ts export async function load({ cookies }) { return { cookieManager: cookies }; } // +layout.svelte <script> import { setContext } from 'svelte'; export let data; import Cookies from 'js-cookie'; import { browser } from '$app/environment'; // 根据环境设置Context中的Cookie管理器 const cookieManager = browser ? { get: Cookies.get, set: Cookies.set } : data.cookieManager; setContext('cookieManager', cookieManager); </script> <slot />
然后在工具函数或组件中获取Context:
import { getContext } from 'svelte'; function cookie$<T>(key: string, options?: Options<T>) { const cookieManager = getContext('cookieManager'); const storage = cookieManager.get(key); const parsed: T = storage ? JSON.parse(storage) : options?.default; let reactiveValue = $state<T>(parsed ?? options?.initializer); $effect.root(() => { $effect(() => { const expirationDate = new Date(Date.now()); expirationDate.setSeconds( expirationDate.getSeconds() + parseInt(PUBLIC_COOKIES_EXPIRATION_SPAN_SECONDS) ); cookieManager.set(key, JSON.stringify(reactiveValue), { expires: expirationDate, path: '/' }); }); }); return { get value$(): T { return reactiveValue; }, set value$(newValue: T) { reactiveValue = newValue; } }; }
这种方式依赖Svelte的上下文传递,适合大型项目中需要明确依赖注入的场景,避免全局状态的隐式依赖。
总结
- 当前方案:简单可靠,适合中小型项目,无需额外修改即可正常使用
- Store封装方案:最贴合Svelte风格,响应式体验更自然,推荐优先考虑
- Context方案:适合需要明确依赖管理的大型项目,避免全局静态类的隐式依赖
内容的提问来源于stack exchange,提问作者ClassY
相关产品推荐
相关产品推荐

