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

Angular SSR应用集成CalHeatMap遇document未定义问题求助

问题:Angular SSR集成CalHeatMap时出现"document is not defined"错误

我尝试将CalHeatMap集成到Angular独立组件中,组件代码如下:

import { Component } from '@angular/core';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { matWavingHandOutline } from '@ng-icons/material-icons/outline';
import CalHeatMap from 'cal-heatmap';

@Component({
  selector: 'app-dashboard-summary',
  standalone: true,
  imports: [NgIconComponent],
  templateUrl: './dashboard-summary.component.html',
  styleUrl: './dashboard-summary.component.css',
  viewProviders: [provideIcons({ matWavingHandOutline })],
})
export class DashboardSummaryComponent {
  ngOnInit() {
    let cal = new CalHeatMap();
    // cal.paint({});
  }
}

但调用cal.paint({})时触发错误:

ReferenceError: document is not defined

错误源于CalHeatMap内部的这段代码:

import {Selection, root} from "./selection/index.js";

export default function(selector) {
  return typeof selector === "string"
      ? new Selection([[document.querySelector(selector)]], [document.documentElement])
      : new Selection([[selector]], root);
}

我尝试通过JSDOM模拟document环境,但仍报错,请问如何在SSR应用中成功集成CalHeatMap?


解决方案

核心思路:CalHeatMap依赖浏览器原生DOM环境,SSR过程中服务器端不存在document等浏览器API,因此必须确保CalHeatMap的初始化和渲染逻辑仅在客户端执行,避免服务器端触发相关代码。

方法1:通过isPlatformBrowser判断执行环境

注入Angular的PLATFORM_ID,用isPlatformBrowser确认当前为客户端环境后,再初始化CalHeatMap:

import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { matWavingHandOutline } from '@ng-icons/material-icons/outline';
import CalHeatMap from 'cal-heatmap';

@Component({
  selector: 'app-dashboard-summary',
  standalone: true,
  imports: [NgIconComponent],
  templateUrl: './dashboard-summary.component.html',
  styleUrl: './dashboard-summary.component.css',
  viewProviders: [provideIcons({ matWavingHandOutline })],
})
export class DashboardSummaryComponent implements OnInit {
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  ngOnInit() {
    // 仅在浏览器环境中执行CalHeatMap初始化
    if (isPlatformBrowser(this.platformId)) {
      const cal = new CalHeatMap();
      cal.paint({
        // 指定渲染的容器元素(需在模板中提前定义)
        itemSelector: '#cal-heatmap-container'
      });
    }
  }
}

同时在组件模板中添加渲染容器:

<div id="cal-heatmap-container"></div>

方法2:动态导入CalHeatMap(推荐)

通过动态导入语法,确保CalHeatMap库仅在客户端被加载,从根源避免服务器端解析该库时触发DOM相关错误:

import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { matWavingHandOutline } from '@ng-icons/material-icons/outline';

@Component({
  selector: 'app-dashboard-summary',
  standalone: true,
  imports: [NgIconComponent],
  templateUrl: './dashboard-summary.component.html',
  styleUrl: './dashboard-summary.component.css',
  viewProviders: [provideIcons({ matWavingHandOutline })],
})
export class DashboardSummaryComponent implements OnInit {
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  async ngOnInit() {
    if (isPlatformBrowser(this.platformId)) {
      // 动态导入CalHeatMap,仅在客户端执行
      const CalHeatMap = (await import('cal-heatmap')).default;
      const cal = new CalHeatMap();
      cal.paint({
        itemSelector: '#cal-heatmap-container'
      });
    }
  }
}

方法3:结合AfterViewInit钩子确保DOM就绪

使用ngAfterViewInit钩子,确保组件DOM元素完全渲染后再初始化CalHeatMap,避免因容器未加载导致的选择器错误:

import { Component, AfterViewInit, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { matWavingHandOutline } from '@ng-icons/material-icons/outline';
import CalHeatMap from 'cal-heatmap';

@Component({
  selector: 'app-dashboard-summary',
  standalone: true,
  imports: [NgIconComponent],
  templateUrl: './dashboard-summary.component.html',
  styleUrl: './dashboard-summary.component.css',
  viewProviders: [provideIcons({ matWavingHandOutline })],
})
export class DashboardSummaryComponent implements AfterViewInit {
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  ngAfterViewInit() {
    if (isPlatformBrowser(this.platformId)) {
      const cal = new CalHeatMap();
      cal.paint({
        itemSelector: '#cal-heatmap-container'
      });
    }
  }
}

为什么修改CalHeatMap源码无效?

你尝试用JSDOM模拟document的方法无法解决问题,原因在于:

  • SSR过程中,Angular服务器端的模块加载逻辑与客户端不同,修改本地源码可能未被正确识别或加载
  • JSDOM模拟的DOM环境与真实浏览器存在差异,CalHeatMap可能依赖window、navigator等其他浏览器API,仅模拟document不足以覆盖所有依赖

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 15:36:02