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 (未来飞马)"
教 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规则仓(本技能随仓发布)。 本技能负责「初始化 + 生成代码」,规则仓负责「规范细则」。
用户出现以下意图时启用:
.js 直接上传。lit、@lit/*、@lit-labs/* 由 importmap 指向 FMODE Repose,禁止把依赖拷进业务仓。fetch/复杂业务;服务禁 DOM。[core] 纯 ESM 核心库 零浏览器依赖、Node 可测 — 跨前端框架最底层
▲ import
[wc] 组件库(Lit 实现的 Web Component) 仅浏览器交互、零业务 — <my-x> 跨框架直接用
▲ import
[page] Lit 无构建页面 组合 core 与 wc,持有路由/布局/服务
core 不认识 DOM,wc 不认识业务,page 组装一切。
| 需求 | 写在哪 | 生成命令 | 规则篇 |
|---|---|---|---|
| 逻辑要在 Node + 多框架复用 | lib/core/ |
new.mjs core <name> |
references/rules/core-library.md |
| UI 要跨框架(Lit/Vue/React/原生) | lib/wc/ |
new.mjs wc <name> |
references/rules/web-component-library.md |
| 只做本应用的页面 | src/routes/ |
new.mjs page <name> |
references/rules/components.md |
import lit、不碰 window/document/fetch;now/id/transport 等不确定输入注入;node --test 可测。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) 指令 |
# 在目标目录生成标准骨架(index.html + importmap + 分层目录 + 示例组件)
node <skill_dir>/scripts/scaffold.mjs ./my-admin --project my-admin
生成结构:
my-admin/
src/
index.html # <base> + importmap + 入口
importmap.json # 本地 importmap(生产用 Repose 公共 importmap)
app.js # provide + 路由 + 根组件
components/ routes/ services/ utils/
.claude/rules/ # 前端规则(从规则仓同步)
package.json # 仅类型提示,不打包
node <skill_dir>/scripts/install-rules.mjs . # 写入 ./.claude/rules/ + CLAUDE.md 片段
node <skill_dir>/scripts/new.mjs component user-card # 页内通用组件
node <skill_dir>/scripts/new.mjs service auth-service # 页面服务
node <skill_dir>/scripts/new.mjs page page-order-list # 页面
node <skill_dir>/scripts/new.mjs core pricing # 核心库模块(纯 ESM,零 DOM)
node <skill_dir>/scripts/new.mjs wc quote-calc # Web Component(跨框架组件库)
bash scripts/deploy.sh <project> # obsutil 同步 src/ → OBS → CDN 刷新
| 项 | 约定 |
|---|---|
| 组件文件 | kebab-case.js,类 PascalCase,标签含连字符 |
| 页面 | page-*.js,标签 <page-*> |
| 服务 | *-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) |
| 子目录部署 | 必须 <base href="/<project>/"> |
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。
.js 必须 Content-Type: text/javascript,CORS 放行业务域名,开 HTTP/2)。obsutil sync src/ obs://<bucket>/<project>/(public-read),再刷 CDN。index.html 且状态码 200,禁 302(否则地址栏改写、OAuth 回调失败、路由丢失)。<base href> 与部署前缀一致;index.html 设 no-cache。references/rules/pitfalls.md)装饰器 / CORS / MIME 非 JS / lit 多版本混用 / CDN 302 / base href 缺失 / 相对 import 漏 .js /
signal 与 property 混用 / 老浏览器无 Import Map / 循环依赖。
来源:一次多页看板首次交付 6 页全部错乱的复盘(2026-10-29) 一句话:
createRenderRoot(){return this}不是「让 Tailwind 生效的小开关」,是渲染模型切换。切完以后<slot>、:host、::slotted、样式隔离全部失效,但代码看起来和普通 Lit 一样、编辑器也不报错。
<slot>没有 shadow root 就没有投影。<slot> 退化成普通未知元素,assignedSlot 为 null,子节点原封不动滞留在宿主元素上——外壳和内容成了兄弟节点,谁也没包住谁,于是滚动区空、导航错、高度链断。
自检一行:document.querySelector('main').children.length,为 0 就是投影断了。
页面元素由外壳自己按路由产出,让它天然成为 <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\` }`。
light DOM 下只能自己搬节点。现成实现见 references/light-slot.js。顺序不能错:
capture(host) 在 connectedCallback 中、渲染前 调用,把 childNodes 存到 host.__lightIncomingproject(host) 在 updated() 中、渲染后 调用,把暂存的节点塞进 [data-light-slot="body"] 等容器dataset.projected 做幂等,否则每次重渲染都重复搬、节点来回跳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 会覆盖掉、把高度链断掉。
height:100% + min-height,不用 flex:1chart-box { height: 100%; min-height: 260px; } /* 父级无确定高度时 flex:1 算出来是 0,ECharts 拿到 0 高度直接不画 */
node scripts/check-elements.mjs 自动校验所有自定义元素都声明了 display —— 自动检查比文档写三遍有用。node --check 检查语法:少一个闭标签,lit-html 会把后面所有兄弟节点当成它的子节点吞掉,整页塌陷,且症状和渲染模型错误混在一起,排查方向严重跑偏。/dev/XXX/ 子路径全 404。用 __BASE__ 占位 + document.write 注入。生成前先探测目标目录;已存在文件不覆盖(除非 --force)。若依赖版本无法从 Repose 解析,
明确报错并给出配置指引,不生成一个跑不起来的骨架。