--- name: "fmode-code-lit" description: "用 Lit 原生 ESM 无构建方式写前端(零编译/零打包/浏览器直跑)。触发词:lit 项目、lit 组件、WebComponent、Web Component 组件库、纯 ESM 核心库、跨框架组件、无构建前端、原生 ESM、importmap、@lit-labs/signals、@lit-labs/context、多标签 KeepAlive、FMODE OBS+CDN 前端部署。适用场景:(1) 新建 lit 无构建项目并生成目录/importmap/骨架 (2) 写 LitElement 组件/Service/路由 (3) 把逻辑层抽成纯 ESM 核心库(core,零浏览器依赖、Node 可测)或把交互层做成跨框架 Web Component 组件库(wc) (4) 给已有项目补齐前端开发规则 (.claude/rules) (5) 部署到 OBS+CDN 并配 History 回退。产出遵循 fmode-code-lit 规则仓的分层与命名规范。" version: "1.0.0" license: "MPL-2.0" author: "Fmode (未来飞马)" keywords: [lit, 项目, lit, 组件, WebComponent, Web, Component, 组件库, 纯, ESM, 核心库, 跨框架组件] --- # fmode-code-lit — Lit 原生 ESM 无构建前端 ## Overview 教 AI 用 **无构建 / 无打包 / 纯原生 ESM + Lit 3.x** 的方式开发前端:源码即产物,浏览器直接跑, 依赖走 **FMODE Repose** 统一托管,业务码走 **OBS + CDN**。分层(组件 / 服务 / 工具)、 `@lit-labs/signals` 响应式、`@lit/context` 依赖注入、自写 `utils/router.js` History 路由(子路径部署**禁用** `@lit-labs/router`)、后台多标签 KeepAlive。 > 完整规则见 `fmode-code-lit` **规则仓**(本技能随仓发布)。 > 本技能负责「**初始化 + 生成代码**」,规则仓负责「**规范细则**」。 ## 何时触发 用户出现以下意图时启用: - 「新建 / 初始化一个 **lit 项目**」「写一个 **lit 组件**」「**无构建** 前端」「**原生 ESM** + importmap」 - 「WebComponent / Shadow DOM 组件」「signals 响应式」「context 依赖注入」 - 「后台多标签 / KeepAlive」「History 干净路由」 - 「把这套 **前端开发规则** 给项目装上」「部署到 OBS+CDN」 ## 三条铁律(生成任何代码前先记住) 1. **源码即产物**:不引入 rollup/esbuild/vite/webpack。写好的 `.js` 直接上传。 2. **依赖不落地项目**:`lit`、`@lit/*`、`@lit-labs/*` 由 **importmap** 指向 FMODE Repose,禁止把依赖拷进业务仓。 3. **分层不越界**:组件→服务→工具。组件禁 `fetch`/复杂业务;服务禁 DOM。 ## 两个方向:页面 & 库(同一套铁律) ``` [core] 纯 ESM 核心库 零浏览器依赖、Node 可测 — 跨前端框架最底层 ▲ import [wc] 组件库(Lit 实现的 Web Component) 仅浏览器交互、零业务 — 跨框架直接用 ▲ import [page] Lit 无构建页面 组合 core 与 wc,持有路由/布局/服务 ``` **core 不认识 DOM,wc 不认识业务,page 组装一切。** | 需求 | 写在哪 | 生成命令 | 规则篇 | |------|--------|----------|--------| | 逻辑要在 Node + 多框架复用 | `lib/core/` | `new.mjs core ` | `references/rules/core-library.md` | | UI 要跨框架(Lit/Vue/React/原生) | `lib/wc/` | `new.mjs wc ` | `references/rules/web-component-library.md` | | 只做本应用的页面 | `src/routes/` | `new.mjs page ` | `references/rules/components.md` | - **core 铁律**:不 `import lit`、不碰 `window`/`document`/`fetch`;`now`/`id`/`transport` 等不确定输入**注入**;`node --test` 可测。 - **wc 铁律**:**必须用 Lit 框架特性写**(`extends LitElement` + `html` + `css`,**禁手写原生 wc**:`extends HTMLElement` / `attachShadow()` / 手写 `observedAttributes`);逻辑来自 core;网络由宿主注入 `transport`;只暴露语义属性 + `composed` 事件 + slot + CSS 变量;标签加统一前缀;`customElements.define` **判重**。 > Lit 本身构建在 Web Component 标准之上,产物即标准自定义元素 → 「用 Lit 写」与「跨框架」不冲突。 ## ⚠️ 无构建环境的硬约束(最容易踩) **禁用一切装饰器语法**:`@customElement` / `@property` / `@state` / `@consume` / `@provide` / `@query`。 装饰器是 TS/Babel 编译期特性,浏览器原生 JS **不执行**,照抄 Lit 官方「装饰器版」文档会直接 `SyntaxError`。 一律改用标准静态字段 + 编程式 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 } }` | | `@consume({context})` | `new ContextConsumer(this, { context, subscribe, callback })` | | `@provide({context})` | 模板里 `provide(ctx, value)` 指令 | ## 用法 ### 1) 初始化一个新 lit 无构建项目 ```bash # 在目标目录生成标准骨架(index.html + importmap + 分层目录 + 示例组件) node /scripts/scaffold.mjs ./my-admin --project my-admin ``` 生成结构: ``` my-admin/ src/ index.html # + importmap + 入口 importmap.json # 本地 importmap(生产用 Repose 公共 importmap) app.js # provide + 路由 + 根组件 components/ routes/ services/ utils/ .claude/rules/ # 前端规则(从规则仓同步) package.json # 仅类型提示,不打包 ``` ### 2) 给已有项目装上规则 ```bash node /scripts/install-rules.mjs . # 写入 ./.claude/rules/ + CLAUDE.md 片段 ``` ### 3) 生成单个组件 / 服务 / 核心库模块 / 组件库组件 ```bash node /scripts/new.mjs component user-card # 页内通用组件 node /scripts/new.mjs service auth-service # 页面服务 node /scripts/new.mjs page page-order-list # 页面 node /scripts/new.mjs core pricing # 核心库模块(纯 ESM,零 DOM) node /scripts/new.mjs wc quote-calc # Web Component(跨框架组件库) ``` ### 4) 部署 ```bash bash scripts/deploy.sh # obsutil 同步 src/ → OBS → CDN 刷新 ``` ## 关键约定速查 | 项 | 约定 | |----|------| | 组件文件 | `kebab-case.js`,类 `PascalCase`,标签含连字符 | | 页面 | `page-*.js`,标签 `` | | 服务 | `*-service.js`,类 `*Service`,单例 `export const xService = new XService()` | | 核心库(core) | `lib/core/*.js`,纯函数/类,**无 `window`/`document`/`fetch`,不 import lit**;入口 `index.js` 单一出口 | | 组件库(wc) | `lib/wc/*.js`,**`extends LitElement`(Lit 实现,禁原生 wc)**,`customElements.define`(**判重**),标签带统一前缀;逻辑来自 core | | 相对导入 | **必须带 `.js` 后缀** | | 全局状态 | `@lit-labs/signals` 的 signal + `SignalWatcher` | | 组件本地状态 | `static properties`(`state: true`) | | 依赖注入 | `@lit/context` 的 `provide()` + `ContextConsumer`(**非** `@lit-labs/context`,已废弃) | | 路由 | 自写 `utils/router.js`,History 干净 URL(子路径部署**禁用** `@lit-labs/router`) | | 子目录部署 | 必须 `` | ## 依赖版本(2026-10 实测可用) ``` lit@3.3.3 lit-html@3.3.3 lit-element@4.2.2 @lit/reactive-element@2.1.2 @lit/context@1.1.6 @lit-labs/signals@0.3.0 signal-polyfill@0.2.2 ``` > 注意:`@lit-labs/signals` 依赖 **`signal-polyfill`**,importmap 必须映射它(入口 `dist/index.js`)。 > `@lit-labs/context` 已废弃 → 用 **`@lit/context`**。 ## 部署要点 - 依赖 → FMODE Repose(按版本目录,`.js` 必须 `Content-Type: text/javascript`,CORS 放行业务域名,开 HTTP/2)。 - 业务码 → `obsutil sync src/ obs:////`(`public-read`),再刷 CDN。 - **CDN History 回退**:不存在路径返回 `index.html` 且状态码 **200**,**禁 302**(否则地址栏改写、OAuth 回调失败、路由丢失)。 - `` 与部署前缀一致;`index.html` 设 `no-cache`。 ## 常见坑(详见规则仓 `references/rules/pitfalls.md`) 装饰器 / CORS / MIME 非 JS / lit 多版本混用 / CDN 302 / base href 缺失 / 相对 import 漏 `.js` / signal 与 property 混用 / 老浏览器无 Import Map / 循环依赖。 ## 多页面开发铁律(light DOM 五条死线) > 来源:一次多页看板首次交付 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 进程还服务旧配置。**改配置必须重启,别信热更新。** ## 诚实失败 生成前先探测目标目录;已存在文件**不覆盖**(除非 `--force`)。若依赖版本无法从 Repose 解析, 明确报错并给出配置指引,**不生成一个跑不起来的骨架**。