You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 22:05:01