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

升级至Angular 17后Angular库中属性装饰器失效问题求助

升级Angular 15到17后属性装饰器失效问题

将Angular 15升级至Angular 17后,Angular库中的@Adapter属性装饰器无法正常工作。已知TypeScript 5的装饰器机制在修改属性值上有变化,已在tsconfig中关闭useDefineForClassFields并开启experimentalDecorators,但组件中被@Adapter装饰的user$和account$在ngOnInit中仍为undefined,无法完成赋值。该装饰器用于将父应用的数据连接到子组件,是适配器逻辑的核心。


相关代码与配置

Angular库组件代码(@some/some-module)

export class CreditsComponent implements OnInit {
  @Adapter('user$')
  public user$: Observable<any>;

  @Adapter('account$')
  public account$: Observable<any>;

  public ngOnInit() {
    // 此处user$为undefined
    this.user$
      .pipe(takeUntil(this.destroy$))
      .subscribe(res => this.user = res);

    // 此处account$为undefined
    this.account$
      .pipe(takeUntil(this.destroy$))
      .subscribe(res => this.account = res);
  }
}

@Adapter装饰器实现(Angular库@some/some-module)

export function Adapter(field: string) {
    return function (target: Object, propertyKey: string) {
        DecoratorService.adapter$.pipe(
            filter((res: AdapterService) => {
                return !!res.getOptions()[field];
            }),
            take(1)
        ).subscribe(
            res => {
                    Object.defineProperty(target, propertyKey, {
                        value: res.getOptions()[field], // 无法修改属性值
                    });
            }
        );
    }
}

主应用初始化适配器代码

import { AdapterService } from '@some/some-module';

export class AppComponent {
  public constructor(
      private adapterService: AdapterService
  ) {}

  public initAdapter() {
    this.adapterService.setOptions('user$', this.currentAccountService.user$);
    this.adapterService.setOptions('accountSub', this.currentAccountService.accountSub);
    this.adapterService.setOptions('account$', this.currentAccountService.account$);
    this.adapterService.setOptions('accountsList$', this.accounts$);
    this.adapterService.setOptions('switchModalSub', this.modalSwitcherService.switchModalSub);
    this.adapterService.setOptions('switchModal$', this.modalSwitcherService.switchModal$);
    this.adapterService.setOptions('depositsProduct$', this.depositsProduct$);
    this.adapterService.setOptions('deposits$', this.deposits$);
    this.adapterService.setOptions('depositsSettings$', this.config$);
    this.adapterService.setOptions('eventSwitcherService', this.eventSwitcherService);
  }
}

主应用tsconfig配置

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "types": [
      "jasmine",
      "node"
    ],
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": false,
    "noPropertyAccessFromIndexSignature": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "ES2022",
    "module": "es2020",
    "strictNullChecks": false,
    "noImplicitAny": false,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": [
      "es2020",
      "dom"
    ],
    "paths": {
      "@business/*": [
        "src/app/*"
      ],
      "@business-test/*": [
        "src/test-data/mock/*"
      ],
      "file-saver-es": [
        "@types/file-saver"
      ]
    },
    "useDefineForClassFields": false
  },
  "angularCompilerOptions": {
    "fullTemplateTypeCheck": true,
    "strictInjectionParameters": true
  }
}

Angular库tsconfig配置(@some/some-module)

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "types": [],
    "paths": {
      "dictionary-employees": [
        "dist/dictionary-employees"
      ],
      "foreign-economic-activity": [
        "dist/foreign-economic-activity"
      ],
      "trading-module": [
        "dist/trading-module"
      ],
      "credits-module": [
        "dist/credits-module"
      ],
      "@abank/shared-module": [
        "dist/shared-module"
      ]
    },
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2022",
    "module": "es2022",
    "noImplicitAny": true,
    "strictNullChecks": false,
    "lib": [
      "es2022",
      "dom"
    ],
    "useDefineForClassFields": false
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}

解决方案

1. 调整装饰器的属性定义权限

当前装饰器中使用Object.defineProperty时未设置可写权限,在TypeScript 5+环境下,类属性默认可能被标记为不可写,导致赋值失败。修改装饰器:

export function Adapter(field: string) {
    return function (target: Object, propertyKey: string) {
        DecoratorService.adapter$.pipe(
            filter((res: AdapterService) => !!res.getOptions()[field]),
            take(1)
        ).subscribe(res => {
            Object.defineProperty(target, propertyKey, {
                value: res.getOptions()[field],
                writable: true, // 添加可写配置
                enumerable: true,
                configurable: true
            });
        });
    }
}

2. 提前初始化属性避免undefined

由于DecoratorService.adapter$是异步流,可能在ngOnInit执行时还未完成赋值。可以先给属性初始化一个空Observable,后续再替换为目标数据源:

export function Adapter(field: string) {
    return function (target: Object, propertyKey: string) {
        // 先初始化空Observable,避免ngOnInit时出现undefined
        const initial$ = new Observable<any>();
        Object.defineProperty(target, propertyKey, {
            value: initial$,
            writable: true,
            enumerable: true,
            configurable: true
        });

        // 订阅适配器数据,更新属性值
        DecoratorService.adapter$.pipe(
            filter((res: AdapterService) => !!res.getOptions()[field]),
            take(1)
        ).subscribe(res => {
            (target as any)[propertyKey] = res.getOptions()[field];
        });
    }
}

3. 统一库与主应用的编译配置

确保Angular库的tsconfig.json中experimentalDecorators和useDefineForClassFields的配置与主应用完全一致,并且库在构建时正确应用了这些配置。如果使用Angular CLI构建库,检查angular.json中的构建配置是否继承了tsconfig的设置。

4. 改用构造函数注入替代属性装饰器(可选)

如果上述方案仍不生效,可以调整适配器逻辑,直接在组件构造函数中通过AdapterService获取数据:

export class CreditsComponent implements OnInit {
  public user$: Observable<any>;
  public account$: Observable<any>;

  constructor(private adapterService: AdapterService) {}

  public ngOnInit() {
    this.user$ = this.adapterService.getOptions()['user$'];
    this.account$ = this.adapterService.getOptions()['account$'];

    this.user$?.pipe(takeUntil(this.destroy$)).subscribe(res => this.user = res);
    this.account$?.pipe(takeUntil(this.destroy$)).subscribe(res => this.account = res);
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 04:00:55