组件只做一件事:把数据渲染成 DOM,并把用户事件转成对 Service 的调用。 复杂业务逻辑、请求、状态计算 —— 全部下沉到 Service。
⚠️ 禁用
@customElement/@property/@state/@consume等装饰器语法。 装饰器是 TypeScript / Babel 的编译期特性,浏览器原生不执行。无构建(源码即产物)下, 一旦写装饰器,浏览器直接SyntaxError。所有 Lit 官方「装饰器版」文档不能照抄。
一律改用标准静态类字段 + 编程式 API:
| 装饰器版(❌ 禁) | 无构建版(✅ 用) |
|---|---|
@customElement('x-y') |
类末尾 customElements.define('x-y', XY) |
@property({ type: String }) name |
static properties = { name: { type: String } } |
@state() kw = '' |
static properties = { kw: { type: String, state: true } } + constructor 初始化 |
@consume({ context }) |
new ContextConsumer(this, { context, subscribe, callback }) |
@provide({ context }) |
模板里的 provide(ctx, value) 指令(见 context-di.md) |
@query('#x') |
this.renderRoot.querySelector('#x') |
@queryAssignedElements |
this.querySelectorAll('slot') / slot.assignedElements() |
static properties是 Lit 的标准 API(装饰器只是它的语法糖),无构建下完全可用。
// components/user-card.js
import { LitElement, html, css } from 'lit';
export class UserCard extends LitElement {
static properties = {
name: { type: String },
user: { type: Object },
};
static styles = css`
:host { display: block; padding: 12px; border: 1px solid #eee; border-radius: 8px; }
.name { font-weight: 600; }
`;
constructor() {
super();
this.name = '';
this.user = null;
}
render() {
return html`
<div class="name">${this.name}</div>
<slot></slot>
`;
}
/** 事件只做转发,不做业务 */
_onClick() {
this.dispatchEvent(new CustomEvent('card-click', {
detail: { user: this.user },
bubbles: true,
composed: true, // 穿透 Shadow DOM 边界,必须
}));
}
}
customElements.define('user-card', UserCard);
| 特性 | 用途 | 要点 |
|---|---|---|
html`...` |
声明式模板 | 自动转义(天然防 XSS);模板字面量按内容缓存,只更新变化部分 |
css`...` |
组件样式 | 在 Shadow DOM 内生效,样式隔离 |
| Shadow DOM | DOM/CSS 隔离 | 默认 open;跨边界事件需 composed: true |
@property |
对外属性(UI 状态 / 入参) | 会触发重渲染,默认 attribute 反射 |
@state |
内部私有状态 | 不对外暴露、不反射到 attribute |
requestUpdate() / updateComplete |
手动触发 / 等待渲染 | 备选手段,优先用 property 驱动 |
<slot> |
插槽 | 封装通用布局组件(弹窗外壳、面板) |
⚠️ 两套响应式必须分开,混用会导致更新时机错乱。
| 场景 | 怎么用 |
|---|---|
| 组件内部 UI(弹窗开关、输入框值、tab 高亮) | Lit @property / @state + this.requestUpdate() |
| 跨组件全局业务状态(登录态、字典、购物车、权限) | @lit-labs/signals,组件用 SignalWatcher 混入 |
需要全局 signal 的组件:用 SignalWatcher(LitElement) 包裹,signal 变化自动重渲染。
import { LitElement, html } from 'lit';
import { SignalWatcher } from '@lit-labs/signals';
import { authService } from '../services/auth-service.js';
export class PageDashboard extends SignalWatcher(LitElement) {
render() {
// authService.user 是 signal:读取即订阅,变化自动重渲染
return html`<h1>你好,${authService.user.get()?.name ?? '访客'}</h1>`;
}
}
| 方向 | 手段 |
|---|---|
| 父 → 子 | static properties 声明的属性(attribute 或 .prop=${x} 绑定) |
| 子 → 父 | dispatchEvent(new CustomEvent('x', { bubbles, composed })) |
| 任意 ↔ 任意 | Service + Signal(首选),组件只读 signal / 调方法 |
| 组件树深层传递依赖 | @lit-labs/context 的 provide() 指令 + ContextConsumer,见 context-di.md |
kebab-case,detail 用对象。composed: true,否则父级监听不到。@customElement/@property/@consume…)—— 无构建下浏览器不执行,见第〇节。fetch / 写接口地址 —— 交给 Service。ContextConsumer 或直接 import 单例。static properties 的 state: true。lit —— 自定义元素注册冲突,见 pitfalls.md。label、value、disabled),不暴露内部实现细节。<... .innerHTML> 传 HTML。--card-bg)而非改内部 class。