class BBImg extends HTMLElement { private observer: IntersectionObserver | null = null; private resizeObserver: ResizeObserver | null = null; private img: HTMLImageElement | null = null; private isLoaded = false; private loadId = 0; private aspectRatio: number = 3 / 2; // 默认 3:2 static get observedAttributes() { return ['src', 'alt', 'placeholder', 'max-width', 'root-margin', 'aspect-ratio']; } constructor() { super(); this.attachShadow({ mode: 'open' }); } private get container(): HTMLElement | null { return this.shadowRoot?.querySelector('.container') ?? null; } connectedCallback() { this.parseAspectRatio(); this.render(); this.setupLazyLoading(); this.setupResizeObserver(); } disconnectedCallback() { this.cleanup(); } private cleanup() { if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } if (this.img) { this.img.onload = null; this.img.onerror = null; } } private parseAspectRatio() { const ratio = this.getAttribute('aspect-ratio'); if (ratio) { const [w, h] = ratio.split('/').map(Number); if (w && h) { this.aspectRatio = w / h; } } } /** * 标准化 max-width 值:纯数字默认转为 px */ private normalizeMaxWidth(value: string | null): string { if (!value) return '100%'; if (/^\d+(\.\d+)?$/.test(value.trim())) { return `${value}px`; } return value; } /** * 根据实际容器宽度计算最小高度 */ private calculateMinHeight(actualWidth: number): string { return `${actualWidth / this.aspectRatio}px`; } /** * 更新容器的 min-height 基于实际渲染宽度 */ private updateMinHeight() { const container = this.container; if (!container || this.isLoaded) return; // 获取实际渲染宽度 const rect = container.getBoundingClientRect(); const actualWidth = rect.width; if (actualWidth > 0) { const minHeight = this.calculateMinHeight(actualWidth); container.style.minHeight = minHeight; } } /** * 设置 ResizeObserver 监听容器宽度变化 */ private setupResizeObserver() { if (!('ResizeObserver' in window)) { // 降级:初始化时计算一次 this.updateMinHeight(); return; } const container = this.container; if (!container) return; this.resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { if (entry.contentRect) { const width = entry.contentRect.width; if (width > 0 && !this.isLoaded) { const minHeight = this.calculateMinHeight(width); container.style.minHeight = minHeight; } } } }); this.resizeObserver.observe(container); } attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { if (oldValue === newValue) return; switch (name) { case 'src': if (this.isConnected) this.resetAndLoad(); break; case 'alt': if (this.img) this.img.alt = newValue || ''; break; case 'placeholder': this.updatePlaceholderColor(newValue); break; case 'max-width': this.updateMaxWidth(newValue); // 宽度属性变化时,等待布局完成后重新计算高度 requestAnimationFrame(() => this.updateMinHeight()); break; case 'aspect-ratio': this.parseAspectRatio(); this.updateAspectRatio(); break; } } private updatePlaceholderColor(color: string | null) { const container = this.container; if (container && !this.isLoaded) { container.style.backgroundColor = color || '#f5f5f5'; } } private updateMaxWidth(value: string | null) { const maxWidth = this.normalizeMaxWidth(value); this.style.maxWidth = maxWidth; const container = this.container; if (container) { container.style.maxWidth = maxWidth; } } private updateAspectRatio() { const container = this.container; if (container) { container.style.aspectRatio = `${this.aspectRatio}`; // 比例变化时重新计算高度 this.updateMinHeight(); } } private resetAndLoad() { this.isLoaded = false; this.loadId++; if (this.img) { this.img.classList.remove('loaded'); this.img.onload = null; this.img.onerror = null; this.img.removeAttribute('src'); } const container = this.container; if (container) { container.classList.remove('loaded'); container.style.backgroundColor = this.getAttribute('placeholder') || '#f5f5f5'; // 重置时清除 min-height,等待 ResizeObserver 重新计算 container.style.minHeight = ''; } if (this.observer) { this.observer.disconnect(); } this.setupLazyLoading(); } render() { const alt = this.getAttribute('alt') || ''; const placeholderColor = this.getAttribute('placeholder') || '#f5f5f5'; const rawMaxWidth = this.getAttribute('max-width'); const maxWidth = this.normalizeMaxWidth(rawMaxWidth); this.style.maxWidth = maxWidth; this.style.display = 'block'; this.shadowRoot!.innerHTML = `