Angular Datatable服务端处理时出现removeChild DOM异常,求原因
这个DOMException: Failed to execute 'removeChild' on 'Node'错误我之前在做Angular+DataTables服务端处理时也踩过坑,核心原因是Angular的DOM渲染机制和DataTables服务端模式下的DOM控制权冲突了。
你现在的写法是用Angular的*ngFor手动渲染<thead>和<tbody>,但开启serverSide: true后,DataTables会默认接管整个表格的DOM操作(比如分页、排序后重新渲染行)。这时候它会尝试去操作那些由Angular生成的DOM节点,但这些节点的父节点关系和DataTables预期的不一致,就会抛出这个移除节点的错误。
另外你提到不想在dtOptions里设置columns,想在模板自定义渲染——其实服务端模式下DataTables必须知道列的配置才能正确处理数据交互,我们可以通过调整写法,既保留模板自定义渲染的灵活性,又避免和DataTables的DOM操作冲突。
下面分步骤给出调整方案,兼顾你的自定义渲染需求和DataTables服务端模式的要求:
1. 给dtOptions添加columns基础配置
首先修改loadDtOptions方法,添加columns配置(只是告诉DataTables列对应的字段名,渲染逻辑我们后续自定义),同时调整ajax回调的返回值,让DataTables直接拿到数据,而不是手动赋值dataArray:
private loadDtOptions(): void { const that = this; // 确保columnSettings加载完成后再初始化dtOptions this.loadColumnSettings(); this.dtOptions = { pagingType: "full_numbers", pageLength: 25, serverSide: true, processing: true, // 新增columns配置,映射你的列设置 columns: this.columnSettings.map(setting => ({ data: setting.PropertyName, // 指定该列对应数据的字段名 })), ajax: (searchParams: any, callback) => { that.httpClient.post<IListingResponse>(that.serverUrl, searchParams, {}) .subscribe(resp => { // 直接把服务端返回的数据传给DataTables,不用手动维护dataArray callback({ recordsTotal: resp.TotalRecords, recordsFiltered: resp.FilteredRecords, data: resp.Data }); }); } } }
2. 调整模板,让DataTables接管tbody渲染
原来的模板用*ngFor渲染tbody的行,现在我们把这部分交给DataTables,只保留空的tbody,同时thead可以继续用Angular的*ngFor渲染(因为thead不会频繁变更):
<div class="card"> <div class="card-body"> <h5>Filter</h5> </div> </div> <div class="card"> <div class="card-body"> <table datatable [dtOptions]="dtOptions" class="row-border hover"> <thead> <tr> <th *ngFor="let columnSetting of columnSettings"> {{ columnSetting.FriendlyName }} </th> </tr> </thead> <!-- 空tbody,由DataTables负责渲染行 --> <tbody></tbody> </table> </div> </div>
3. 自定义单元格渲染(两种可选方式)
方式一:用DataTables的render函数快速实现
如果你的自定义渲染逻辑比较简单,可以直接在columns配置里写render函数,比如处理布尔值的图标:
columns: this.columnSettings.map(setting => { let renderFunc: any = null; // 布尔类型列渲染图标 if (setting.IsBoolean) { renderFunc = (data: boolean) => { return data ? '<i class="fa fa-check"></i>' : '<i class="fa fa-times"></i>'; }; } // 字符串类型直接返回值(也可以加自定义样式) if (setting.IsString) { renderFunc = (data: string) => `<span class="text-muted">${data}</span>`; } return { data: setting.PropertyName, render: renderFunc }; }),
方式二:用Angular模板实现更复杂的渲染(推荐)
如果需要用Angular的指令(比如*ngIf、组件)来渲染,可以结合TemplateRef和ViewContainerRef来实现,完全符合Angular的开发习惯:
- 在模板里定义列的渲染模板:
<!-- 字符串类型列模板 --> <ng-template #stringTemplate let-row="row" let-setting="setting"> <span class="text-primary">{{ row[setting.PropertyName] }}</span> </ng-template> <!-- 布尔类型列模板 --> <ng-template #booleanTemplate let-row="row" let-setting="setting"> <fa name="check" class="text-success" *ngIf="row[setting.PropertyName]"></fa> <fa name="times" class="text-danger" *ngIf="!row[setting.PropertyName]"></fa> </ng-template>
- 在组件里获取模板引用,并注入
ViewContainerRef:
import { ViewChild, TemplateRef, ViewContainerRef } from '@angular/core'; export class DatatableComponent implements OnInit, AfterViewInit, OnDestroy { // ...其他属性 @ViewChild('stringTemplate') stringTemplate: TemplateRef<any>; @ViewChild('booleanTemplate') booleanTemplate: TemplateRef<any>; constructor( private httpClient: HttpClient, private viewContainerRef: ViewContainerRef // 新增注入 ) { } // ...其他方法 }
- 修改
columns配置,用Angular模板渲染:
columns: this.columnSettings.map(setting => { return { data: setting.PropertyName, render: (data: any, type: any, row: any) => { // 根据列类型选择对应的模板 let template: TemplateRef<any>; if (setting.IsBoolean) { template = this.booleanTemplate; } else if (setting.IsString) { template = this.stringTemplate; } // 创建嵌入式视图并转换为HTML字符串 const view = this.viewContainerRef.createEmbeddedView(template, { row, setting }); const html = view.rootNodes[0].outerHTML; view.destroy(); // 销毁视图避免内存泄漏 return html; } }; }),
4. 清理不必要的代码
现在DataTables直接从服务端获取数据并渲染,不需要手动维护dataArray了,所以可以删除:
- 组件里的
dataArray: any[] = []; - 模板里的
<ng-container *ngIf="dataArray">
当开启serverSide: true时,DataTables会认为自己完全控制表格的DOM生命周期,包括tbody行的创建和销毁。但你用Angular的*ngFor生成了tbody的行,这些行是Angular在自身变更检测周期中创建的,DataTables尝试移除这些行时,会发现它们的父节点关系不符合预期(或者已经被Angular修改),从而抛出removeChild的错误。
通过让DataTables接管数据渲染,同时用columns配置定义列的行为,就能完美避免这种DOM冲突,同时保留自定义渲染的灵活性。
内容的提问来源于stack exchange,提问作者monstertjie_za

