Files
gh-secondsky-sap-skills-ski…/references/best-practices-guide.md
2025-11-30 08:55:27 +08:00

14 KiB

SAP SAC Custom Widget Best Practices

Comprehensive guide for performance, security, and development best practices.

Sources:


Table of Contents

  1. Performance Best Practices
  2. Security Best Practices
  3. Development Best Practices
  4. Testing Guidelines
  5. Deployment Checklist

Performance Best Practices

Widget Initialization

DO:

// Lazy initialization - only load when visible
connectedCallback() {
  if (this._initialized) return;
  this._initialized = true;
  this._init();
}

// Defer non-critical setup
_init() {
  // Critical setup first
  this._setupDOM();

  // Defer expensive operations
  requestAnimationFrame(() => {
    this._loadResources();
  });
}

DON'T:

// Heavy processing in constructor
constructor() {
  super();
  this._processLargeDataset(); // Blocks main thread
  this._loadExternalLibraries(); // Network call in constructor
}

Data Handling

Use getResultSet() Instead of getMembers():

// RECOMMENDED - No backend roundtrip
async _getData() {
  const resultSet = await this.dataBinding.getResultSet();
  return resultSet;
}

// AVOID - Causes extra backend roundtrip
async _getData() {
  const members = await this.dataBinding.getMembers("Dimension");
  return members;
}

Limit Data Points:

_renderChart(data) {
  const MAX_POINTS = 100;

  // Warn if data is truncated
  if (data.length > MAX_POINTS) {
    console.warn(`Data truncated from ${data.length} to ${MAX_POINTS} points`);
  }

  const limitedData = data.slice(0, MAX_POINTS);
  this._chart.setOption({ series: [{ data: limitedData }] });
}

Rendering Optimization

Debounce Updates:

onCustomWidgetAfterUpdate(changedProperties) {
  // Debounce rapid property changes
  if (this._updateTimer) {
    clearTimeout(this._updateTimer);
  }
  this._updateTimer = setTimeout(() => {
    this._render();
  }, 50);
}

Batch DOM Updates:

// GOOD - Single DOM update
_render() {
  const html = this._data.map(item => `<div>${item.label}: ${item.value}</div>`).join("");
  this._container.innerHTML = html;
}

// BAD - Multiple DOM updates
_render() {
  this._container.innerHTML = "";
  this._data.forEach(item => {
    const div = document.createElement("div");
    div.textContent = `${item.label}: ${item.value}`;
    this._container.appendChild(div); // Triggers reflow each time
  });
}

Use requestAnimationFrame for Visual Updates:

_scheduleRender() {
  if (this._renderScheduled) return;
  this._renderScheduled = true;

  requestAnimationFrame(() => {
    this._renderScheduled = false;
    this._render();
  });
}

Resize Handling

onCustomWidgetResize() {
  // Debounce resize events
  if (this._resizeTimer) {
    clearTimeout(this._resizeTimer);
  }
  this._resizeTimer = setTimeout(() => {
    if (this._chart) {
      this._chart.resize();
    }
  }, 100);
}

Memory Management

onCustomWidgetDestroy() {
  // Clear timers
  if (this._updateTimer) clearTimeout(this._updateTimer);
  if (this._resizeTimer) clearTimeout(this._resizeTimer);

  // Dispose chart libraries
  if (this._chart) {
    this._chart.dispose();
    this._chart = null;
  }

  // Remove event listeners
  if (this._boundHandlers) {
    window.removeEventListener("resize", this._boundHandlers.resize);
  }

  // Clear data references
  this._data = null;
  this._props = null;
}

Third-Party Library Loading

Lazy Load Libraries:

async _loadEcharts() {
  if (window.echarts) return window.echarts;

  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = "[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";)
    script.onload = () => resolve(window.echarts);
    script.onerror = () => reject(new Error("Failed to load ECharts"));
    document.head.appendChild(script);
  });
}

// Use async initialization
async connectedCallback() {
  try {
    const echarts = await this._loadEcharts();
    this._initChart(echarts);
  } catch (error) {
    this._showError("Failed to load chart library");
  }
}

Security Best Practices

Input Validation

Sanitize User Input:

_setTitle(value) {
  // Sanitize to prevent XSS
  const sanitized = this._sanitizeHTML(value);
  this._shadowRoot.getElementById("title").textContent = sanitized;
}

_sanitizeHTML(str) {
  const temp = document.createElement("div");
  temp.textContent = str;
  return temp.innerHTML;
}

Validate Property Types:

set value(val) {
  // Type validation
  if (typeof val !== "number" || isNaN(val)) {
    console.warn("Invalid value type, expected number");
    return;
  }

  // Range validation
  if (val < 0 || val > 100) {
    console.warn("Value out of range (0-100)");
    val = Math.max(0, Math.min(100, val));
  }

  this._props.value = val;
  this._render();
}

Content Security

Avoid innerHTML with User Data:

// DANGEROUS - XSS vulnerability
_render() {
  this._container.innerHTML = `<div>${this._userInput}</div>`;
}

// SAFE - Use textContent or sanitize
_render() {
  const div = document.createElement("div");
  div.textContent = this._userInput;
  this._container.innerHTML = "";
  this._container.appendChild(div);
}

Use Shadow DOM Encapsulation:

constructor() {
  super();
  // Shadow DOM isolates styles and scripts
  this._shadowRoot = this.attachShadow({ mode: "open" });
}

Integrity Hash (Production)

Generate SHA256 Hash:

# Generate integrity hash for JavaScript file
openssl dgst -sha256 -binary widget.js | openssl base64 -A

# Example output: K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=

Configure in JSON:

{
  "webcomponents": [
    {
      "kind": "main",
      "tag": "my-widget",
      "url": "[https://host.com/widget.js",](https://host.com/widget.js",)
      "integrity": "sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=",
      "ignoreIntegrity": false
    }
  ]
}

Warning: ignoreIntegrity: true triggers security warnings for admins. Only use in development.

CORS Configuration

Server Headers Required:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type

Express.js Example:

const express = require("express");
const cors = require("cors");
const app = express();

app.use(cors({
  origin: "*",
  methods: ["GET", "OPTIONS"]
}));

app.use(express.static("public"));
app.listen(3000);

External API Calls

Use HTTPS Only:

async _fetchExternalData() {
  // Always use HTTPS
  const url = "[https://api.example.com/data";](https://api.example.com/data";)

  try {
    const response = await fetch(url, {
      method: "GET",
      headers: {
        "Content-Type": "application/json"
      }
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error("API call failed:", error);
    throw error;
  }
}

Development Best Practices

Code Organization

Modular Structure:

(function() {
  // Constants
  const CONFIG = {
    MAX_ITEMS: 100,
    DEBOUNCE_MS: 50,
    DEFAULT_COLOR: "#336699"
  };

  // Template
  const template = document.createElement("template");
  template.innerHTML = `<style>...</style><div>...</div>`;

  // Helper functions
  function formatNumber(n) { ... }
  function sanitize(str) { ... }

  // Main class
  class MyWidget extends HTMLElement {
    // Properties first
    static get observedAttributes() { return ["title", "value"]; }

    // Constructor
    constructor() { ... }

    // Lifecycle methods (in order)
    connectedCallback() { ... }
    onCustomWidgetBeforeUpdate() { ... }
    onCustomWidgetAfterUpdate() { ... }
    onCustomWidgetResize() { ... }
    onCustomWidgetDestroy() { ... }

    // Public methods
    refresh() { ... }
    setValue(v) { ... }

    // Private methods (underscore prefix)
    _render() { ... }
    _handleClick() { ... }

    // Getters/setters last
    get title() { ... }
    set title(v) { ... }
  }

  customElements.define("my-widget", MyWidget);
})();

Error Handling

class MyWidget extends HTMLElement {
  _render() {
    try {
      // Rendering logic
      this._doRender();
    } catch (error) {
      console.error("Widget render failed:", error);
      this._showErrorState(error.message);
    }
  }

  _showErrorState(message) {
    this._shadowRoot.innerHTML = `
      <div class="error-container">
        <span class="error-icon">⚠️</span>
        <span class="error-message">Widget Error: ${this._sanitize(message)}</span>
      </div>
    `;
  }

  _sanitize(str) {
    const div = document.createElement("div");
    div.textContent = str;
    return div.innerHTML;
  }
}

Logging for Debugging

class MyWidget extends HTMLElement {
  _log(level, message, data) {
    if (!this._props.debug) return;

    const prefix = `[MyWidget]`;
    switch (level) {
      case "info":
        console.log(prefix, message, data);
        break;
      case "warn":
        console.warn(prefix, message, data);
        break;
      case "error":
        console.error(prefix, message, data);
        break;
    }
  }

  onCustomWidgetAfterUpdate(changedProperties) {
    this._log("info", "Properties updated", changedProperties);
    this._render();
  }
}

Documentation

JSDoc Comments:

/**
 * Custom KPI Widget for SAP Analytics Cloud
 * @class
 * @extends HTMLElement
 *
 * @property {string} title - Widget title
 * @property {number} value - KPI value (0-100)
 * @property {string} color - Primary color (hex)
 *
 * @fires onClick - When widget is clicked
 *
 * @example
 * // In SAC script
 * KPIWidget_1.title = "Revenue";
 * KPIWidget_1.value = 85;
 */
class KPIWidget extends HTMLElement { ... }

Testing Guidelines

Local Development Server

// server.js
const express = require("express");
const cors = require("cors");
const path = require("path");

const app = express();
app.use(cors());
app.use(express.static(path.join(__dirname, "dist")));

app.listen(3000, () => {
  console.log("Widget dev server: [http://localhost:3000](http://localhost:3000)");
});

Test Scenarios

1. Property Updates:

  • Change each property via script
  • Verify visual updates
  • Check console for errors

2. Data Binding:

  • Add/remove data binding
  • Empty data handling
  • Large dataset handling

3. Resize:

  • Resize container
  • Switch responsive layouts
  • Check chart redraws

4. Lifecycle:

  • Navigate away and back
  • Remove and re-add widget
  • Verify cleanup in destroy

5. Error Cases:

  • Invalid property values
  • Network failures
  • Missing data

Browser DevTools

  1. Console: Watch for errors and logs
  2. Network: Verify file loading
  3. Elements: Inspect Shadow DOM
  4. Performance: Profile render times
  5. Memory: Check for leaks

Deployment Checklist

Pre-Deployment

  • Remove console.log statements (or guard with debug flag)
  • Set ignoreIntegrity: false
  • Generate and set integrity hash
  • Minify JavaScript files
  • Test with production data
  • Verify HTTPS hosting
  • Check CORS headers

JSON Configuration

  • Unique ID (reverse domain notation)
  • Correct version number
  • All URLs are absolute HTTPS
  • Integrity hashes set
  • Properties have descriptions
  • Methods documented

Documentation

  • README with usage instructions
  • Property descriptions
  • Event documentation
  • Known limitations
  • Version changelog

Hosting

  • Files accessible via HTTPS
  • CORS configured correctly
  • CDN or reliable hosting
  • Backup of all files

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
Heavy constructor Blocks initial render Defer to connectedCallback
Sync external loads Freezes UI Use async/await
innerHTML with user data XSS vulnerability Use textContent or sanitize
No error handling Silent failures try/catch with error display
Memory leaks Performance degradation Clean up in destroy
Unbounded data UI freeze Limit and paginate
Frequent DOM updates Janky UI Batch updates
ignoreIntegrity: true in prod Security warning Generate proper hash

Resources


Last Updated: 2025-11-22