You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用Shadow DOM与Chart.js创建图表组件失败问题排查

Let's break down why your Shadow DOM-based custom element isn't working and fix it step by step:

Core Issues in Your Current Code

  • You're using Chart.js before it finishes loading: The script tag you create in redraw() loads asynchronously, so when you try to call Chart.defaults or new Chart(), the Chart object doesn't exist yet—this throws a ReferenceError.
  • You're not attaching resources properly: The Bootstrap stylesheet and Chart.js script are created but never added to the DOM. Even if you added them to the main document, Shadow DOM's style isolation means Bootstrap styles won't apply inside your component.
  • Redundant resource loading: Every call to redraw() creates new script/link tags, leading to duplicate network requests and potential conflicts.
  • No cleanup for old chart instances: Re-running redraw() without destroying existing charts can cause memory leaks or overlapping visuals.

Fixed Implementation

Here's the corrected code with explanations for each change:

(function() {
  let tmpl = document.createElement('template');
  tmpl.innerHTML = `
    <div class="container">
      <canvas id="myChart"></canvas>
    </div>
    <style>
      /* Add component-specific styles here (Shadow DOM isolates styles) */
      .container {
        padding: 1rem;
      }
    </style>
  `;

  // Cache Chart.js load promise to avoid duplicate requests
  let chartLoadPromise = null;

  customElements.define('com-sap-sample-helloworld1', class HelloWorld1 extends HTMLElement {
    constructor() {
      super();
      this._shadowRoot = this.attachShadow({mode: "open"});
      this._shadowRoot.appendChild(tmpl.content.cloneNode(true));
      this._firstConnection = false;
      this._chartInstance = null; // Store chart instance for cleanup
    }

    connectedCallback(){
      this._firstConnection = true;
      this.redraw();
    }

    disconnectedCallback(){
      // Clean up chart when element is removed from DOM
      if (this._chartInstance) {
        this._chartInstance.destroy();
        this._chartInstance = null;
      }
    }

    onCustomWidgetAfterUpdate(oChangedProperties) {
      if (this._firstConnection){
        this.redraw();
      }
    }

    onCustomWidgetDestroy(){
      // Clean up on component destroy
      if (this._chartInstance) {
        this._chartInstance.destroy();
        this._chartInstance = null;
      }
    }

    // Helper to load Chart.js once
    loadChartJS() {
      if (chartLoadPromise) return chartLoadPromise;

      chartLoadPromise = new Promise((resolve, reject) => {
        if (window.Chart) {
          // Chart is already loaded globally
          resolve();
          return;
        }

        const script = document.createElement('script');
        script.src = "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js";
        script.onload = resolve;
        script.onerror = reject;
        document.head.appendChild(script);
      });

      return chartLoadPromise;
    }

    async redraw() {
      try {
        // Wait for Chart.js to load before proceeding
        await this.loadChartJS();

        // Clean up existing chart if it exists
        if (this._chartInstance) {
          this._chartInstance.destroy();
        }

        // Get canvas context from Shadow DOM
        const canvas = this._shadowRoot.getElementById('myChart');
        const myChart = canvas.getContext('2d');
        const typeofchart = 'horizontalBar';

        // Fixed: 'Lato' is a font family, not a color
        Chart.defaults.global.defaultFontFamily = 'Lato';
        Chart.defaults.global.defaultFontSize = 18;
        Chart.defaults.global.defaultFontColor = '#333';

        // Create new chart instance
        this._chartInstance = new Chart(myChart, {
          type: typeofchart,
          data: {
            labels: ['Boston','Worcester','Springfield','Lowell','Cambridge','New Bedford'],
            datasets:[{
              label:'Population',
              data:[617594, 181045, 153060, 106519, 105162, 95072],
              backgroundColor:[
                'rgba(255,99,132,0.6)',
                'rgba(54,162,235,0.6)',
                'rgba(255,206,86,0.6)',
                'rgba(75,192,192,0.6)',
                'rgba(153,102,255,0.6)',
                'rgba(255,159,64,0.6)',
                'rgba(255,99,132,0.6)'
              ],
              borderWidth:1,
              borderColor:'#777',
              hoverBorderWidth:3,
              hoverBorderColor:'#000'
            }]
          },
          options:{
            title:{
              display:true,
              text:'Largest Cities In Massachusetts',
              fontSize:25
            },
            legend:{
              display:true,
              position:'right',
              labels:{
                fontColor:'#000'
              }
            },
            layout:{
              padding:{
                left:50,
                right:0,
                bottom:0,
                top:0
              }
            },
            tooltips:{
              enabled:true
            }
          }
        });
      } catch (error) {
        console.error('Failed to load Chart.js or initialize chart:', error);
      }
    }
  });
})();

Key Changes Explained

  1. Async script loading with Promise: The loadChartJS() method ensures Chart.js is loaded only once, and we await it in redraw() to guarantee the Chart object exists before using it.
  2. Chart instance cleanup: We store the chart instance in _chartInstance and destroy it before creating a new one (and when the element is removed from the DOM) to prevent memory leaks.
  3. Fixed font color typo: You had defaultFontColor='Lato'—that's a font family, not a color! Changed it to a valid dark gray value.
  4. Component-specific styles: Added a <style> tag inside the template for Shadow DOM-specific styling (since Bootstrap styles won't leak into the component by default).
  5. Proper resource injection: Chart.js is loaded into the main document (since it's a global library), but you could also inject it directly into the Shadow DOM if you prefer.

Bonus: Using Bootstrap in Shadow DOM

If you want Bootstrap styles to apply inside your component, add this to the redraw() method (or create a separate helper):

// Inject Bootstrap into Shadow DOM
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css";
this._shadowRoot.appendChild(link);

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

火山引擎 最新活动