# 组件规范(LitElement) > 组件只做一件事:**把数据渲染成 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**(装饰器只是它的语法糖),无构建下完全可用。 ## 一、最小组件骨架(无装饰器) ```js // 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`
${this.name}
`; } /** 事件只做转发,不做业务 */ _onClick() { this.dispatchEvent(new CustomEvent('card-click', { detail: { user: this.user }, bubbles: true, composed: true, // 穿透 Shadow DOM 边界,必须 })); } } customElements.define('user-card', UserCard); ``` ## 二、Lit 核心特性用法 | 特性 | 用途 | 要点 | |------|------|------| | `` html`...` `` | 声明式模板 | 自动转义(**天然防 XSS**);模板字面量按内容缓存,只更新变化部分 | | `` css`...` `` | 组件样式 | 在 Shadow DOM 内生效,样式隔离 | | Shadow DOM | DOM/CSS 隔离 | 默认 `open`;跨边界事件需 `composed: true` | | `@property` | 对外属性(UI 状态 / 入参) | 会触发重渲染,默认 attribute 反射 | | `@state` | 内部私有状态 | 不对外暴露、不反射到 attribute | | `requestUpdate()` / `updateComplete` | 手动触发 / 等待渲染 | 备选手段,优先用 property 驱动 | | `` | 插槽 | 封装通用布局组件(弹窗外壳、面板) | ## 三、响应式:本地 vs 全局(关键分界) > ⚠️ **两套响应式必须分开**,混用会导致更新时机错乱。 | 场景 | 怎么用 | |------|--------| | 组件内部 UI(弹窗开关、输入框值、tab 高亮) | Lit `@property` / `@state` + `this.requestUpdate()` | | 跨组件全局业务状态(登录态、字典、购物车、权限) | `@lit-labs/signals`,组件用 `SignalWatcher` 混入 | 需要全局 signal 的组件:用 `SignalWatcher(LitElement)` 包裹,signal 变化自动重渲染。 ```js 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`

你好,${authService.user.get()?.name ?? '访客'}

`; } } ``` ## 四、组件通信 | 方向 | 手段 | |------|------| | 父 → 子 | `static properties` 声明的属性(attribute 或 `.prop=${x}` 绑定) | | 子 → 父 | `dispatchEvent(new CustomEvent('x', { bubbles, composed }))` | | 任意 ↔ 任意 | **Service + Signal**(首选),组件只读 signal / 调方法 | | 组件树深层传递依赖 | `@lit-labs/context` 的 `provide()` 指令 + `ContextConsumer`,见 [context-di.md](./context-di.md) | - 事件名用 `kebab-case`,`detail` 用对象。 - 跨 Shadow DOM 边界必须 `composed: true`,否则父级监听不到。 - **禁止**用全局事件总线撒逻辑——用 Service。 ## 五、绝对禁止 - ❌ 用装饰器(`@customElement`/`@property`/`@consume`…)—— 无构建下浏览器不执行,见第〇节。 - ❌ 组件里 `fetch` / 写接口地址 —— 交给 Service。 - ❌ 组件里写复杂业务计算 / 校验 —— 交给 Service 或 utils。 - ❌ 组件 new 另一个组件需要的 Service —— 用 `ContextConsumer` 或直接 import 单例。 - ❌ 用全局 signal 存「只影响本组件」的状态 —— 用 `static properties` 的 `state: true`。 - ❌ 跨版本混用 `lit` —— 自定义元素注册冲突,见 [pitfalls.md](./pitfalls.md)。 ## 六、可复用组件的封装原则 - **只暴露语义属性**(`label`、`value`、`disabled`),不暴露内部实现细节。 - **用 slot 做内容投影**,不要用 `<... .innerHTML>` 传 HTML。 - **样式可覆盖**:暴露 CSS 自定义属性(`--card-bg`)而非改内部 class。 - 通用性强的组件抽到独立目录并考虑托管到 **FMODE Repose 组件库**,跨项目复用。