--- 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 进程还服务旧配置。**改配置必须重启,别信热更新。** ## 诚实失败 探测目录不覆盖已有文件;依赖解析失败明确报错。