name: lit description: 使用Lit进行Web Components开发,包括自定义元素、响应式属性、Shadow DOM和互操作性。 allowed-tools: Read, Write, Edit, Bash, Glob, Grep
Lit技能
使用Lit构建Web Components的专家级协助。
能力
- 创建具有响应式属性的Lit元素
- 实现Shadow DOM样式
- 处理事件和生命周期回调
- 构建可组合的组件库
- 确保框架互操作性
- 配置Lit的SSR(服务器端渲染)
使用场景
在以下情况时调用此技能:
- 构建框架无关的组件
- 创建设计系统基础组件
- 实现自定义元素
- 确保跨框架兼容性
- 构建微前端
输入参数
| 参数 | 类型 | 是否必需 | 描述 |
|---|---|---|---|
| elementName | 字符串 | 是 | 自定义元素名称(必须包含连字符) |
| properties | 数组 | 否 | 响应式属性 |
| shadowDom | 布尔值 | 否 | 使用Shadow DOM(默认:true) |
| typescript | 布尔值 | 否 | 使用TypeScript(默认:true) |
组件模式
基础Lit元素
// src/components/user-card.ts
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
@customElement('user-card')
export class UserCard extends LitElement {
static styles = css`
:host {
display: block;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
}
.user-card {
display: flex;
gap: 1rem;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
overflow: hidden;
background: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.initials {
font-weight: bold;
font-size: 1.25rem;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
button.primary {
background: #3b82f6;
color: white;
}
`;
@property({ type: Object })
user!: User;
@property({ type: Boolean })
editable = false;
@state()
private isEditing = false;
@state()
private editedName = '';
private get initials() {
return this.user.name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase();
}
private startEditing() {
this.editedName = this.user.name;
this.isEditing = true;
}
private save() {
this.dispatchEvent(new CustomEvent('user-updated', {
detail: { ...this.user, name: this.editedName },
bubbles: true,
composed: true,
}));
this.isEditing = false;
}
private cancel() {
this.isEditing = false;
}
render() {
return html`
<div class="user-card">
<div class="avatar">
${this.user.avatar
? html`<img src=${this.user.avatar} alt=${this.user.name} />`
: html`<span class="initials">${this.initials}</span>`}
</div>
<div class="info">
${this.isEditing
? html`
<input
.value=${this.editedName}
@input=${(e: Event) => this.editedName = (e.target as HTMLInputElement).value}
@keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.save()}
/>
<button class="primary" @click=${this.save}>保存</button>
<button @click=${this.cancel}>取消</button>
`
: html`
<h2>${this.user.name}</h2>
<p>${this.user.email}</p>
${this.editable
? html`<button @click=${this.startEditing}>编辑</button>`
: null}
`}
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'user-card': UserCard;
}
}
响应式控制器
// src/controllers/fetch-controller.ts
import { ReactiveController, ReactiveControllerHost } from 'lit';
export class FetchController<T> implements ReactiveController {
host: ReactiveControllerHost;
data: T | null = null;
loading = false;
error: Error | null = null;
private abortController: AbortController | null = null;
constructor(host: ReactiveControllerHost, private url: string) {
(this.host = host).addController(this);
}
hostConnected() {
this.fetch();
}
hostDisconnected() {
this.abortController?.abort();
}
async fetch() {
this.abortController?.abort();
this.abortController = new AbortController();
this.loading = true;
this.error = null;
this.host.requestUpdate();
try {
const response = await fetch(this.url, {
signal: this.abortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
this.data = await response.json();
} catch (e) {
if ((e as Error).name !== 'AbortError') {
this.error = e as Error;
}
} finally {
this.loading = false;
this.host.requestUpdate();
}
}
}
// 在组件中使用
@customElement('user-list')
export class UserList extends LitElement {
private usersController = new FetchController<User[]>(this, '/api/users');
render() {
if (this.usersController.loading) {
return html`<p>加载中...</p>`;
}
if (this.usersController.error) {
return html`<p>错误: ${this.usersController.error.message}</p>`;
}
return html`
<ul>
${this.usersController.data?.map(
user => html`<li>${user.name}</li>`
)}
</ul>
`;
}
}
自定义渲染指令
// src/directives/highlight.ts
import { Directive, directive, PartInfo } from 'lit/directive.js';
class HighlightDirective extends Directive {
render(text: string, query: string) {
if (!query) return text;
const regex = new RegExp(`(${query})`, 'gi');
const parts = text.split(regex);
return parts.map(part =>
regex.test(part)
? document.createElement('mark').textContent = part
: part
);
}
}
export const highlight = directive(HighlightDirective);
// 使用
html`<p>${highlight(text, searchQuery)}</p>`
插槽和组合
@customElement('modal-dialog')
export class ModalDialog extends LitElement {
static styles = css`
:host {
display: none;
}
:host([open]) {
display: block;
}
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.dialog {
background: white;
padding: 1.5rem;
border-radius: 0.5rem;
max-width: 500px;
width: 100%;
}
`;
@property({ type: Boolean, reflect: true })
open = false;
private handleBackdropClick(e: Event) {
if (e.target === e.currentTarget) {
this.dispatchEvent(new CustomEvent('close'));
}
}
render() {
return html`
<div class="backdrop" @click=${this.handleBackdropClick}>
<div class="dialog" role="dialog" aria-modal="true">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</div>
`;
}
}
// 使用
html`
<modal-dialog ?open=${showModal} @close=${() => showModal = false}>
<h2 slot="header">确认操作</h2>
<p>确定要继续吗?</p>
<div slot="footer">
<button @click=${confirm}>确认</button>
<button @click=${cancel}>取消</button>
</div>
</modal-dialog>
`
最佳实践
- 使用自定义元素命名约定(连字符分隔)
- 利用Shadow DOM实现封装
- 使用@state管理内部状态
- 分发组合事件进行父组件通信
- 实现响应式控制器用于可重用逻辑
目标流程
- web-component-library
- design-system-creation
- micro-frontend-architecture
- framework-agnostic-components