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

基于下拉筛选器动态渲染图表:Livewire+Chart.js getContext报错解决

问题描述

我不需要刷新Blade上已有的图表,场景为:每个模块包含问题、选项及用户选择的答案列表,通过下拉菜单按模块筛选问题。

页面加载时第一个模块可成功生成动态图表,但切换到模块2时,chartData已更新并通过emit传递给脚本,却无法更新Blade,报错信息如下:

Uncaught (in promise) TypeError: Cannot read properties of null (reading 'getContext') at 4:2072:66

报错出自代码行:

const ctx = document.getElementById(canvasId).getContext('2d');

我尝试过销毁Chart实例的方案但问题依旧,相关代码如下:

Livewire组件代码

public function mount()
{
    $this->modules = $this->session->course->modules->where('status', 'PUBLISHED');
    $this->selectedModule = $this->modules->first()->id;

    $this->GetQuestions();
    $this->preparePieChartData();
}

public function preparePieChartData()
{
    $singleChoiceQuestions = $this->questions->where('type', 'single_choice');

    $data = [];

    foreach ($singleChoiceQuestions as $question) {
        if ($question->answers->count() > 0) {
            $options = $question->options;

            $questionData = [
                'question' => $question->question_text,
                'options' => [],
            ];

            foreach ($options as $option) {
                $count = $question->answers
                    ->where('option_uuid', $option->uuid)
                    ->where('session_id', $this->session->id)
                    ->count();

                $questionData['options'][] = [
                    'option' => $option->option_text,
                    'count' => $count,
                ];
            }

            $data[] = $questionData;
        }
    }


    $this->chartData = $data;
    
    $this->emitSelf('chartDataUpdated', $data);
}

public function updatedSelectedModule()
{
    $this->GetQuestions();
    $this->preparePieChartData();
}

Blade视图代码

<select class="form-control-sm form-control" wire:model="selectedModule">
    @foreach($modules as $module)
        <option value="{{ $module->id }}">{{ $module->title }}</option>
    @endforeach
</select>

@foreach($questions as $question)
    @if ($question->type == 'single_choice')
        <canvas id="pieChart{{ $loop->index }}" style="max-height: 300px; display: block;"></canvas>
    @endif
@endforeach

Blade底部脚本代码

<script>
    document.addEventListener('livewire:load', function () {
        const chartInstances = [];

        function GenerateCharts(chartData) {
            console.log('drawing charts...');

            chartData.forEach((chart, index) => {
                console.log(index);
                console.log(chart);

                const config = {
                    type: 'pie',
                    data: {
                        labels: chart.options.map(option => option.option),
                        datasets: [{
                            label: 'Total',
                            data: chart.options.map(option => option.count),
                        }]
                    },
                    options: {
                        plugins: {}
                    },
                }

                const canvasId = `pieChart${index}`;

                if (chartInstances[canvasId]) {
                    chartInstances[canvasId].destroy();
                    delete chartInstances[canvasId];
                }

                const ctx = document.getElementById(canvasId).getContext('2d');

                chartInstances[canvasId] = new Chart(ctx, config);
            });
        }

        GenerateCharts(@js($chartData));

        @this.on('chartDataUpdated', (data) => {
            GenerateCharts(data);
        });
    });
</script>

解决方案

问题根源

  1. 切换模块时,Livewire会重新渲染questions循环,原canvas元素被替换,但chartDataUpdated事件触发时,Livewire的DOM更新可能尚未完成,导致找不到对应ID的canvas元素,抛出null错误。
  2. 使用$loop->index作为canvas ID的一部分,切换模块后新问题列表的索引会从头开始,容易出现ID冲突和实例管理混乱。

修复步骤

  1. 等待DOM更新完成后生成图表
    修改Livewire组件的emit代码,从emitSelf改为普通emit:
// 替换原emitSelf代码
$this->emit('chartDataUpdated', $data);

脚本中用Livewire的$nextTick确保DOM渲染完成后再执行图表生成:

@this.on('chartDataUpdated', (data) => {
    @this.$nextTick(() => {
        GenerateCharts(data);
    });
});
  1. 使用稳定的canvas ID
    改用问题的唯一标识(如$question->id)作为canvas ID,避免索引重复:
@foreach($questions as $question)
    @if ($question->type == 'single_choice')
        <canvas id="pieChart-{{ $question->id }}" style="max-height: 300px; display: block;"></canvas>
    @endif
@endforeach

在preparePieChartData中给questionData添加question_id字段:

$questionData = [
    'question_id' => $question->id, // 新增该行
    'question' => $question->question_text,
    'options' => [],
];

脚本中修改ID生成逻辑:

chartData.forEach((chart) => {
    const canvasId = `pieChart-${chart.question_id}`; // 改用question_id
    // 其余代码不变
});
  1. 完善实例清理逻辑
    生成新图表前,先销毁所有旧实例并清空存储,避免内存泄漏:
function GenerateCharts(chartData) {
    // 销毁所有旧图表实例
    Object.values(chartInstances).forEach(instance => {
        instance.destroy();
    });
    // 清空实例存储
    Object.keys(chartInstances).forEach(key => delete chartInstances[key]);

    chartData.forEach((chart) => {
        const canvasId = `pieChart-${chart.question_id}`;
        const ctx = document.getElementById(canvasId)?.getContext('2d');
        if (!ctx) return; // 增加空判断,避免报错

        const config = {
            type: 'pie',
            data: {
                labels: chart.options.map(option => option.option),
                datasets: [{
                    label: 'Total',
                    data: chart.options.map(option => option.count),
                }]
            },
            options: {
                plugins: {}
            },
        }

        chartInstances[canvasId] = new Chart(ctx, config);
    });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 12:07:11