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

Angular 18中含Signal的模型序列化问题

Angular 18中含Signal的模型序列化问题

最近我在做Angular 18项目时碰到个头疼的问题:想把带Signal的模型对象序列化到localStorage或者IndexedDB里,但Signal本质上是函数,直接用JSON.stringify根本没法把Signal的值存进去。举个例子,我的模型是这样的:

// An example of the model I'm dealing with
export class ExampleModel {
    canSerialise = "";
    cantSerialise = signal("");
}

我原本想直接这么写:

// What I'd like to be able to do
let exampleModel = new ExampleModel();
localStorage.setItem("example", JSON.stringify(exampleModel));

但运行后发现,cantSerialise这个Signal属性直接被忽略了——因为JSON.stringify只会序列化可枚举的普通属性,而Signal是个函数包装器,自然被跳过了。我可不想给每个带Signal的模型都手动写序列化函数,太繁琐了。

我先试了最直接的JSON.stringify:

  • 直接调用JSON.stringify序列化
    • 期望结果:{"canSerialise":"","cantSerialise":X},只要X能反序列化回原来的值,并且还原成Signal就行
    • 实际结果:{"canSerialise":""},Signal属性直接丢失了

针对这个问题,我整理了几个不用手动写大量重复代码的解决方案:

方案一:用JSON.stringify的replacer自动提取Signal值

给JSON.stringify加个自定义的replacer函数,遍历对象时自动识别Signal并提取它的当前值,这个方法通用性强,所有带Signal的对象都能用:

import { isSignal } from '@angular/core';

// 自定义replacer函数,处理Signal
function signalReplacer(key, value) {
  // 用Angular官方的isSignal工具函数判断,更准确
  if (isSignal(value)) {
    return value(); // 提取Signal的当前值
  }
  return value;
}

// 序列化使用方式
let exampleModel = new ExampleModel();
localStorage.setItem("example", JSON.stringify(exampleModel, signalReplacer));

// 反序列化时需要把值转回Signal,写个reviver函数
function signalReviver(key, value) {
  // 这里可以根据属性名判断,或者和后端约定哪些字段是Signal
  // 示例:假设我们知道ExampleModel的cantSerialise是Signal
  if (key === 'cantSerialise') {
    return signal(value);
  }
  return value;
}

// 反序列化示例
const storedData = localStorage.getItem("example");
const restoredModel = Object.assign(new ExampleModel(), JSON.parse(storedData, signalReviver));

方案二:给模型类添加toJSON方法

如果你的模型类数量不多,或者想让每个模型自己控制序列化逻辑,可以给类加个toJSON方法,JSON.stringify会自动调用这个方法:

import { signal, isSignal } from '@angular/core';

export class ExampleModel {
    canSerialise = "";
    cantSerialise = signal("");

    // 自定义toJSON,自动提取所有Signal值
    toJSON() {
      const serialised = {};
      for (const key in this) {
        if (Object.prototype.hasOwnProperty.call(this, key)) {
          const value = this[key];
          serialised[key] = isSignal(value) ? value() : value;
        }
      }
      return serialised;
    }

    // 配套的fromJSON静态方法,方便反序列化还原Signal
    static fromJSON(json: Partial<ExampleModel>) {
      const model = new ExampleModel();
      for (const key in json) {
        if (Object.prototype.hasOwnProperty.call(json, key)) {
          const value = json[key];
          // 判断该属性原本是不是Signal
          if (key === 'cantSerialise') {
            model[key] = signal(value);
          } else {
            model[key] = value;
          }
        }
      }
      return model;
    }
}

// 序列化使用方式
let exampleModel = new ExampleModel();
localStorage.setItem("example", JSON.stringify(exampleModel));

// 反序列化使用方式
const storedData = localStorage.getItem("example");
const restoredModel = ExampleModel.fromJSON(JSON.parse(storedData));

方案三:用装饰器批量处理Signal属性

如果项目里有大量带Signal的模型,推荐用装饰器来批量处理,一劳永逸,不用给每个模型都写重复代码:

import { signal, isSignal } from '@angular/core';

// 装饰器:标记哪些属性是Signal
function SignalProperty() {
  return function(target: any, propertyKey: string) {
    // 把Signal属性名存到类的静态属性里,方便后续处理
    if (!target.constructor._signalProperties) {
      target.constructor._signalProperties = [];
    }
    target.constructor._signalProperties.push(propertyKey);
  };
}

// 装饰器:给模型类自动添加序列化反序列化方法
function SerializableModel() {
  return function(constructor: Function) {
    // 添加toJSON方法
    constructor.prototype.toJSON = function() {
      const serialised = {};
      const signalProps = constructor._signalProperties || [];
      for (const key in this) {
        if (Object.prototype.hasOwnProperty.call(this, key)) {
          const value = this[key];
          serialised[key] = signalProps.includes(key) && isSignal(value) ? value() : value;
        }
      }
      return serialised;
    };

    // 添加fromJSON静态方法
    constructor['fromJSON'] = function(json: any) {
      const model = new constructor();
      const signalProps = constructor._signalProperties || [];
      for (const key in json) {
        if (Object.prototype.hasOwnProperty.call(json, key)) {
          const value = json[key];
          if (signalProps.includes(key)) {
            model[key] = signal(value);
          } else {
            model[key] = value;
          }
        }
      }
      return model;
    };
  };
}

// 使用装饰器改造模型类
@SerializableModel()
export class ExampleModel {
    canSerialise = "";
    @SignalProperty()
    cantSerialise = signal("");
}

// 序列化和反序列化使用起来超简单
let exampleModel = new ExampleModel();
localStorage.setItem("example", JSON.stringify(exampleModel));
const restoredModel = ExampleModel.fromJSON(JSON.parse(localStorage.getItem("example")));

需要注意的是,反序列化时一定要把存储的普通值转回Signal,不然模型的响应式能力就没了。上面的每个方案都考虑了反序列化的步骤,你可以根据项目的规模和复杂度选择最适合的方案~

备注:内容来源于stack exchange,提问作者Jarno

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.16 09:43:13