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

Angular生产环境导航返回后mat-header-cell不显示问题

Angular Material 表格表头渲染异常问题

问题现象

  • 页面首次加载时mat-header-cell可正常显示
  • 导航至二级页面、在二级页面触发浏览器刷新后,再导航回原表格页面时,mat-header-cell无法正常渲染,仅手动刷新当前页面后才可恢复显示
  • 该问题仅在生产环境复现,开发环境无此渲染异常,仅开发环境首次加载页面时会抛出对应错误

相关代码

组件ngOnInit逻辑

ngOnInit() {
    if (this.model instanceof TableMeta) {
      this.table = this.model;
    } else {
      this.table = MetaModelRegistry.get(this.model).table; // 从模型注册表获取表格配置
    }

    this.displayedColumns = combineLatest(this.table.columns.reduce((observables: Observable<boolean>[], col) => {
      // 处理列的showIf显示控制属性
      const show = col.showIf(this.injector, this.route.queryParamMap);
      observables.push(show instanceof Observable ? show : of(show));
      return observables;
    }, [])).pipe(
      map(showCols => {
        const cols = this.table.columns.filter((c, i) => showCols[i])
          .map(c => c.id);
        this.editEnabled && cols.push('edit');
        this.deleteEnabled && cols.push('delete');
        return cols;
      })
    );
    this.displayedFilters = combineLatest( [of(this.table.columns.map(c => c.id))] ).pipe(
        map( ( [cols] ) => {
        const formFields = this.table.filters.fieldSets[0].filter((ff, i) => {
          const id = ff['columnid'] || ff['id'];
          // 若存在columnid则覆盖id,用于按需同步过滤器和列的showIf显示状态
          return cols.includes(id);
        });
        const filters = this.table.filters;
        filters.fieldSets[0] = formFields;
        return filters;
      })
    );
    this.filterQueryParams = this.route.queryParams.pipe(
      map(params => {
        params = Object.assign({}, params);
        delete params['page'];
        delete params['pageSize'];
        return params;
      })
    );
    this.editEnabled = this.disableEdit ? false : this.edit.observers.length > 0;
    if (!this.deleteEnabled) this.deleteEnabled = this.delete.observers.length > 0;
    this.paginationEnabled = this.dataSource instanceof PaginatedDataSource;
    if (this.dataSource instanceof PaginatedDataSource) {
      this.pagination = this.dataSource.pagination;
    }
    this.navRoute = this.routerHelper.getPathRoute(this.router);
  }

表格模板代码

<div [ngClass]="cssClasses">
  <table mat-table multiTemplateDataRows="true" [dataSource]="dataSource">

    <ng-container *ngFor='let column of table.columns' [matColumnDef]="column.id">
      <th mat-header-cell *matHeaderCellDef>{{column.name}}</th>
      <td mat-cell *matCellDef="let value;">
        <ng-container *ngIf="column.def; else defaultDef">
          <ng-template [cdkPortalOutlet]="column | colDef: value"></ng-template>
        </ng-container>
        <ng-template #defaultDef>
          <a *ngIf='column["href"]' [href]='getCellHref(column, value)'>
            {{getCellValue(column, value)}}
          </a>
          <a *ngIf='column["routerLink"]' [routerLink]='getCellRouterLink(column, value)'
             [queryParams]='getCellRouterParams(column, value)'>
            {{getCellValue(column, value)}}
          </a>
          <span *ngIf='!(column.routerLink||column.href)'>
              {{getCellValue(column, value)}}
            </span>
        </ng-template>
      </td>
    </ng-container>
 <ng-container *ngIf="displayedColumns | async as cols">
  <tr mat-header-row *matHeaderRowDef="cols"></tr>
  <tr mat-row class="cbdweb-row-separator" *matRowDef="let row; columns: ['separator']; when: separateRowsFn()">
  </tr>
  <tr mat-row *matRowDef="let row; columns: cols;"></tr>
</ng-container>
  </table>
</div>

开发环境报错信息

core.js:6498 
ERROR Error: Missing definitions for header, footer, and row; cannot determine which columns should be rendered.
    at getTableMissingRowDefsError (table.js:1107:1)
    at MatTable.ngAfterContentChecked (table.js:1491:1)
    at callHook (core.js:2548:1)
    at callHooks (core.js:2507:1)
    at executeInitAndCheckHooks (core.js:2458:1)
    at refreshView (core.js:9525:1)
    at refreshComponent (core.js:10655:1)
    at refreshChildComponents (core.js:9280:1)
    at refreshView (core.js:9534:1)
    at refreshComponent (core.js:10655:1)

元模型注册表代码(meta-model-registry.ts)

import { CBDWebMetaModel } from "./cbdweb-meta-model";
import { TableConfigBuilder } from "./datatable/meta/table";
import { FormConfigBuilder } from "./form";

export namespace MetaModelRegistry {

    const metaModels: CBDWebMetaModel[] = [];
    const builders: CBDWebMetaModelBuilder[] = [];

    export function get(model: any) {
      const metaModel = _find(model.constructor, metaModels);
      if (metaModel) return metaModel;
      const builder = _find(model.constructor, builders);
      if (builder) {
        const metaModel = builder.build();
        builders.splice(builders.indexOf(builder), 1);
        metaModels.push(metaModel);
        return metaModel;
      }
      const formMetaModel = _findFormFormMeta(model.identifiedBy,metaModels);
      if(formMetaModel) return formMetaModel;
    }

    export function getBuilder(constructor: Function): CBDWebMetaModelBuilder {
      const metaModel = _find(constructor, metaModels);
      if (metaModel) throw new Error(`Meta model for ${constructor} already exist`);
      let builder = _find(constructor, builders);
      if (!builder) {
        builder = new CBDWebMetaModelBuilder(constructor);
        builders.push(builder);
      }
      return builder;
    }

    export function safeGet(constructor): CBDWebMetaModel {
        let metaModel = get(new constructor());
        if (!metaModel) {
            metaModel = new CBDWebMetaModel(constructor);
            add(metaModel);
        }
        return metaModel;
    }

    export function add(metaModel: CBDWebMetaModel) {
        metaModels.push(metaModel);
    }

    function _find<T extends {model: Function}>(constructor: Function, arr: T[]): T {
      return arr.find(({model}) => constructor == model);
    }

    function _findFormFormMeta(identifiedBy: string, arr: CBDWebMetaModel[]) {
      return arr.find((model) => identifiedBy == model.identifiedBy);
    }

    class CBDWebMetaModelBuilder {// 支持按需构建元模型,兼容元模型继承特性

      constructor(readonly model: Function,
                  public identifiedBy = "id",
                  public api?,
                  public table = new TableConfigBuilder(),
                  public form = new FormConfigBuilder()) { }

      build(): CBDWebMetaModel {
        return new CBDWebMetaModel(
          this.model,
          this.identifiedBy,
          this.api,
          this.table.build(),
          this.form.build()
        );
      }

    }

}

根因定位

Missing definitions for header, footer, and row是核心触发点,问题来自三个叠加的隐患:

  1. 渲染时序问题:matHeaderRowDef和matRowDef被包裹在带*ngIf="displayedColumns | async as cols"的容器中,表格组件首次执行ngAfterContentChecked生命周期时,异步的displayedColumns还未发出第一个值,表格检测不到行定义直接抛错。生产环境AOT压缩后变更检测时序和开发环境不一致,首次加载异步事件刚好赶上检测周期所以没报错,但二级页面刷新后路由跳回时,模块懒加载、注册表初始化的时序变化导致行定义注册晚于表格内容检测周期,表头无法渲染。
  2. 列定义缺失:模板中配置了columns: ['separator']的分隔行,但没有对应matColumnDef="separator"的列定义,会加剧列定义匹配异常。
  3. 全局配置污染:代码直接修改MetaModelRegistry中存储的全局表格配置对象的filters.fieldSets[0]属性,注册表是全局单例,路由复用时被篡改的配置会直接影响后续组件的初始化逻辑。

修复方案

  • 移除包裹行定义的*ngIf容器,给行定义传入空数组做首次渲染兜底,避免表格初始化时找不到行定义:
<!-- 删除外层带ngIf的ng容器,直接写行定义 -->
<tr mat-header-row *matHeaderRowDef="(displayedColumns | async) || []"></tr>
<tr mat-row class="cbdweb-row-separator" *matRowDef="let row; columns: ['separator']; when: separateRowsFn()"></tr>
<tr mat-row *matRowDef="let row; columns: (displayedColumns | async) || [];"></tr>
  • 补全separator列的matColumnDef定义,不需要分隔行则直接删除对应行配置。
  • 修改过滤器配置的生成逻辑,不要直接修改全局注册表中的原配置对象,返回新的拷贝对象避免引用污染:
this.displayedFilters = combineLatest( [of(this.table.columns.map(c => c.id))] ).pipe(
    map( ( [cols] ) => {
    const formFields = this.table.filters.fieldSets[0].filter((ff) => {
      const id = ff['columnid'] || ff['id'];
      return cols.includes(id);
    });
    // 返回拷贝的新对象,不修改全局原配置
    return {
      ...this.table.filters,
      fieldSets: [formFields, ...this.table.filters.fieldSets.slice(1)]
    };
  })
);

内容的提问来源于stack exchange,提问作者Cluadia Hedda

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 10:03:19