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
相关产品推荐
相关产品推荐

