SKILL.md 6.1 KB


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 生效的小开关”,是渲染模型切换。切完以后 <slot>、:host、::slotted、样式隔离全部失效,但代码看起来和普通 Lit 一样、编辑器也不报错。

死线1:light DOM 下禁用 <slot>

没有 shadow root 就没有投影。<slot> 退化成普通未知元素,assignedSlot 为 null,子节点原封不动滞留在宿主元素上——外壳和内容成了兄弟节点,谁也没包住谁,于是滚动区空、导航错、高度链断。 自检一行:document.querySelector('main').children.length,为 0 就是投影断了。

死线2:外壳接管路由渲染,不靠投影传内容

页面元素由外壳自己按路由产出,让它天然成为 <main> 的子节点,父子关系由渲染函数保证:

// app-shell.js
_renderRoute() {
  const { name, params } = parseRoute(this.path);
  switch (name) {
    case 'dashboard': return html`<page-dashboard></page-dashboard>`;
    case 'project-detail': return html`<page-project-detail .code=${params.code||''}></page-project-detail>`;
  }
}
// render 里:<main ...><div class="page-enter key-route-${this.active}">${this._renderRoute()}</div></main>

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 失去参照)、相邻元素间出现空白间隙。

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

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 进程还服务旧配置。改配置必须重启,别信热更新。

诚实失败

探测目录不覆盖已有文件;依赖解析失败明确报错。