使用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 callChart.defaultsornew Chart(), theChartobject doesn't exist yet—this throws aReferenceError. - 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
- Async script loading with Promise: The
loadChartJS()method ensures Chart.js is loaded only once, and weawaitit inredraw()to guarantee theChartobject exists before using it. - Chart instance cleanup: We store the chart instance in
_chartInstanceand destroy it before creating a new one (and when the element is removed from the DOM) to prevent memory leaks. - Fixed font color typo: You had
defaultFontColor='Lato'—that's a font family, not a color! Changed it to a valid dark gray value. - 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). - 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




