web-components-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb Components Architecture
Web Components 架构
Core Principles
核心原则
This skill enforces a strict architectural pattern for web components:
- Zero DOM Selection: NEVER use ,
querySelector, orquerySelectorAllgetElementById - Attribute-Driven State: All state flows through HTML attributes
- Event Delegation on : Use
thisand thethis.addEventListenerpatternhandleEvent - No External Dependencies: Use only standard Web Platform APIs
- Declarative Shadow DOM: Use for instant rendering
<template shadowrootmode="open"> - Progressive Enhancement: Components work (degraded) even if JavaScript fails
- Customized Built-ins Over Autonomous: Extend native elements when possible to preserve accessibility
本技能为Web Components强制执行严格的架构模式:
- 零DOM选择:绝对禁止使用、
querySelector或querySelectorAllgetElementById - 属性驱动状态:所有状态通过HTML属性流转
- 基于的事件委托:使用
this和this.addEventListener模式handleEvent - 无外部依赖:仅使用标准Web平台API
- 声明式Shadow DOM:使用实现即时渲染
<template shadowrootmode="open"> - 渐进式增强:即使JavaScript失效,组件仍能(降级)工作
- 优先定制内置元素而非自主元素:尽可能扩展原生元素以保留可访问性
Relationship with JavaScript Best Practices
与JavaScript最佳实践的关系
This skill defines the architectural pattern for building web components. When implementing components, combine this skill with for production-quality code:
javascript-pragmatic-rulesThis skill provides the WHAT (component architecture):
- Component structure (extends HTMLElement)
- Lifecycle callbacks (connectedCallback, disconnectedCallback)
- State management (attribute-driven)
- Event patterns (handleEvent, CustomEvent)
- Shadow DOM and encapsulation
javascript-pragmatic-rules- Async operation handling (timeouts, cancellation)
- Resource cleanup patterns
- Error handling strategies
- Memory leak prevention
- Performance optimization
Example: Building an component:
<async-button>javascript
// Architecture from web-components-architecture skill
class AsyncButton extends HTMLButtonElement {
#controller = null; // Private field for cleanup
connectedCallback() {
this.addEventListener('click', this);
}
// Using handleEvent pattern from web-components-architecture
async handleEvent(e) {
if (e.type === 'click') {
// Rule 2 from javascript-pragmatic-rules: Timeout async operations
this.#controller = new AbortController();
const timeoutId = setTimeout(() => this.#controller.abort(), 5_000);
try {
const response = await fetch(this.getAttribute('data-url'), {
signal: this.#controller.signal
});
clearTimeout(timeoutId);
// Handle response...
} catch (error) {
// Rule 1 from javascript-pragmatic-rules: Handle rejections
if (error.name === 'AbortError') {
console.warn('Request timed out');
} else {
throw new Error('Request failed', { cause: error });
}
}
}
}
// Rule 4 from javascript-pragmatic-rules: Clean up resources
disconnectedCallback() {
this.removeEventListener('click', this);
if (this.#controller) this.#controller.abort();
}
}Key Integration Points:
- Use this skill's with
connectedCallbackRule 4 (cleanup)javascript-pragmatic-rules - Use this skill's with
handleEventRules 1-2 (async safety)javascript-pragmatic-rules - Use this skill's attribute patterns with Rule 5 (immutability)
javascript-pragmatic-rules
See skill for comprehensive JavaScript best practices.
javascript-pragmatic-rules本技能定义了构建Web Components的架构模式。在实现组件时,需将本技能与结合,以生成生产级质量的代码:
javascript-pragmatic-rules本技能提供WHAT(组件架构):
- 组件结构(继承HTMLElement)
- 生命周期回调(connectedCallback、disconnectedCallback)
- 状态管理(属性驱动)
- 事件模式(handleEvent、CustomEvent)
- Shadow DOM与封装
javascript-pragmatic-rules- 异步操作处理(超时、取消)
- 资源清理模式
- 错误处理策略
- 内存泄漏预防
- 性能优化
示例:构建组件:
<async-button>javascript
// 来自web-components-architecture技能的架构
class AsyncButton extends HTMLButtonElement {
#controller = null; // 用于清理的私有字段
connectedCallback() {
this.addEventListener('click', this);
}
// 使用web-components-architecture技能中的handleEvent模式
async handleEvent(e) {
if (e.type === 'click') {
// javascript-pragmatic-rules规则2:异步操作超时处理
this.#controller = new AbortController();
const timeoutId = setTimeout(() => this.#controller.abort(), 5_000);
try {
const response = await fetch(this.getAttribute('data-url'), {
signal: this.#controller.signal
});
clearTimeout(timeoutId);
// 处理响应...
} catch (error) {
// javascript-pragmatic-rules规则1:处理拒绝状态
if (error.name === 'AbortError') {
console.warn('请求超时');
} else {
throw new Error('请求失败', { cause: error });
}
}
}
}
// javascript-pragmatic-rules规则4:资源清理
disconnectedCallback() {
this.removeEventListener('click', this);
if (this.#controller) this.#controller.abort();
}
}关键集成点:
- 将本技能的与javascript-pragmatic-rules规则4(清理)结合使用
connectedCallback - 将本技能的与javascript-pragmatic-rules规则1-2(异步安全)结合使用
handleEvent - 将本技能的属性模式与javascript-pragmatic-rules规则5(不可变性)结合使用
查看技能以获取全面的JavaScript最佳实践。
javascript-pragmatic-rulesComponent Types
组件类型
1. Customized Built-in Elements (PREFERRED)
1. 定制内置元素(优先选择)
Extend native HTML elements to preserve built-in accessibility and behavior.
javascript
class AsyncAction extends HTMLButtonElement {
connectedCallback() {
// Principle: Event Delegation on Self
this.addEventListener('click', this);
}
// Principle: HandleEvent pattern avoids binding 'this'
async handleEvent(e) {
if (e.type === 'click') {
// Principle: Attribute-Driven State (Input)
this.setAttribute('aria-busy', 'true');
this.disabled = true;
// Simulate async work
await new Promise((resolve) => setTimeout(resolve, 1_500));
this.removeAttribute('aria-busy');
this.disabled = false;
// Principle: Events are the ONLY output
this.dispatchEvent(new CustomEvent('action-complete', {
bubbles: true,
detail: { originalEvent: e }
}));
}
}
disconnectedCallback() {
this.removeEventListener('click', this);
}
}
// Register with { extends: 'button' }
customElements.define('async-action', AsyncAction, { extends: 'button' });Usage in HTML:
html
<button is="async-action" aria-label="Add to cart">
Add to Cart
</button>扩展原生HTML元素以保留内置的可访问性和行为。
javascript
class AsyncAction extends HTMLButtonElement {
connectedCallback() {
// 原则:在自身上进行事件委托
this.addEventListener('click', this);
}
// 原则:handleEvent模式避免绑定'this'
async handleEvent(e) {
if (e.type === 'click') {
// 原则:属性驱动状态(输入)
this.setAttribute('aria-busy', 'true');
this.disabled = true;
// 模拟异步工作
await new Promise((resolve) => setTimeout(resolve, 1_500));
this.removeAttribute('aria-busy');
this.disabled = false;
// 原则:事件是唯一的输出方式
this.dispatchEvent(new CustomEvent('action-complete', {
bubbles: true,
detail: { originalEvent: e }
}));
}
}
disconnectedCallback() {
this.removeEventListener('click', this);
}
}
// 注册时指定{ extends: 'button' }
customElements.define('async-action', AsyncAction, { extends: 'button' });HTML中的用法:
html
<button is="async-action" aria-label="Add to cart">
加入购物车
</button>2. Autonomous Custom Elements
2. 自主自定义元素
Use for layout containers and composite components.
javascript
class ProductCard extends HTMLElement {
// Purely declarative - no logic needed if using DSD
}
customElements.define('product-card', ProductCard);用于布局容器和复合组件。
javascript
class ProductCard extends HTMLElement {
// 纯声明式 - 如果使用DSD则无需逻辑
}
customElements.define('product-card', ProductCard);Declarative Shadow DOM
声明式Shadow DOM
Use for instant rendering without JavaScript.
<template shadowrootmode="open">html
<product-card>
<template shadowrootmode="open">
<style>
:host {
display: block;
max-width: 300px;
}
/* Attribute-Driven Styling */
button[aria-busy="true"] {
opacity: 0.7;
cursor: wait;
}
.card {
background: var(--card-bg, white);
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.footer {
margin-top: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
<article class="card" part="container">
<slot name="header"></slot>
<slot></slot>
<div class="footer">
<slot name="price">Free</slot>
<button is="async-action" part="action-btn">
Add to Cart
</button>
</div>
</article>
</template>
<h3 slot="header">Product Title</h3>
<p>Product description goes here.</p>
<strong slot="price">$450.00</strong>
</product-card>使用实现无需JavaScript的即时渲染。
<template shadowrootmode="open">html
<product-card>
<template shadowrootmode="open">
<style>
:host {
display: block;
max-width: 300px;
}
/* 属性驱动的样式 */
button[aria-busy="true"] {
opacity: 0.7;
cursor: wait;
}
.card {
background: var(--card-bg, white);
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.footer {
margin-top: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
<article class="card" part="container">
<slot name="header"></slot>
<slot></slot>
<div class="footer">
<slot name="price">免费</slot>
<button is="async-action" part="action-btn">
加入购物车
</button>
</div>
</article>
</template>
<h3 slot="header">产品标题</h3>
<p>产品描述内容。</p>
<strong slot="price">$450.00</strong>
</product-card>Attribute-Driven State Pattern
属性驱动状态模式
State flows IN via attributes and OUT via events.
状态通过属性输入,通过事件输出。
Input: Reading Attributes
输入:读取属性
javascript
class DataDisplay extends HTMLElement {
connectedCallback() {
// Read initial state from attributes
const apiUrl = this.getAttribute('data-url');
const refreshInterval = this.getAttribute('refresh-interval') || 5_000;
this.setAttribute('status', 'loading');
this.loadData(apiUrl);
}
async loadData(url) {
try {
const response = await fetch(url);
const data = await response.json();
// State change via attribute
this.setAttribute('status', 'loaded');
this.setAttribute('data-count', data.items.length);
// Output via event
this.dispatchEvent(new CustomEvent('data-loaded', {
bubbles: true,
detail: { data }
}));
} catch (error) {
this.setAttribute('status', 'error');
this.dispatchEvent(new CustomEvent('data-error', {
bubbles: true,
detail: { error: error.message }
}));
}
}
// Observe attribute changes
static get observedAttributes() {
return ['data-url', 'refresh-interval'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === 'data-url' && newValue) {
this.loadData(newValue);
}
}
}javascript
class DataDisplay extends HTMLElement {
connectedCallback() {
// 从属性读取初始状态
const apiUrl = this.getAttribute('data-url');
const refreshInterval = this.getAttribute('refresh-interval') || 5_000;
this.setAttribute('status', 'loading');
this.loadData(apiUrl);
}
async loadData(url) {
try {
const response = await fetch(url);
const data = await response.json();
// 通过属性变更状态
this.setAttribute('status', 'loaded');
this.setAttribute('data-count', data.items.length);
// 通过事件输出
this.dispatchEvent(new CustomEvent('data-loaded', {
bubbles: true,
detail: { data }
}));
} catch (error) {
this.setAttribute('status', 'error');
this.dispatchEvent(new CustomEvent('data-error', {
bubbles: true,
detail: { error: error.message }
}));
}
}
// 监听属性变化
static get observedAttributes() {
return ['data-url', 'refresh-interval'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === 'data-url' && newValue) {
this.loadData(newValue);
}
}
}Output: Events Only
输出:仅使用事件
javascript
// CORRECT: Dispatch events for output
this.dispatchEvent(new CustomEvent('value-changed', {
bubbles: true,
composed: true, // Cross shadow DOM boundaries
detail: { value: newValue }
}));
// WRONG: Never modify external DOM
// document.querySelector('.result').textContent = value; // ❌ FORBIDDENjavascript
// 正确做法:通过事件输出
this.dispatchEvent(new CustomEvent('value-changed', {
bubbles: true,
composed: true, // 跨Shadow DOM边界
detail: { value: newValue }
}));
// 错误做法:绝不要修改外部DOM
// document.querySelector('.result').textContent = value; // ❌ 禁止HandleEvent Pattern
HandleEvent模式
Use the interface to avoid and maintain clean memory management.
handleEvent.bind(this)javascript
class MultiEventHandler extends HTMLElement {
connectedCallback() {
// Single listener for multiple event types
this.addEventListener('click', this);
this.addEventListener('keydown', this);
this.addEventListener('focus', this);
}
handleEvent(e) {
// Route by event type
switch(e.type) {
case 'click':
this.handleClick(e);
break;
case 'keydown':
this.handleKeydown(e);
break;
case 'focus':
this.handleFocus(e);
break;
}
}
handleClick(e) {
this.setAttribute('last-interaction', 'click');
}
handleKeydown(e) {
if (e.key === 'Enter' || e.key === ' ') {
this.setAttribute('last-interaction', 'keyboard');
}
}
handleFocus(e) {
this.setAttribute('focused', 'true');
}
disconnectedCallback() {
this.removeEventListener('click', this);
this.removeEventListener('keydown', this);
this.removeEventListener('focus', this);
}
}使用接口避免并保持清晰的内存管理。
handleEvent.bind(this)javascript
class MultiEventHandler extends HTMLElement {
connectedCallback() {
// 为多种事件类型添加单个监听器
this.addEventListener('click', this);
this.addEventListener('keydown', this);
this.addEventListener('focus', this);
}
handleEvent(e) {
// 按事件类型路由
switch(e.type) {
case 'click':
this.handleClick(e);
break;
case 'keydown':
this.handleKeydown(e);
break;
case 'focus':
this.handleFocus(e);
break;
}
}
handleClick(e) {
this.setAttribute('last-interaction', 'click');
}
handleKeydown(e) {
if (e.key === 'Enter' || e.key === ' ') {
this.setAttribute('last-interaction', 'keyboard');
}
}
handleFocus(e) {
this.setAttribute('focused', 'true');
}
disconnectedCallback() {
this.removeEventListener('click', this);
this.removeEventListener('keydown', this);
this.removeEventListener('focus', this);
}
}Styling API
样式API
CSS Custom Properties (Theming)
CSS自定义属性(主题化)
css
/* Consumer defines theme */
:root {
--primary-color: #6200ea;
--bg-color: #ffffff;
--spacing: 1rem;
}
product-card {
--card-bg: var(--bg-color);
--card-padding: var(--spacing);
}css
/* 使用者定义主题 */
:root {
--primary-color: #6200ea;
--bg-color: #ffffff;
--spacing: 1rem;
}
product-card {
--card-bg: var(--bg-color);
--card-padding: var(--spacing);
}::part() Pseudo-element
::part()伪元素
html
<template shadowrootmode="open">
<style>
.internal-element {
/* internal styles */
}
</style>
<!-- Expose parts for external styling -->
<div class="internal-element" part="container">
<button part="action-btn">Click me</button>
</div>
</template>css
/* External styling via ::part() */
product-card::part(container) {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
product-card::part(action-btn) {
background-color: var(--primary-color);
color: white;
}html
<template shadowrootmode="open">
<style>
.internal-element {
/* 内部样式 */
}
</style>
<!-- 暴露部分供外部样式修改 -->
<div class="internal-element" part="container">
<button part="action-btn">点击我</button>
</div>
</template>css
/* 通过::part()进行外部样式修改 */
product-card::part(container) {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
product-card::part(action-btn) {
background-color: var(--primary-color);
color: white;
}:host and :host-context
:host和:host-context
css
<style>
/* Style the host element */
:host {
display: block;
font-family: system-ui, sans-serif;
}
/* State-based styling */
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
/* Context-based styling */
:host-context(.dark-mode) {
background: #1a1a1a;
color: #ffffff;
}
</style>css
<style>
/* 样式化宿主元素 */
:host {
display: block;
font-family: system-ui, sans-serif;
}
/* 基于状态的样式 */
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
/* 基于上下文的样式 */
:host-context(.dark-mode) {
background: #1a1a1a;
color: #ffffff;
}
</style>Lifecycle Callbacks
生命周期回调
javascript
class LifecycleExample extends HTMLElement {
constructor() {
super();
// ONLY: Initialize instance variables
// DO NOT: Access attributes, children, or DOM
}
connectedCallback() {
// Element added to DOM
// DO: Set up event listeners, start timers, fetch data
this.addEventListener('click', this);
const initialValue = this.getAttribute('value');
if (initialValue) {
this.initialize(initialValue);
}
}
disconnectedCallback() {
// Element removed from DOM
// DO: Clean up event listeners, timers, connections
this.removeEventListener('click', this);
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
attributeChangedCallback(name, oldValue, newValue) {
// Observed attribute changed
if (oldValue === newValue) return;
switch(name) {
case 'value':
this.updateValue(newValue);
break;
case 'disabled':
this.updateDisabled(newValue !== null);
break;
}
}
adoptedCallback() {
// Element moved to new document
// Rarely needed - usually for iframes
}
static get observedAttributes() {
return ['value', 'disabled', 'theme'];
}
}javascript
class LifecycleExample extends HTMLElement {
constructor() {
super();
// 仅在此处:初始化实例变量
// 禁止:访问属性、子元素或DOM
}
connectedCallback() {
// 元素被添加到DOM中
// 可以:设置事件监听器、启动定时器、获取数据
this.addEventListener('click', this);
const initialValue = this.getAttribute('value');
if (initialValue) {
this.initialize(initialValue);
}
}
disconnectedCallback() {
// 元素从DOM中移除
// 可以:清理事件监听器、定时器、连接
this.removeEventListener('click', this);
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
attributeChangedCallback(name, oldValue, newValue) {
// 监听的属性发生变化
if (oldValue === newValue) return;
switch(name) {
case 'value':
this.updateValue(newValue);
break;
case 'disabled':
this.updateDisabled(newValue !== null);
break;
}
}
adoptedCallback() {
// 元素被移动到新文档
// 很少需要 - 通常用于iframe
}
static get observedAttributes() {
return ['value', 'disabled', 'theme'];
}
}Form Integration with ElementInternals
使用ElementInternals集成表单
Use ElementInternals API for form-associated custom elements.
javascript
class CustomInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals = this.attachInternals();
}
connectedCallback() {
this.addEventListener('input', this);
this.updateValue(this.getAttribute('value') || '');
}
handleEvent(e) {
if (e.type === 'input') {
const value = e.target.value;
this.updateValue(value);
}
}
updateValue(value) {
// Update form value
this.internals.setFormValue(value);
// Validate
if (!value) {
this.internals.setValidity(
{ valueMissing: true },
'This field is required'
);
} else {
this.internals.setValidity({});
}
// Emit event
this.dispatchEvent(new CustomEvent('value-changed', {
bubbles: true,
detail: { value }
}));
}
// Form lifecycle callbacks
formResetCallback() {
this.updateValue('');
}
formDisabledCallback(disabled) {
this.setAttribute('aria-disabled', disabled);
}
}
customElements.define('custom-input', CustomInput);使用ElementInternals API实现与表单关联的自定义元素。
javascript
class CustomInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals = this.attachInternals();
}
connectedCallback() {
this.addEventListener('input', this);
this.updateValue(this.getAttribute('value') || '');
}
handleEvent(e) {
if (e.type === 'input') {
const value = e.target.value;
this.updateValue(value);
}
}
updateValue(value) {
// 更新表单值
this.internals.setFormValue(value);
// 验证
if (!value) {
this.internals.setValidity(
{ valueMissing: true },
'此字段为必填项'
);
} else {
this.internals.setValidity({});
}
// 触发事件
this.dispatchEvent(new CustomEvent('value-changed', {
bubbles: true,
detail: { value }
}));
}
// 表单生命周期回调
formResetCallback() {
this.updateValue('');
}
formDisabledCallback(disabled) {
this.setAttribute('aria-disabled', disabled);
}
}
customElements.define('custom-input', CustomInput);Accessibility Patterns
可访问性模式
ARIA Attributes
ARIA属性
javascript
class AccessibleToggle extends HTMLButtonElement {
connectedCallback() {
this.addEventListener('click', this);
// Set initial ARIA state
if (!this.hasAttribute('aria-pressed')) {
this.setAttribute('aria-pressed', 'false');
}
}
handleEvent(e) {
if (e.type === 'click') {
const isPressed = this.getAttribute('aria-pressed') === 'true';
this.setAttribute('aria-pressed', String(!isPressed));
this.dispatchEvent(new CustomEvent('toggle', {
bubbles: true,
detail: { pressed: !isPressed }
}));
}
}
}
customElements.define('accessible-toggle', AccessibleToggle, { extends: 'button' });javascript
class AccessibleToggle extends HTMLButtonElement {
connectedCallback() {
this.addEventListener('click', this);
// 设置初始ARIA状态
if (!this.hasAttribute('aria-pressed')) {
this.setAttribute('aria-pressed', 'false');
}
}
handleEvent(e) {
if (e.type === 'click') {
const isPressed = this.getAttribute('aria-pressed') === 'true';
this.setAttribute('aria-pressed', String(!isPressed));
this.dispatchEvent(new CustomEvent('toggle', {
bubbles: true,
detail: { pressed: !isPressed }
}));
}
}
}
customElements.define('accessible-toggle', AccessibleToggle, { extends: 'button' });Keyboard Navigation
键盘导航
javascript
class KeyboardNav extends HTMLElement {
connectedCallback() {
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', '0');
}
this.addEventListener('keydown', this);
}
handleEvent(e) {
if (e.type === 'keydown') {
switch(e.key) {
case 'Enter':
case ' ':
e.preventDefault();
this.activate();
break;
case 'Escape':
this.deactivate();
break;
}
}
}
activate() {
this.setAttribute('active', 'true');
this.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
}
deactivate() {
this.removeAttribute('active');
this.dispatchEvent(new CustomEvent('deactivated', { bubbles: true }));
}
}javascript
class KeyboardNav extends HTMLElement {
connectedCallback() {
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', '0');
}
this.addEventListener('keydown', this);
}
handleEvent(e) {
if (e.type === 'keydown') {
switch(e.key) {
case 'Enter':
case ' ':
e.preventDefault();
this.activate();
break;
case 'Escape':
this.deactivate();
break;
}
}
}
activate() {
this.setAttribute('active', 'true');
this.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
}
deactivate() {
this.removeAttribute('active');
this.dispatchEvent(new CustomEvent('deactivated', { bubbles: true }));
}
}Complete Example: Async Action Button
完整示例:异步操作按钮
javascript
/* async-action.js */
class AsyncAction extends HTMLButtonElement {
connectedCallback() {
this.addEventListener('click', this);
}
async handleEvent(e) {
if (e.type === 'click') {
// Prevent double-clicks
if (this.getAttribute('aria-busy') === 'true') {
return;
}
// Set loading state
this.setAttribute('aria-busy', 'true');
this.disabled = true;
const originalText = this.textContent;
const loadingText = this.getAttribute('loading-text') || 'Loading...';
this.textContent = loadingText;
try {
// Dispatch event for external handler
const actionEvent = new CustomEvent('async-action', {
bubbles: true,
cancelable: true,
detail: {
button: this,
originalEvent: e
}
});
const shouldContinue = this.dispatchEvent(actionEvent);
if (shouldContinue) {
// Simulate async work (in real usage, parent handles this)
await new Promise((resolve) => setTimeout(resolve, 1_500));
// Success state
this.setAttribute('aria-busy', 'false');
this.removeAttribute('aria-busy');
this.dispatchEvent(new CustomEvent('action-complete', {
bubbles: true,
detail: { success: true }
}));
}
} catch (error) {
// Error state
this.setAttribute('aria-busy', 'false');
this.removeAttribute('aria-busy');
this.dispatchEvent(new CustomEvent('action-error', {
bubbles: true,
detail: { error: error.message }
}));
} finally {
// Reset state
this.textContent = originalText;
this.disabled = false;
}
}
}
disconnectedCallback() {
this.removeEventListener('click', this);
}
}
customElements.define('async-action', AsyncAction, { extends: 'button' });HTML Usage:
html
<button
is="async-action"
loading-text="Saving..."
aria-label="Save changes">
Save
</button>
<script>
document.addEventListener('async-action', async (e) => {
// Handle the action
const response = await fetch('/api/save', {
method: 'POST',
body: JSON.stringify({ data: 'example' })
});
if (!response.ok) {
throw new Error('Save failed');
}
});
</script>javascript
/* async-action.js */
class AsyncAction extends HTMLButtonElement {
connectedCallback() {
this.addEventListener('click', this);
}
async handleEvent(e) {
if (e.type === 'click') {
// 防止重复点击
if (this.getAttribute('aria-busy') === 'true') {
return;
}
// 设置加载状态
this.setAttribute('aria-busy', 'true');
this.disabled = true;
const originalText = this.textContent;
const loadingText = this.getAttribute('loading-text') || '加载中...';
this.textContent = loadingText;
try {
// 触发事件供外部处理
const actionEvent = new CustomEvent('async-action', {
bubbles: true,
cancelable: true,
detail: {
button: this,
originalEvent: e
}
});
const shouldContinue = this.dispatchEvent(actionEvent);
if (shouldContinue) {
// 模拟异步工作(实际使用中,由父元素处理)
await new Promise((resolve) => setTimeout(resolve, 1_500));
// 成功状态
this.setAttribute('aria-busy', 'false');
this.removeAttribute('aria-busy');
this.dispatchEvent(new CustomEvent('action-complete', {
bubbles: true,
detail: { success: true }
}));
}
} catch (error) {
// 错误状态
this.setAttribute('aria-busy', 'false');
this.removeAttribute('aria-busy');
this.dispatchEvent(new CustomEvent('action-error', {
bubbles: true,
detail: { error: error.message }
}));
} finally {
// 重置状态
this.textContent = originalText;
this.disabled = false;
}
}
}
disconnectedCallback() {
this.removeEventListener('click', this);
}
}
customElements.define('async-action', AsyncAction, { extends: 'button' });HTML用法:
html
<button
is="async-action"
loading-text="保存中..."
aria-label="保存更改">
保存
</button>
<script>
document.addEventListener('async-action', async (e) => {
// 处理操作
const response = await fetch('/api/save', {
method: 'POST',
body: JSON.stringify({ data: '示例' })
});
if (!response.ok) {
throw new Error('保存失败');
}
});
</script>Anti-Patterns (NEVER DO THIS)
反模式(绝对禁止)
❌ DOM Selection
❌ DOM选择
javascript
// WRONG
connectedCallback() {
const button = this.querySelector('button'); // ❌
button.addEventListener('click', ...);
}javascript
// 错误做法
connectedCallback() {
const button = this.querySelector('button'); // ❌
button.addEventListener('click', ...);
}❌ Direct DOM Manipulation of External Elements
❌ 直接操作外部元素的DOM
javascript
// WRONG
handleClick() {
document.getElementById('result').textContent = 'Done'; // ❌
}javascript
// 错误做法
handleClick() {
document.getElementById('result').textContent = '完成'; // ❌
}❌ innerHTML for Dynamic Content
❌ 使用innerHTML处理动态内容
javascript
// WRONG
updateContent(data) {
this.innerHTML = `<div>${data}</div>`; // ❌
}javascript
// 错误做法
updateContent(data) {
this.innerHTML = `<div>${data}</div>`; // ❌
}❌ Imperative Shadow DOM Creation
❌ 命令式创建Shadow DOM
javascript
// WRONG (use Declarative Shadow DOM instead)
constructor() {
super();
this.attachShadow({ mode: 'open' }); // ❌
this.shadowRoot.innerHTML = '<div>...</div>'; // ❌
}javascript
// 错误做法(请使用声明式Shadow DOM替代)
constructor() {
super();
this.attachShadow({ mode: 'open' }); // ❌
this.shadowRoot.innerHTML = '<div>...</div>'; // ❌
}❌ Global State
❌ 全局状态
javascript
// WRONG
let globalState = {}; // ❌
class MyComponent extends HTMLElement {
connectedCallback() {
globalState.value = 'bad'; // ❌
}
}javascript
// 错误做法
let globalState = {}; // ❌
class MyComponent extends HTMLElement {
connectedCallback() {
globalState.value = 'bad'; // ❌
}
}Testing Pattern
测试模式
javascript
// test.html
<!DOCTYPE html>
<html>
<head>
<script type="module" src="./async-action.js"></script>
</head>
<body>
<button is="async-action" id="test-btn">Test</button>
<script type="module">
const btn = document.getElementById('test-btn');
// Test attribute-driven state
btn.addEventListener('action-complete', (e) => {
console.log('✓ Action completed', e.detail);
});
// Programmatically trigger
btn.click();
// Verify state
console.assert(
btn.getAttribute('aria-busy') === 'true',
'Should be busy during action'
);
</script>
</body>
</html>javascript
// test.html
<!DOCTYPE html>
<html>
<head>
<script type="module" src="./async-action.js"></script>
</head>
<body>
<button is="async-action" id="test-btn">测试</button>
<script type="module">
const btn = document.getElementById('test-btn');
// 测试属性驱动状态
btn.addEventListener('action-complete', (e) => {
console.log('✓ 操作完成', e.detail);
});
// 以编程方式触发
btn.click();
// 验证状态
console.assert(
btn.getAttribute('aria-busy') === 'true',
'操作期间应处于忙碌状态'
);
</script>
</body>
</html>Progressive Enhancement
渐进式增强
Components should degrade gracefully without JavaScript:
html
<!-- Works as regular button if JS fails -->
<button is="async-action" formaction="/submit" formmethod="post">
Submit Form
</button>
<!-- Works as regular link if JS fails -->
<a is="spa-link" href="/page">
Navigate
</a>组件在没有JavaScript的情况下应优雅降级:
html
<!-- 如果JS失效,可作为常规按钮工作 -->
<button is="async-action" formaction="/submit" formmethod="post">
提交表单
</button>
<!-- 如果JS失效,可作为常规链接工作 -->
<a is="spa-link" href="/page">
导航
</a>Integrating with Utopia Fluid Scales
与Utopia Fluid Scales集成
When using this skill alongside Utopia skills (, , ), follow these patterns for CSS Custom Properties and shared styles.
utopia-fluid-scalesutopia-grid-layoututopia-container-queries当本技能与Utopia技能(、、)一起使用时,请遵循以下CSS自定义属性和共享样式模式。
utopia-fluid-scalesutopia-grid-layoututopia-container-queriesCSS Custom Properties Pierce Shadow Boundaries
CSS自定义属性可穿透Shadow DOM边界
Utopia tokens defined on automatically work inside shadow DOM via :
:rootvar()css
/* Inside shadow DOM - these just work */
:host {
display: block;
container-type: inline-size; /* Required for cqi units */
}
.card {
padding: var(--space-m); /* Utopia spacing */
font-size: var(--step-0); /* Utopia typography */
gap: var(--grid-gutter); /* Utopia grid */
}
.title {
font-size: var(--step-2);
font-weight: 700;
}
.body {
font-size: var(--step--1);
color: var(--color-muted, #6b7280);
}在上定义的Utopia令牌可通过自动在Shadow DOM内生效:
:rootvar()css
/* 在Shadow DOM内 - 这些可直接生效 */
:host {
display: block;
container-type: inline-size; /* 使用cqi单位必需 */
}
.card {
padding: var(--space-m); /* Utopia间距 */
font-size: var(--step-0); /* Utopia排版 */
gap: var(--grid-gutter); /* Utopia网格 */
}
.title {
font-size: var(--step-2);
font-weight: 700;
}
.body {
font-size: var(--step--1);
color: var(--color-muted, #6b7280);
}Sharing Utility Classes via Constructable Stylesheets
通过可构造样式表共享工具类
Shadow DOM encapsulates styles, so global utility classes (, , ) don't penetrate. Use Constructable Stylesheets with to share utilities without duplication:
.u-stack.u-grid.u-clusteradoptedStyleSheetsjavascript
/* js/styles/shared-styles.js */
// Create stylesheets once - shared across all components
export const utopiaGridStyles = new CSSStyleSheet();
utopiaGridStyles.replaceSync(`
.u-container {
max-width: var(--grid-max-width, 77.5rem);
margin-inline: auto;
padding-inline: var(--grid-gutter, var(--space-s-l));
}
.u-grid {
display: grid;
gap: var(--grid-gutter, var(--space-s-l));
}
.u-grid-auto {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: var(--grid-gutter, var(--space-s-l));
}
.u-stack {
display: flex;
flex-direction: column;
gap: var(--space-s);
}
.u-cluster {
display: flex;
flex-wrap: wrap;
gap: var(--space-s);
align-items: center;
}
`);
export const utopiaTypeStyles = new CSSStyleSheet();
utopiaTypeStyles.replaceSync(`
.u-step-5 { font-size: var(--step-5); }
.u-step-4 { font-size: var(--step-4); }
.u-step-3 { font-size: var(--step-3); }
.u-step-2 { font-size: var(--step-2); }
.u-step-1 { font-size: var(--step-1); }
.u-step-0 { font-size: var(--step-0); }
.u-step--1 { font-size: var(--step--1); }
.u-step--2 { font-size: var(--step--2); }
`);
// Helper to create component-specific stylesheets
export function createStyleSheet(css) {
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
return sheet;
}Shadow DOM会封装样式,因此全局工具类(、、)无法穿透。使用可构造样式表和来共享工具类而无需重复代码:
.u-stack.u-grid.u-clusteradoptedStyleSheetsjavascript
/* js/styles/shared-styles.js */
// 一次性创建样式表 - 供所有组件共享
export const utopiaGridStyles = new CSSStyleSheet();
utopiaGridStyles.replaceSync(`
.u-container {
max-width: var(--grid-max-width, 77.5rem);
margin-inline: auto;
padding-inline: var(--grid-gutter, var(--space-s-l));
}
.u-grid {
display: grid;
gap: var(--grid-gutter, var(--space-s-l));
}
.u-grid-auto {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: var(--grid-gutter, var(--space-s-l));
}
.u-stack {
display: flex;
flex-direction: column;
gap: var(--space-s);
}
.u-cluster {
display: flex;
flex-wrap: wrap;
gap: var(--space-s);
align-items: center;
}
`);
export const utopiaTypeStyles = new CSSStyleSheet();
utopiaTypeStyles.replaceSync(`
.u-step-5 { font-size: var(--step-5); }
.u-step-4 { font-size: var(--step-4); }
.u-step-3 { font-size: var(--step-3); }
.u-step-2 { font-size: var(--step-2); }
.u-step-1 { font-size: var(--step-1); }
.u-step-0 { font-size: var(--step-0); }
.u-step--1 { font-size: var(--step--1); }
.u-step--2 { font-size: var(--step--2); }
`);
// 辅助函数:创建组件专属样式表
export function createStyleSheet(css) {
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
return sheet;
}Component Using Shared Styles
使用共享样式的组件
Note: When using , imperative shadow DOM creation is acceptable. This is the exception to the "no imperative shadow DOM" rule.
adoptedStyleSheetsjavascript
import { utopiaGridStyles, utopiaTypeStyles, createStyleSheet } from '../styles/shared-styles.js';
const componentStyles = createStyleSheet(`
:host {
display: block;
container-type: inline-size;
}
/* Use :host([attr]) for attribute-based styling */
:host([variant="primary"]) {
--card-accent: var(--color-primary, #6366f1);
}
:host([variant="secondary"]) {
--card-accent: var(--color-secondary, #8b5cf6);
}
.card {
background: var(--card-bg, white);
border: 2px solid var(--card-accent, #e5e7eb);
border-radius: var(--space-xs);
padding: var(--space-m);
}
.title {
font-size: var(--step-1);
font-weight: 700;
}
`);
class FeatureCard extends HTMLElement {
static get observedAttributes() {
return ['title', 'variant'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Adopt shared + component styles (no duplication!)
this.shadowRoot.adoptedStyleSheets = [
utopiaGridStyles,
utopiaTypeStyles,
componentStyles
];
// Now utility classes work inside shadow DOM
this.shadowRoot.innerHTML = `
<article class="card u-stack" part="container">
<h3 class="title u-step-1" part="title"></h3>
<slot></slot>
</article>
`;
this._titleEl = this.shadowRoot.querySelector('.title');
}
connectedCallback() {
this.addEventListener('click', this);
this.render();
}
disconnectedCallback() {
this.removeEventListener('click', this);
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
if (this.isConnected) this.render();
}
handleEvent(e) {
if (e.type === 'click') {
this.dispatchEvent(new CustomEvent('card-selected', {
bubbles: true,
composed: true,
detail: { title: this.getAttribute('title') }
}));
}
}
render() {
this._titleEl.textContent = this.getAttribute('title') || '';
}
}
customElements.define('feature-card', FeatureCard);注意: 使用时,允许使用命令式创建Shadow DOM。这是“禁止命令式Shadow DOM”规则的例外情况。
adoptedStyleSheetsjavascript
import { utopiaGridStyles, utopiaTypeStyles, createStyleSheet } from '../styles/shared-styles.js';
const componentStyles = createStyleSheet(`
:host {
display: block;
container-type: inline-size;
}
/* 使用:host([attr])实现基于属性的变体样式 */
:host([variant="primary"]) {
--card-accent: var(--color-primary, #6366f1);
}
:host([variant="secondary"]) {
--card-accent: var(--color-secondary, #8b5cf6);
}
.card {
background: var(--card-bg, white);
border: 2px solid var(--card-accent, #e5e7eb);
border-radius: var(--space-xs);
padding: var(--space-m);
}
.title {
font-size: var(--step-1);
font-weight: 700;
}
`);
class FeatureCard extends HTMLElement {
static get observedAttributes() {
return ['title', 'variant'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
// 采用共享样式+组件样式(无重复!)
this.shadowRoot.adoptedStyleSheets = [
utopiaGridStyles,
utopiaTypeStyles,
componentStyles
];
// 现在工具类可在Shadow DOM内生效
this.shadowRoot.innerHTML = `
<article class="card u-stack" part="container">
<h3 class="title u-step-1" part="title"></h3>
<slot></slot>
</article>
`;
this._titleEl = this.shadowRoot.querySelector('.title');
}
connectedCallback() {
this.addEventListener('click', this);
this.render();
}
disconnectedCallback() {
this.removeEventListener('click', this);
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
if (this.isConnected) this.render();
}
handleEvent(e) {
if (e.type === 'click') {
this.dispatchEvent(new CustomEvent('card-selected', {
bubbles: true,
composed: true,
detail: { title: this.getAttribute('title') }
}));
}
}
render() {
this._titleEl.textContent = this.getAttribute('title') || '';
}
}
customElements.define('feature-card', FeatureCard);Key Patterns for Utopia Integration
Utopia集成的关键模式
-
Always seton
container-type: inline-size- Required for:hostunits in Utopia fluid scalescqi -
Usefor all Utopia tokens - They pierce shadow DOM automatically:
var()- Typography: ,
var(--step-0),var(--step-1)var(--step--1) - Spacing: ,
var(--space-xs),var(--space-m)var(--space-l) - Grid: ,
var(--grid-gutter)var(--grid-max-width)
- Typography:
-
Usefor variant styling - Attribute-driven state maps directly to CSS:
:host([attribute])css:host([size="small"]) { --internal-padding: var(--space-xs); } :host([size="large"]) { --internal-padding: var(--space-l); } -
Create component-scoped custom properties - Map Utopia tokens to semantic names:css
:host { --card-padding: var(--space-m); --card-gap: var(--space-s); --title-size: var(--step-1); } -
Share CSSStyleSheet objects, not CSS strings -references the same object across all components
adoptedStyleSheets
-
始终在上设置
:host- Utopia流体刻度中使用container-type: inline-size单位必需cqi -
所有Utopia令牌使用- 它们可自动穿透Shadow DOM:
var()- 排版:、
var(--step-0)、var(--step-1)var(--step--1) - 间距:、
var(--space-xs)、var(--space-m)var(--space-l) - 网格:、
var(--grid-gutter)var(--grid-max-width)
- 排版:
-
使用实现变体样式 - 属性驱动状态直接映射到CSS:
:host([attribute])css:host([size="small"]) { --internal-padding: var(--space-xs); } :host([size="large"]) { --internal-padding: var(--space-l); } -
创建组件作用域的自定义属性 - 将Utopia令牌映射为语义化名称:css
:host { --card-padding: var(--space-m); --card-gap: var(--space-s); --title-size: var(--step-1); } -
共享CSSStyleSheet对象而非CSS字符串 -在所有组件中引用同一个对象
adoptedStyleSheets
Container Query Checklist
容器查询检查清单
When using Utopia's -based fluid scales inside shadow DOM:
cqi- has
:hostcontainer-type: inline-size - Parent elements in light DOM have where needed
container-type: inline-size - Typography uses tokens
var(--step-*) - Spacing uses tokens
var(--space-*) - Component is tested at various container widths
在Shadow DOM内使用Utopia基于的流体刻度时:
cqi- 设置了
:hostcontainer-type: inline-size - 轻量DOM中的父元素在需要时设置了
container-type: inline-size - 排版使用令牌
var(--step-*) - 间距使用令牌
var(--space-*) - 已在不同容器宽度下测试组件
FOUC Prevention (Flash of Unstyled Content)
FOUC预防(未样式内容闪烁)
Custom elements are "undefined" until JavaScript loads and registers them via . During this window, browsers render raw content without shadow DOM styles, causing a visible flash (FOUC) and layout shift (CLS).
customElements.define()自定义元素在JavaScript加载并通过注册前处于“未定义”状态。在此期间,浏览器会渲染无Shadow DOM样式的原始内容,导致可见的闪烁(FOUC)和布局偏移(CLS)。
customElements.define()The Problem
问题
html
<!-- Before JavaScript loads, this shows raw slot content -->
<site-nav>
<span slot="brand">My Site</span>
<a slot="menu" href="/">Home</a>
</site-nav>Without FOUC prevention:
- HTML parses, custom element renders as inline element with raw slot content
- JavaScript loads and defines the component
- Shadow DOM attaches, styles apply, component "snaps" into place
- Users see an ugly flash and layout shift
html
<!-- JavaScript加载前,会显示原始插槽内容 -->
<site-nav>
<span slot="brand">我的网站</span>
<a slot="menu" href="/">首页</a>
</site-nav>未进行FOUC预防时:
- HTML解析完成,自定义元素以内联元素形式渲染,显示原始插槽内容
- JavaScript加载并定义组件
- Shadow DOM附加,样式生效,组件“突然”变为最终样式
- 用户会看到丑陋的闪烁和布局偏移
Solution 1: CSS :not(:defined)
Selector (Required)
:not(:defined)解决方案1:CSS :not(:defined)
选择器(必需)
:not(:defined)The pseudo-class targets elements that have been registered via . Use to hide or style elements before definition:
:definedcustomElements.define():not(:defined)css
/* In global CSS (e.g., accessibility.css) */
/* Hide custom elements until JavaScript defines them */
site-nav:not(:defined),
nav-link-item:not(:defined) {
opacity: 0;
}
/* Reserve layout space to prevent CLS */
site-nav:not(:defined) {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: var(--z-sticky, 100);
min-height: calc(var(--nav-height, 56px) + var(--space-s) * 2);
background: var(--theme-surface);
border-block-end: 1px solid var(--theme-outline);
}
/* Instant visibility once defined */
site-nav:defined {
opacity: 1;
}Why instead of ?
opacity: 0display: none- preserves layout space, preventing CLS
opacity: 0 - causes layout shift when element appears
display: none - also works but can cause focus issues
visibility: hidden
:definedcustomElements.define():not(:defined)css
/* 在全局CSS中(例如accessibility.css) */
/* 隐藏自定义元素直到JavaScript定义它们 */
site-nav:not(:defined),
nav-link-item:not(:defined) {
opacity: 0;
}
/* 预留布局空间以防止CLS */
site-nav:not(:defined) {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: var(--z-sticky, 100);
min-height: calc(var(--nav-height, 56px) + var(--space-s) * 2);
background: var(--theme-surface);
border-block-end: 1px solid var(--theme-outline);
}
/* 定义完成后立即显示 */
site-nav:defined {
opacity: 1;
}为什么使用而非?
opacity: 0display: none- 保留布局空间,防止CLS
opacity: 0 - 会在元素出现时导致布局偏移
display: none - 也可使用,但可能导致焦点问题
visibility: hidden
Solution 2: Module Preload (Recommended)
解决方案2:模块预加载(推荐)
Preload critical above-the-fold components to reduce time-to-definition:
html
<head>
<!-- Styles first -->
<link rel="stylesheet" href="/css/styles/index.css">
<!-- Preload critical component scripts -->
<link rel="modulepreload" href="/js/components/site-nav.js">
<link rel="modulepreload" href="/js/components/nav-link-item.js">
<link rel="modulepreload" href="/js/components/page-transition.js">
<!-- Import maps -->
<script type="importmap">...</script>
</head>Benefits:
- Browser starts downloading modules in parallel with HTML parsing
- Reduces time between HTML render and component definition
- Works with ES modules (scripts)
type="module"
预加载首屏关键组件以缩短定义时间:
html
<head>
<!-- 先加载样式 -->
<link rel="stylesheet" href="/css/styles/index.css">
<!-- 预加载关键组件脚本 -->
<link rel="modulepreload" href="/js/components/site-nav.js">
<link rel="modulepreload" href="/js/components/nav-link-item.js">
<link rel="modulepreload" href="/js/components/page-transition.js">
<!-- Import maps -->
<script type="importmap">...</script>
</head>优势:
- 浏览器在解析HTML的同时并行下载模块
- 缩短HTML渲染与组件定义之间的时间
- 适用于ES模块(脚本)
type="module"
Solution 3: Declarative Shadow DOM (Best)
解决方案3:声明式Shadow DOM(最佳)
For server-rendered or static sites, use Declarative Shadow DOM for instant styling:
html
<site-nav>
<template shadowrootmode="open">
<style>
:host {
display: block;
position: fixed;
top: 0;
/* ... all component styles inline ... */
}
</style>
<nav>
<slot name="brand"></slot>
<slot name="menu"></slot>
</nav>
</template>
<!-- Light DOM content -->
<span slot="brand">My Site</span>
<a slot="menu" href="/">Home</a>
</site-nav>Benefits:
- Styles apply immediately during HTML parsing
- No JavaScript required for initial render
- Best possible CLS score
- JavaScript enhances (adds interactivity) but doesn't enable styling
对于服务端渲染或静态站点,使用声明式Shadow DOM实现即时样式:
html
<site-nav>
<template shadowrootmode="open">
<style>
:host {
display: block;
position: fixed;
top: 0;
/* ... 所有组件样式内联 ... */
}
</style>
<nav>
<slot name="brand"></slot>
<slot name="menu"></slot>
</nav>
</template>
<!-- 轻量DOM内容 -->
<span slot="brand">我的网站</span>
<a slot="menu" href="/">首页</a>
</site-nav>优势:
- 样式在HTML解析期间立即生效
- 初始渲染无需JavaScript
- 获得最佳CLS分数
- JavaScript仅用于增强(添加交互性),而非启用样式
Implementation Checklist
实现检查清单
For every custom element in your application:
- Add CSS rule in global stylesheet
:not(:defined) - Reserve layout space for fixed/positioned elements
- Add for above-the-fold components
modulepreload - Consider Declarative Shadow DOM for critical-path components
- Test with network throttling to verify no FOUC
- Measure CLS in Lighthouse/PageSpeed Insights
应用中的每个自定义元素都需:
- 在全局样式表中添加CSS规则
:not(:defined) - 为固定/定位元素预留布局空间
- 为首屏组件添加
modulepreload - 考虑为关键路径组件使用声明式Shadow DOM
- 通过网络节流测试以验证无FOUC
- 在Lighthouse/PageSpeed Insights中测量CLS
Project Pattern
项目模式
This project handles FOUC in :
css/styles/accessibility.csscss
/* Navigation - reserve space and hide content */
site-nav:not(:defined) {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: var(--z-sticky, 100);
min-height: calc(var(--nav-height, 56px) + var(--space-s, 0.75rem) * 2);
background: var(--theme-surface, #1f1b15);
border-block-end: 1px solid var(--theme-outline, #5c4d3d);
opacity: 0;
}
/* Cards and grids - hide until ready */
filterable-grid:not(:defined),
word-card:not(:defined) {
opacity: 0;
}And in HTML :
<head>html
<!-- Modulepreload critical components to prevent FOUC -->
<link rel="modulepreload" href="/js/components/site-nav.js">
<link rel="modulepreload" href="/js/components/nav-link-item.js">
<link rel="modulepreload" href="/js/components/page-transition.js">本项目在中处理FOUC:
css/styles/accessibility.csscss
/* 导航 - 预留空间并隐藏内容 */
site-nav:not(:defined) {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: var(--z-sticky, 100);
min-height: calc(var(--nav-height, 56px) + var(--space-s, 0.75rem) * 2);
background: var(--theme-surface, #1f1b15);
border-block-end: 1px solid var(--theme-outline, #5c4d3d);
opacity: 0;
}
/* 卡片和网格 - 准备就绪前隐藏 */
filterable-grid:not(:defined),
word-card:not(:defined) {
opacity: 0;
}并在HTML 中:
<head>html
<!-- 预加载关键组件以防止FOUC -->
<link rel="modulepreload" href="/js/components/site-nav.js">
<link rel="modulepreload" href="/js/components/nav-link-item.js">
<link rel="modulepreload" href="/js/components/page-transition.js">Anti-Pattern: Generic Wildcard
反模式:通用通配符
Avoid hiding all undefined elements with a wildcard:
css
/* DON'T DO THIS - breaks progressive enhancement */
*:not(:defined) {
display: none;
}This breaks elements that should be visible during progressive enhancement (forms, links, etc.). Instead, explicitly list the components that need FOUC prevention.
避免使用通配符隐藏所有未定义元素:
css
/* 不要这样做 - 破坏渐进式增强 */
*:not(:defined) {
display: none;
}这会破坏渐进式增强期间应可见的元素(表单、链接等)。相反,应明确列出需要FOUC预防的组件。
When to Use This Pattern
何时使用此模式
- Building reusable UI components
- Creating design systems
- Server-side rendering (SSR) with hydration
- Progressive web apps (PWAs)
- Micro-frontends
- Accessibility-first applications
- 构建可复用UI组件
- 创建设计系统
- 服务端渲染(SSR)与水化
- 渐进式Web应用(PWA)
- 微前端
- 优先考虑可访问性的应用
When NOT to Use
何时不使用
- Simple static websites (use plain HTML)
- Heavy data visualization (consider Canvas/WebGL)
- Applications requiring IE11 support (no polyfills allowed)
- 简单静态网站(使用纯HTML)
- 重型数据可视化(考虑Canvas/WebGL)
- 需要支持IE11的应用(不允许使用polyfill)
Instructions for Implementation
实现指南
When implementing web components:
- Choose Element Type: Prefer customized built-ins over autonomous elements
- Define State Contract: Document all attributes and events
- Use HandleEvent: Implement for event delegation
handleEvent() - Avoid DOM Selection: NEVER use or similar methods
querySelector - Use DSD: Implement Declarative Shadow DOM for instant rendering
- Expose Styling API: Use CSS custom properties and
::part() - Implement Accessibility: Add ARIA attributes and keyboard support
- Test Progressive Enhancement: Verify behavior with JavaScript disabled
- Document Usage: Provide clear HTML examples
实现Web Components时:
- 选择元素类型:优先选择定制内置元素而非自主元素
- 定义状态契约:记录所有属性和事件
- 使用HandleEvent:实现进行事件委托
handleEvent() - 避免DOM选择:绝对禁止使用或类似方法
querySelector - 使用DSD:实现声明式Shadow DOM以实现即时渲染
- 暴露样式API:使用CSS自定义属性和
::part() - 实现可访问性:添加ARIA属性和键盘支持
- 测试渐进式增强:验证JavaScript禁用时的行为
- 记录用法:提供清晰的HTML示例
File Organization
文件组织
components/
├── async-action/
│ ├── async-action.js
│ ├── async-action.test.html
│ └── README.md
├── product-card/
│ ├── product-card.js
│ ├── product-card.test.html
│ └── README.md
└── index.js (exports all components)components/
├── async-action/
│ ├── async-action.js
│ ├── async-action.test.html
│ └── README.md
├── product-card/
│ ├── product-card.js
│ ├── product-card.test.html
│ └── README.md
└── index.js(导出所有组件)