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

Svelte+TypeScript可复用Store封装:实现合规性与有效性问询

关于Svelte自定义Store实现的分析与优化

问题描述

我刚接触Svelte Store,想写一个能在自制包中使用的Store来跨组件共享状态。下面是我针对需求写的实现示例,不确定是否符合规范和最佳实践,请问这个实现能按预期工作吗?

import { writable, get } from 'svelte/store';
import type { Writable } from 'svelte/store';
import { BROWSER } from 'esm-env';

// NOTE: same as how Svelte-Kit does it under the hood
export const browser = BROWSER

// NOTE: just an example, we will pretend to have an API, but for this example
// we are just reading individual CSV files from /static/data/stocks/*.csv
const DIR_STOCK_FILES = '/data/stocks/'

export const csvStringToRows = (csvString: string, header?: boolean) => {
    const rows = csvString.split('\n')
    return header ? rows.slice(1) : rows;
}

export const parseColumnCSV = (csvString: string, n?: number) => {
    const rows = csvStringToRows(csvString, true)
    return (n ? rows.slice(0, n) : rows).map(row => Number(row))
}

export type StockValueMap = {
    [key: string]: number[]
}

interface GetStockValueURI {
    (symbol: string): string
}

export type StockValue = {
    $STOCKS: StockValueMap
    $API: string;
    getURI: GetStockValueURI;
}

export type WritableStockStore = Writable<StockValue>

interface StockStore extends Writable<StockValue> {
    $STOCKS: StockValueMap
    $API: string;
    getStock:  (symbol: string) => Promise<void>;
    setStock: (symbol: string, value: number[]) => void;

    fetchStockValue: (symbol: string) => Promise<void>;
    numberOfStocks: () => number;
    stockInStore: (symbol: string) => boolean;

    
    setAPI: (api: string) => void;
    getAPI: () => string;

    getURI: (symbol: string) => string;
    setGetURI: (fn: GetStockValueURI) => void;
    
    reset: () => void;
}


const setAPI = (store:StockStore, api: string, debug?:boolean) => {
    if (debug) console.debug('setAPI', store, api)
    store.update((s) => ({...s, $API:api}));
}

const getAPI = (store:StockStore, debug?:boolean) => {
    if (debug) console.debug('getAPI', store, get(store))
    return get(store).$API
}

const getURI = (store:StockStore, stock: string, debug?:boolean) => {
    if (debug) console.debug('getURI', store, stock)
    const api = getAPI(store)
    const uri = `${api}/${stock.toLocaleUpperCase()}.csv`
    return uri    
}

const setGetURI = (store:StockStore, fn:GetStockValueURI, debug?:boolean) => {
    if (debug) console.debug('setGetURI', store, fn)
    store.update((s) => ({...s, getURI:fn}));
}

const fetchStockValues = async (store:StockStore, stock:string, debug?:boolean) => {
    if (debug) console.debug('fetchStockValues', store, stock)
    const uri = getURI(store, stock)
    const res = await fetch(uri);
    const csv = await res.text();
    const data = parseColumnCSV(csv)
    return data
}

export const setStock = (store:StockStore, stock:string, values:number[], debug?:boolean) => {
    if (debug) console.debug('fetchStockValues', store, stock, values)
    store.update(vals => ({
        ...vals, $STOCKS:{...vals.$STOCKS, [stock]: values} 
    }));
}

export const getStock = async (store:StockStore, stock:string, debug?:boolean) => {
    if (debug) console.debug('getStock', store, stock)
    let curr = get(store).$STOCKS;
    // Check if results for this stock are in the store
    if (curr && curr[stock]) {
        console.log(`Values for ${stock} already in store:`, curr[stock]);
        Promise.resolve();
        return curr[stock]
    }

    // NOTE: it values is large (say 20k time points per stock)
    //       it is easy to over-allocate memory in the browser
    // Check if results for this stock are in localStorage
    // const val = getValueFromLocalStroage(stock)
    // if (val !== null && isValuesArray(val)) {
    //     setStock(store, stock, val)
    //     Promise.resolve();
    //     return val
    // } else {
    //     clearValueFromLocalStorage(stock)
    // }

    // Fetch results
    const data = await fetchStockValues(store, stock)
    
    // Store the fetched results in the Svelte store and localStorage
    setStock(store, stock, data)
    // setValueInLocalStorage(stock, data)
    console.log(`Fetched results for ${stock}:`, data);
    Promise.resolve();
    return data
}

const numberOfStocks = (store:StockStore, debug?:boolean) => {
    if (debug) console.debug('numberOfStocks', store)
    return Object.keys(get(store).$STOCKS).length
}

const stockInStore = (store:StockStore, stock:string, debug?:boolean) => {
    if (debug) console.debug('stockInStore', store)
    return get(store).$STOCKS.hasOwnProperty(stock)
}


export const createStockStore = (debug?:boolean) => {
    const store = (writable<StockValue>({} as StockValue))
    const { subscribe, set, update } = store;

    let stst = {
        subscribe,
        update,
        set,
        reset: () => set({} as StockValue),
        $STOCKS: {} as StockValueMap,
        $API: DIR_STOCK_FILES,  // Replace with your directory of stock files.
    } as StockStore;

    stst.setAPI = (api: string) => setAPI(stst, api, debug)
    stst.getAPI = () => getAPI(stst, debug)

    stst.getURI = (symbol: string) => getURI(stst, symbol, debug)
    stst.setGetURI = (fn: GetStockValueURI) => setGetURI(stst, fn, debug)

    stst.setStock = (symbol: string, value: number[]) => setStock(stst, symbol, value, debug)
    stst.getStock = (symbol: string) => getStock(stst, symbol, debug)

    stst.stockInStore = (symbol: string) => stockInStore(stst, symbol, debug)
    stst.numberOfStocks = () => numberOfStocks(stst, debug)

    stst.fetchStockValue = (symbol: string) => fetchStockValue(stst, symbol, debug)
    stst.setAPI(DIR_STOCK_FILES)  // Replace with your directory of stock files.
    return stst
}

let debug = false
const stocks = createStockStore(debug)
export default stocks

实现分析与优化建议

核心结论

这个实现整体思路正确,基本能按预期工作,但存在一些类型安全、逻辑冗余和不符合最佳实践的问题,修正后可以更稳定可靠。

具体问题与优化方案

1. 初始状态的类型安全问题

创建writable时用({} as StockValue)做类型断言,绕开了TS的类型检查,初始状态没有正确初始化$STOCKS、$API、getURI等必填字段,可能导致运行时错误。应该改为:

const store = writable<StockValue>({
  $STOCKS: {},
  $API: DIR_STOCK_FILES,
  getURI: (symbol) => `${DIR_STOCK_FILES}/${symbol.toLocaleUpperCase()}.csv`
});

2. 冗余的顶层属性

StockStore接口中定义的$STOCKS和$API顶层属性没有实际意义,它们不会随store状态更新而变化,始终是初始值。应该去掉这些顶层属性,改用get(store).$STOCKS或在组件中通过$stocks.$STOCKS订阅获取响应式值。

3. getStock函数的Promise处理冗余

  • 函数中多余的Promise.resolve();可以删除,async函数会自动将返回值包装为Promise
  • 已存在股票时直接return值即可,无需额外处理

修正后的getStock:

export const getStock = async (store:StockStore, stock:string, debug?:boolean) => {
    if (debug) console.debug('getStock', store, stock)
    const curr = get(store).$STOCKS;
    if (curr && curr[stock]) {
        console.log(`Values for ${stock} already in store:`, curr[stock]);
        return curr[stock];
    }

    const data = await fetchStockValues(store, stock);
    setStock(store, stock, data);
    console.log(`Fetched results for ${stock}:`, data);
    return data;
}

4. 函数拼写错误

createStockStore中给stst.fetchStockValue赋值的fetchStockValue未定义,应该改为已存在的fetchStockValues函数:

stst.fetchStockValue = (symbol: string) => fetchStockValues(stst, symbol, debug);

5. 避免过度使用get()

getAPI、numberOfStocks等函数频繁调用get(store)读取状态,非响应式。如果需要响应式的统计值,建议改用派生store:

// 示例:派生当前股票数量的store
export const createStockStore = (debug?:boolean) => {
    // ... 其他代码
    const numberOfStocks = derived(store, ($store) => Object.keys($store.$STOCKS).length);
    
    return {
        // ... 其他方法
        numberOfStocks
    } as StockStore;
}

6. 调试参数简化

createStockStore中已经传入了debug参数,无需在每个方法中重复传递,内部函数直接使用该值即可,简化方法定义。

7. 缓存逻辑优化(若启用localStorage)

如果要实现localStorage缓存,注意:

  • 存储时需将数组转为JSON字符串:localStorage.setItem(stock, JSON.stringify(data))
  • 读取时解析并验证类型:
    const val = localStorage.getItem(stock);
    if (val) {
        try {
            const parsed = JSON.parse(val);
            if (Array.isArray(parsed) && parsed.every(item => typeof item === 'number')) {
                setStock(store, stock, parsed);
                return parsed;
            }
        } catch (e) {}
        localStorage.removeItem(stock);
    }
    

最佳实践总结

  • 始终保证初始状态的完整性,避免类型断言绕过TS检查
  • 自定义Store的核心是封装状态操作逻辑,对外暴露简洁的方法
  • 优先通过订阅或派生store获取响应式值,减少get()的使用
  • 异步逻辑清晰,避免冗余的Promise处理

内容的提问来源于stack exchange,提问作者SumNeuron

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 00:07:40