使用信号同步 Web 存储
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
在 JavaScript 中使用本地存储既简单又有时令人沮丧。虽然Web StorageAPI非常直观,但它缺少一个在现代 Angular 应用中比以往任何时候都更加重要的关键特性:响应式。
幸运的是,最新版本提供了许多工具,让我们能够创建一个方便的实用函数,根据本地存储的值创建一个响应式信号!
在本文中,我们将了解如何在专用的 Angular 服务中抽象出 Web 存储,以及如何利用该服务通过信号同步其值,以实现以下结果:

本文所有代码都托管在 GitHub 上,欢迎查看!
抽象化 Web 存储
在实际构建信号逻辑之前,我们首先需要抽象出原生 Web Storage API。
这将使我们能够选择要操作的内容、操作方式,以及在使用何种存储方式方面拥有更大的灵活性(更不用说更容易模拟以进行测试)。
动态存储类型
我们首先要做的是为要使用的存储类型定义一个注入令牌:
// 📂 storage.service.ts
export const STORAGE = new InjectionToken<Storage>(
'Web Storage Injection Token'
);
从现在起,我们可以为我们的 Angular 应用程序提供所需的Storage(localStorage,,sessionStorage等等):
// 📂 main.ts
bootstrapApplication(AppComponent, {
providers: [{ provide: STORAGE, useValue: localStorage }],
}).catch((err) => console.error(err));
创建StorageService
由于Storage注入容器中现在已知该类型,我们可以在 Angular 服务中使用它来管理读写操作,同时还能强制执行类型安全(在一定限制内):
// 📂 storage.service.ts
@Injectable({ providedIn: 'root' })
export class StorageService {
readonly #storage = inject(STORAGE);
getItem<T>(key: string): T | null {
const raw = this.#storage.getItem(key);
return raw === null
? null
: JSON.parse(raw) as T;
}
setItem<T>(key: string, value: T | null): void {
const stringified = JSON.stringify(value);
this.#storage.setItem(key, stringified);
}
}
现在基本组成部分已经准备就绪,我们可以开始处理实际信号了!
利用信号
信号使用起来非常简单,而且也很容易进行封装和扩展。
在深入探讨读写同步之前,我们先来创建实用函数:
// 📂 from-storage.function.ts
export const fromStorage = <TValue>(storageKey: string): WritableSignal<TValue | null> => {
const storage = inject(StorageService);
const initialValue = storage.getItem<TValue>(storageKey);
return signal<TValue | null>(initialValue);
}
目前,这只会signal根据注入的值(或其缺失)创建一个Storage。
调用后fromStorage,我们现在可以跟踪特定的键及其(键入的)值:
// 📂 app.component.ts
type ColorScheme = 'light' | 'dark';
@Component({ /*...*/ })
export class AppComponent {
readonly preferredTheme = fromStorage<ColorScheme>('preferred-theme');
}
这看起来很有希望,但在两个主要方面,我们仍然缺乏所需的反应能力:
- 更新值时,我们也应该更新存储的值。
- 当此键的存储发生任何更新时,我们也应该更新其值。
让我们来解决这些问题!
同步写入
更新值时,Storage同时更新signal值本身是最简单的部分。
这样,effect我们只需调用StorageService.setItem该方法即可写入更新后的值,因为它会在值实际更改时(或与定义的相等性匹配时)被调用:
// 📂 from-storage.function.ts
export const fromStorage = <TValue>(storageKey: string): WritableSignal<TValue | null> => {
// ...
const fromStorageSignal = signal<TValue | null>(initialValue);
const writeToStorageOnUpdateEffect = effect(() => {
const updated = fromStorageSignal();
untracked(() => storage.setItem(storageKey, updated));
});
return fromStorageSignal;
}
一个问题解决了!
Syncinc 读取:充分利用 Web 存储 API
主要问题在于,更新可能发生在两种我们无法始终控制的情况下:
- 另一段代码更新了存储的值。
- 另一个标签页更新了相同的值
我们需要一种方法来检测某些变化,这些变化可能发生在我们的应用程序之外。
使用setTimeout
我们首先想到的解决方案是使用setTimeout。
虽然可行,但如果选择较小的轮询机制,要么会在更新过程中引入较长的延迟(更不用说监视多个值会引入大量的轮询系统了)。
这样的系统可以这样实现:
// 📂 from-storage.function.ts
export const fromStorage = <TValue>(storageKey: string): WritableSignal<TValue | null> => {
// ...
const updateSignalOnSignalWriteEffect = effect((onCleanup) => {
const intervalId = setInterval(() => {
const newValue = storage.getItem<TValue>(key);
const currentValue = fromStorageSignal();
const hasValueChanged = newValue !== currentValue;
if (hasValueChanged) fromStorageSignal.set(newValue);
}, 150)
onCleanup(() => clearInterval(intervalId));
});
return fromStorageSignal;
}
🚨 在仍然使用 zonejs 进行变更检测的应用程序中,您应该在 zone 之外运行此代码,以避免触发不必要的变更检测:
inject(NgZone).runOutsideAngular(() => /*...*/ );
使用存储事件
如果我们不用轮询机制,而是对变更做出反应呢?幸运的是,Web Storage API 定义了一个存储事件storage.onstorage,我们可以使用 `getStorageEvent`或 ` getStorageEvent`来监听该storage事件,它会告诉我们存储中的某些内容Storage已被修改,修改的是哪个键,以及其他一些信息。
听起来不错,我们就用这个吧:
// 📂 from-storage.function.ts
export const fromStorage = <TValue>(storageKey: string): WritableSignal<TValue | null> => {
// ...
const storageEventListener = (event: StorageEvent) => {
const isWatchedValueTargeted = event.key === storageKey;
if (!isWatchedValueTargeted) {
return;
}
const currentValue = fromStorageSignal();
const newValue = storage.getItem<TValue>(storageKey);
const hasValueChanged = newValue !== currentValue;
if (hasValueChanged) {
fromStorageSignal.set(newValue);
};
}
window.addEventListener('storage', storageEventListener);
// 👇 Don't forget to clean up after yourself
inject(DestroyRef).onDestroy(() => {
window.removeEventListener('storage', storageEventListener);
});
return fromStorageSignal;
}
太好了!我们来试试:
// 📂 app.component.ts
@Component({ /*...*/ })
export class AppComponent {
readonly preferredTheme1 = fromStorage<ColorScheme>('preferred-theme');
togglePreferredTheme(): void {
this.preferredTheme1.update(current => current === 'light' ? 'dark' : 'light');
}
readonly preferredTheme2 = fromStorage<ColorScheme>('preferred-theme');
setLightTheme(): void {
this.preferredTheme2.set('light');
}
}
调用后setLightTheme没有更新preferredTheme1,也togglePreferredTheme没有任何效果preferredTheme2!我以为我们刚刚解决了这个问题?
解释其实在MDN 文档页面上:
注意:这在进行更改的同一浏览环境中无效(……)
这意味着我们自己更新值不会触发我们自己标签页中的事件。我们离实现响应式值就差一步之遥了!
幸运的是,我们StorageEvent可以自行创建该事件,并且我们有自己的服务来与之交互storage,这意味着我们可以在编写代码时自行触发该事件:
// 📂 storage.service.ts
@Injectable({ providedIn: 'root' })
export class StorageService {
readonly #storage = inject(STORAGE);
getItem<T>(key: string): T | null { /*...*/ }
setItem<T>(key: string, value: T | null): void {
const stringified = JSON.stringify(value);
this.#storage.setItem(key, stringified);
// 👇 Notify of the update
const storageEvent = new StorageEvent('storage', {
key: key,
newValue: stringified,
storageArea: this.#storage,
});
window.dispatchEvent(storageEvent);
}
}
🚨 请注意,这可能会为其他选项卡重复事件,因此需要在事件处理程序中检查值是否已更改,以避免任何问题。
如果我们再次尝试调用togglePreferredTheme`or` setLightTheme,我们可以看到这两个信号现在确实都更新了,并且 `中的值也更新了Storage。我们成功了!
总结
本文Storage对 Web Storage API 及其交互进行了抽象,以便更好地控制其使用。然后,我们引入了一种方法,可以根据键创建信号来监视 Web Storage 中的值Storage,从而实现信号与 Web Storage 之间的同步:

如果您想亲自尝试这段代码,请查看GitHub 上的代码!
希望你学到了有用的东西!
文章来源:https://dev.to/this-is-angular/synchronized-web-storage-with-signals-5b05图片来自Unsplash用户CHUTTERSNAP