---
name: fmode-code-lit
description: "Lit 原生 ESM 无构建前端。触发词:lit 组件、无构建、ESM。"
schema_version: 1.0.0
level: system
category: development
version: 1.0.0
author: Yuyang001 (FmodeAgent)
tags: [Lit, 无构建, 前端]
---
# fmode-code-lit — Lit 原生 ESM 无构建前端
用无构建/无打包/纯原生 ESM + Lit 3.x 开发前端:源码即产物,浏览器直跑;依赖走 FMODE Repose,业务码走 OBS+CDN。分层(组件/服务/工具)、signals 响应式、@lit/context DI、History 路由、多标签 KeepAlive。
## 触发
新建/初始化 lit 项目 · 写 lit 组件 · 无构建前端 · 原生 ESM+importmap · WebComponent · signals · context DI · 多标签 KeepAlive · 部署 OBS+CDN。
## 铁律
1. 源码即产物:禁 rollup/esbuild/vite/webpack。
2. 依赖不落地:lit 由 importmap 指向 FMODE Repose。
3. 分层不越界:组件禁 fetch/复杂业务,服务禁 DOM。
## 无构建硬约束
禁装饰器(@customElement/@property/@state/@consume/@provide),用静态字段+编程式 API。
## 框架边界(用户定调,必守)
- 新写项目默认可用无构建 Lit。
- **项目明确要求 Angular:沿用 Angular 不改 Lit。**
## 用法
scaffold.mjs 初始化 / install-rules.mjs 装 .claude/rules / new.mjs 生成组件服务页 / deploy.sh 部署。
## 关键约定
组件 kebab-case.js 类 PascalCase · 页面 page-*.js · 服务 *-service.js 单例 · 相对导入带 .js · 全局 signal+SignalWatcher · DI 用 @lit/context · 子目录 · CDN History 回退 200 禁 302。
## 部署
依赖→FMODE Repose(.js text/javascript、CORS 放行、HTTP/2);业务码→obsutil sync src/→OBS(public-read)+刷 CDN;index.html no-cache。
## 常见坑
装饰器/CORS/MIME 非 JS/多版本混用/CDN 302/base href 缺失/漏 .js/signal 与 property 混用/老浏览器无 ImportMap。
## 多页面开发铁律(light DOM 五条死线)
> 来源:tongjisheji/report-review 首次交付 6 页全部错乱的复盘(2026-10-29)
> **一句话**:`createRenderRoot(){return this}` 不是“让 Tailwind 生效的小开关”,是**渲染模型切换**。切完以后 ``、`:host`、`::slotted`、样式隔离全部失效,但代码看起来和普通 Lit 一样、编辑器也不报错。
### 死线1:light DOM 下禁用 ``
没有 shadow root 就没有投影。`` 退化成普通未知元素,`assignedSlot` 为 null,**子节点原封不动滞留在宿主元素上**——外壳和内容成了兄弟节点,谁也没包住谁,于是滚动区空、导航错、高度链断。
**自检一行**:`document.querySelector('main').children.length`,为 0 就是投影断了。
### 死线2:外壳接管路由渲染,不靠投影传内容
页面元素由外壳**自己按路由产出**,让它天然成为 `` 的子节点,父子关系由渲染函数保证:
```js
// app-shell.js
_renderRoute() {
const { name, params } = parseRoute(this.path);
switch (name) {
case 'dashboard': return html``;
case 'project-detail': return html``;
}
}
// render 里:${this._renderRoute()}
```
`app-root` 退化成只负责挂载:`render() { return html\`\` }`。
### 死线3:真需传内容的组件(卡片),用 capture/project 手动搬
light DOM 下只能自己搬节点。现成实现见 `references/light-slot.js`。**顺序不能错**:
- `capture(host)` 在 **connectedCallback 中、渲染前** 调用,把 `childNodes` 存到 `host.__lightIncoming`
- `project(host)` 在 **updated() 中、渲染后** 调用,把暂存的节点塞进 `[data-light-slot="body"]` 等容器
- **必须靠 `dataset.projected` 做幂等**,否则每次重渲染都重复搬、节点来回跳
- 渲染后再扫宿主子节点是致命的:会把组件自己刚生成的内容当成“外部传入”搬走,越搬越乱
### 死线4:自定义元素必须显式 `display: block; min-width: 0`
自定义元素是未知元素,浏览器默认按 inline 处理:不吃 height/width(`h-full`/`flex-1` 全失效)、不形成块级盒子(内部 flex/grid 失去参照)、相邻元素间出现空白间隙。
```css
app-shell, page-dashboard, page-analysis, data-table, kpi-card, empty-state, ... {
display: block; min-width: 0; /* min-width:0 让 flex 子项可收缩,否则长表格撑破父容器 */
}
```
**两个例外必须排除**:`section-card`、`chart-box` 的 class 自带 `flex h-full flex-col`,写死 `display: block` 会覆盖掉、把高度链断掉。
### 死线5:图表容器用 `height:100% + min-height`,不用 `flex:1`
```css
chart-box { height: 100%; min-height: 260px; } /* 父级无确定高度时 flex:1 算出来是 0,ECharts 拿到 0 高度直接不画 */
```
### 交付前自检清单
1. **先跑通最小路径再铺量**:外壳 + 一个空白页 + 一次路由跳转,确认 DOM 父子关系正确,再往里填 6 个页面。规模越大定位成本越高。
2. `node scripts/check-elements.mjs` 自动校验所有自定义元素都声明了 display —— 自动检查比文档写三遍有用。
3. 长模板用 `node --check` 检查语法:**少一个闭标签,lit-html 会把后面所有兄弟节点当成它的子节点吞掉**,整页塌陷,且症状和渲染模型错误混在一起,排查方向严重跑偏。
4. 症状≠根因:“样式全乱”先去查 DOM 结构,别一上来翻 Tailwind 配置(这次查错方向错了两轮)。
5. **多页症状不一致时先找共同根因**:各页症状不同通常是同一个总根因的不同投影,不是 n 个独立 bug。最严重的那页反而最好的线索。
### 同批高频白屏(非 Lit 问题,但一起记)
- **子路径白屏**:index.html 用绝对路径引资源,部署到 `/dev/XXX/` 子路径全 404。用 `__BASE__` 占位 + `document.write` 注入。
- **改配置后白屏**:dev-server 进程还服务旧配置。**改配置必须重启,别信热更新。**
## 诚实失败
探测目录不覆盖已有文件;依赖解析失败明确报错。