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是核心触发点,问题来自三个叠加的隐患:
- 渲染时序问题:
matHeaderRowDef和matRowDef被包裹在带*ngIf="displayedColumns | async as cols"的容器中,表格组件首次执行ngAfterContentChecked生命周期时,异步的displayedColumns还未发出第一个值,表格检测不到行定义直接抛错。生产环境AOT压缩后变更检测时序和开发环境不一致,首次加载异步事件刚好赶上检测周期所以没报错,但二级页面刷新后路由跳回时,模块懒加载、注册表初始化的时序变化导致行定义注册晚于表格内容检测周期,表头无法渲染。 - 列定义缺失:模板中配置了
columns: ['separator']的分隔行,但没有对应matColumnDef="separator"的列定义,会加剧列定义匹配异常。 - 全局配置污染:代码直接修改
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
相关产品推荐
相关产品推荐

