惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
N
Netflix TechBlog - Medium
Google Online Security Blog
Google Online Security Blog
月光博客
月光博客
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
腾讯CDC
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
M
MIT News - Artificial intelligence
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
博客园 - 三生石上(FineUI控件)
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
MongoDB | Blog
MongoDB | Blog
H
Help Net Security
The Cloudflare Blog
Blog — PlanetScale
Blog — PlanetScale
F
Full Disclosure
G
Google Developers Blog
罗磊的独立博客
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
A
About on SuperTechFans
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
有赞技术团队
有赞技术团队
GbyAI
GbyAI
雷峰网
雷峰网
T
The Blog of Author Tim Ferriss
The Register - Security
The Register - Security
U
Unit 42
D
Docker
Martin Fowler
Martin Fowler
L
LINUX DO - 热门话题
NISL@THU
NISL@THU
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
博客园_首页
Google DeepMind News
Google DeepMind News

博客园 - dudu

.NET CQRS 的实现中引入 ReadOnlyRepository 初试 .NET CQRS 开源库 LiteBus 什么是 Agentic ? 初试 Microsoft Agent Framework 初识 Microsoft Agent Framework:一句话介绍 ASP.NET Core 中读取 UserAgent 的正确姿势 记录一下对 ASP.NET Core Middleware 进行单元测试的代码 C# 实现通用的 IdEqualityComparer 用 Angular Signal Inputs 完成一个组件的重构 量子网络操作系统 QNodeOS 资料收集 Kubernetes 集群上部署 Open WebUI 在 Kubernetes 集群的 GPU 节点上部署 Ollama 尝试在 Kubernetes 集群上用阿里云 GPU 实例部署 Ollama + DeekSeek-R1 阿里云 GPU 实例云服务器本地部署 DeepSeek R1 尝试使用阿里云计算巢部署 DeepSeek-R1 园子博客后台 Angular 升级:手工迁移至 Standalone Component Angular 中使用 ChildContent 记录 园子博客后台升级至 angular 19 后 eslint 9 迁移记录 学习大模型(LLM)的英文好文收集
Angular 中依赖注入问题造成 Observable 订阅不更新
dudu · 2025-01-19 · via 博客园 - dudu

这是园子博客后台从 angular 15 升级到 angular 19 后遇到的一个问题。博客后台「随笔 」的侧边栏会显示随笔的分类列表 ,通过这个列表的上下文菜单可以修改分类名称,升级后测试时发现一个问题,修改分类名称后分类列表没有随之更新。

侧边栏随笔分类列表是 SidebarBlogCategoriesComponent 通过订阅 BlogCategoryStore 的 Observable 类型的 categories$ 属性更新的

class SidebarBlogCategoriesComponent implements OnInit {
    ngOnInit(): void {
        this._store.categories$
            .pipe(
                this._rxHelper.autoUnsubscribe,
                distinctUntilChanged<BlogCategoryEditDto[]>(isEqual),
                map(categories => {
                    //...
                })
            )
            .subscribe(async nodes => {
                //...
            });
    }
}

对应的 BlogCategoryStore 部分实现代码

@Injectable({
    providedIn: 'any'
})
export class BlogCategoryStore {
    readonly categoryList$: Observable<BlogCategoryList>;
    protected readonly $categoryList = new BehaviorSubject<BlogCategoryList | undefined>(undefined);
    private _categories$?: Observable<BlogCategoryEditDto[]> | null;

    get categories$(): Observable<BlogCategoryEditDto[]> {
        return (this._categories$ ??= this.$categoryList.pipe(map(x => x?.categories ?? [])));
    }
}

页面加载时通过 SidebarBlogCategoriesComponent 的 parent component PostsSidebarComponent 调用 BlogCategoryStore.refresh 方法触发分类列表的订阅更新,这个地方正常

export class PostsSidebarComponent implements OnInit {
    ngOnInit() {
        this.categoryStore.refresh(BlogCategoryType.postCollection);
    }
}

修改分类名称通过上下文菜单打开编辑分类的对话框进行的:

1)打开上下文菜单是通过调用 CategoryContextMenuServiceopen 方法动态创建 CategoryContextMenuComponent

@Injectable({
    providedIn: 'any',
})
export class CategoryContextMenuService {
    constructor(
        private readonly contextMenuServ: NzContextMenuService,
    ) { }

    async open<THost extends object>(
        categoryType: BlogCategoryType,
        ev: MouseEvent,
        containerRef: ViewContainerRef
    ) {
        const { CategoryContextMenuComponent: _CategoryContextMenuComponent } = await import(
            './category-context-menu/category-context-menu.component'
        );

        const compRef = containerRef.createComponent(_CategoryContextMenuComponent);
        //...
        if (comp.menu) this.contextMenuServ.create(ev, comp.menu);
    }
}

2)在上下文菜单中点击编辑按钮,则调用 BlogCategoryEditorModalServiceopen 方法通过 NzModalService 打开模态对话框

openEditCategoryModal() {
    if (this.category) {
        this._categoryModalServ.open(this.categoryType, {
            category: this.category,
        });
    }
}

BlogCategoryEditorModalService 中对应的部分实现代码

@Injectable({
    providedIn: 'any',
})
export class BlogCategoryEditorModalService {
    constructor(
        private _categoryServ: BlogCategoryService,
        private _categoryStore: BlogCategoryStore,
        private readonly nzModalService: NzModalService
    ) { }

    open(categoryType: BlogCategoryType, { category, createdCallback }: CategoryEditModalOption = {}) {
        const isCreating = !category || category.categoryId <= 0;
        const categoryTypeName = BlogCategoryTypeNameMap[categoryType];
        const title = isCreating ? `新建${categoryTypeName}` : `编辑${categoryTypeName}`;
        const modalRef = this.nzModalService.create({
            nzTitle: title,
            nzContent: BlogCategoryEditComponent,
            nzData: { categoryType, category },
            nzOnOk: async comp => {
                //.... 
                await Promise.all(
                      Array.from(parentsToRefresh).map(p =>
                          // 在编辑分类后刷新分类列表 
                          this._categoryStore.refreshAsync(categoryType, p)
                      )
                 );
                //....
                return true;
            },
        });
    }
}

3)模态对话框中运行的是进行分类编辑操作的 BlogCategoryEditComponent

export class BlogCategoryEditComponent implements OnInit {
    constructor(
        private readonly fb: NonNullableFormBuilder,
        private readonly systemInfoServ: SystemInfoService,
        @Inject(NZ_MODAL_DATA)
        private readonly nzModalData: {
            categoryType: BlogCategoryType,
            category: BlogCategoryEditDto | null
        },
    ) {
        //...
    }
    //...
}

在模态对话框中完成分类编辑(修改分类名称)后,模态对话框会关闭,然后执行 BlogCategoryEditorModalService 中的 nzOnOk 回调方法,调用 this._categoryStore.refreshAsync 方法更新 BlogCategoryStore 中的 _categories$SidebarBlogCategoriesComponent 订阅了这个 Observable,从而触发侧边栏分类列表的更新。

现在的问题是虽然 this._categoryStore.refreshAsync 用修改后分类列表数据更新了 _categories$,但 SidebarBlogCategoriesComponent 并没有响应这个 Observable 的更新。

一开始折腾了一段时间,没找到任何线索。

后来突然想到,出现这个问题唯一可以解释得通的原因就是 SidebarBlogCategoriesComponent 订阅的 BlogCategoryStoreBlogCategoryEditorModalService 更新的 BlogCategoryStore 不是同一个实例。

有了这个思路后,立马想到的是动手验证这个2个实例是否是同一个,参考这篇博文 Get object reference IDs in JavaScript/TypeScript,很快完成了验证,详见博问 https://q.cnblogs.com/q/151643

验证结果如下:

CategoryContextMenuService.categoryStore id:  2
SidebarBlogCategoriesComponent._store id:  2
CategoryContextMenuComponent.categoryStore id:  3
BlogCategoryEditorModalService._categoryStore id:  3

果然不是同一个实例!从 CategoryContextMenuComponent 开始就不是同一个实例,这个 Component 是通过下面的代码动态创建的

const compRef = containerRef.createComponent(_CategoryContextMenuComponent);

改为在 createComponent 时将 Injector 与 EnvironmentInjector 传递过去,问题就解决了。

const compRef = containerRef.createComponent(
    _CategoryContextMenuComponent,
    {
        injector: this.injector,
        environmentInjector: this.envInjector
    }
);

之所以升级后出现这个问题,是因为在升级过程中重构代码时删除了上面的 createComponent 时传递 Injector 的代码。