mcpField
M
marko6

marko6

github.com/svallory开发5142 次安装5 周前更新
代码仓库

关于 marko6

marko6 是供编写或修改 `.marko` 文件的 Agent 使用的 Marko 6 语言参考资料,涵盖语法、响应性、类型和渲染规则,并明确不包含 Marko 5。

主要功能

  1. 01Marko 6 语法参考
  2. 02响应式状态与更新规则
  3. 03TypeScript 和模板类型
  4. 04服务端与客户端序列化说明
  5. 05标签发现与资源约定

适用场景

  1. 01编写或修改 `.marko` 组件
  2. 02排查响应性和渲染问题
  3. 03检查 Marko 专用 TypeScript 类型
  4. 04避免服务端与客户端序列化错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/svallory/marko-ui --skill marko6

安装后在 Claude Code 中输入/marko6确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
core-tags.md
custom-tags-styling.md
language.md
native-tags.md
patterns.md
template-api.md
typescript.md

详细介绍

Marko 6 语言

从完整的 Marko 6 文档(参考、说明、指南、教程)中提炼而成(有意排除 Marko 5/类 API)。此文件列出 Agent 容易出错的规则和速查内容。详细内容见 references/。如果此处内容与观察到的行为不符,以在线文档为准——Marko 更新很快:索引见 https://markojs.com/llms.txt;在任意页面 URL 末尾添加 .md,即可获取 Markdown 格式页面(例如 https://markojs.com/docs/reference/core-tag.md)。

仅限 Marko 6——绝不编写 Marko 5

一眼识别并拒绝:class { onCreate/onMount... } 代码块、$ const x = ... 脚本片段、<if(cond)>/<for(x in y)> 括号参数、字符串事件处理器 on-click("handleClick")、state/this.state、<include>/<invoke>、components/ 自动发现机制、Marko.Component 类型。Marko 6 的对应写法:<let>/<const>/<script>/<lifecycle>、<if=cond>、<for|x| of=y>、onClick() {}、tags/ 自动发现机制、export interface Input。

新建或编辑任何 .marko 文件时的检查清单

  • 仅使用 Marko 6 语法(见上文)。
  • 优先使用状态机制:如果 <let>/<const>/可控原生元素(value:=)可以完成任务,就不要使用 <script>/<lifecycle>;只有真正需要浏览器副作用时才使用 <script>。
  • 事件处理器必须是函数(onClick() {}/onClick=fn),绝不能是字符串;参数使用 (event, element),不要用 currentTarget。
  • 声明 export interface Input;使用 mtc(@marko/type-check)进行类型检查——tsc 会跳过 .marko 并报告成功。
  • 尽量减少客户端 JS:优先使用 HTML/CSS(<details>、<dialog>、:has、popover),而不是脚本行为;无论如何,只有响应式部分会被打包。
  • 保持组件精简;按关注点拆分,而不是按技术拆分(拆分不会影响打包体积)。
  • 确保 SSR 安全:<let> 中不要放置无法序列化的内容;$signal/元素引用仅用于 <script>/<lifecycle>/事件处理器。
  • 使用不可变更新(x = x.concat());可重新排序的 <for> 使用 by=;有意选择 <show> 或 <if>。
文件阅读时机
references/language.md语法:语句、属性及简写、内容、属性标签、标签变量/参数、动态标签、精简模式、空白字符
references/core-tags.md<if> <show> <for> <let> <const> <return> <script> <style> <define> <lifecycle> <id> <await> <try>,以及响应式/调度模型
references/native-tags.mdclass=/style=/content=、事件委托、可控的 value:=/checked:=/open:=、展开属性的归属
references/typescript.mdInput、Marko.* 类型、扩展原生元素/全局变量/CSS、标签中的 TS 语法
references/template-api.mdrender/mount、$global(serializedGlobals、cspNonce、renderId、runtimeId)
references/custom-tags-styling.md标签发现顺序、tags/ 目录、marko.json、样式、惰性 with { load }
references/patterns.md可控组件、嵌套状态、可序列化/不可变状态、流式处理、工具

本仓库的组件架构(Zag 接线、属性标签 API):skills/port-to-marko/references/marko-gotchas.md。

心智模型(10 行)

  1. 文件 = 组件。input = 属性。tags/ 目录会自动发现 <tag-name>;PascalCase 变量是标签;<${...}> 中的字符串始终是原生元素。
  2. 仅模块语句:import、export、static、server、client。其他所有内容都是模板主体(响应式)。
  3. 响应式来源:标签变量(/x)、标签参数(|x|)、input。图在编译时生成,不会在运行时跟踪。表达式必须是纯函数。
  4. <let> = 状态(赋值触发响应,更新不可变,SSR 时序列化)。<const> = 派生值(惰性求值,在刷新时执行)。static const = 模块常量。
  5. 更新会在微任务后批量处理。执行 count++ 后,<let> 是新的,但所有 <const>/DOM 在刷新前都是过期的。
  6. <script> = 仅在浏览器运行的副作用,会在引用的标签变量/参数变化时重新执行;通过 $signal.onabort 清理。<lifecycle> = 使用持久化 this 的 onMount/onUpdate/onDestroy。
  7. 原生标签变量 <div/el/> 是一个getter 函数,仅在浏览器中可用,且只能在渲染后使用。
  8. 一切皆可控:value:=x ≡ value=x valueChange(v){x=v};成员表达式上等价于 value=input.x valueChange=input.xChange。<let/x:=input.x> 会让组件变为可控组件。
  9. 服务端/客户端边界:只有可序列化的普通数据可以跨越;状态中不能有类实例、DOM 节点或外部模块闭包;除非 $global 在 serializedGlobals 中,否则它留在服务端。
  10. 默认不生成 JS:只有响应式部分会发送到浏览器;组件粒度不会改变打包体积。

快速参考 — 容易踩坑的规则

语法
  • 属性值中未转义的 > → value=(a > b)。>= 和 => 可以。
  • <div#id.cls.cls2/> 是简写;<Comp.x/> 表示 class="x",不是属性访问 → <${Comp.x}/>。
  • 简洁模式下,根节点裸文本会被解析为标签 → -- text。多行属性:行首加逗号。
  • 属性字符串是 JS 字面量;aria-* 枚举属性需要字符串:aria-pressed=on && "true"。
  • null|undefined|false 属性会被跳过;0、""、NaN 会被写入。文本中会跳过 null|undefined|false|""|NaN|0n,而 0 会被渲染。
  • 两个行内标签之间的换行 = 没有空格。请在同一行中添加空格。
  • <if>/<else if=…>/<else> 必须是直接兄弟节点。文档使用 <else if=cond>;在 6.3.34 版本中,<else-if=cond> 也可以编译(已验证)。
  • <${input.content}/> 会转发内容;原生标签也接受 content=input.content 或直接使用 <div ...input/>。字面量主体(甚至是注释)会覆盖 content=。
  • 组件的 content 属性必须是其默认主体的全部内容——Marko 会自动根据标签的非属性标签子节点设置 input.content。调用方无需为此编写 <@content>;直接将主体内容放入即可(参数也一样:<Tag|params|>body</Tag>,而不是 <@content|params|>)。如果组件需要两个或更多并列的、形似主体的插槽,请为它们命名(例如 main),不要命名为 content——content 表示“主体的其余部分”,第二个同形插槽会导致冲突。
  • 重复的 <@item> → input.item 是首个值加可迭代对象:<for|it| of=input.item> 或 [...input.item || []]。类型为 Marko.AttrTag<{...}>。
  • 标签参数:<child|{ n }|>;参数形式 <${input.content}(1,2)/> → |a, b|。属性和参数不能同时使用。属性标签无法读取参数。
  • 结束标签名可以省略(</>);动态标签使用 </> 闭合。
响应性与时序
  • <let/x=input.x> 不会跟随之后的 input.x 变化。请使用 <let/x:=input.x>(可控),或直接读取 input.x。
  • 修改操作不具备响应性:arr.push() ❌ → arr = arr.concat();obj.k = v ❌ → obj = { ...obj, k: v }。
  • 在处理程序中,从 <let> 重新计算;刷新完成前,<const> 的值和 DOM 都是陈旧的。
  • <if> 会销毁并重新创建状态;<show> 会保持挂载。使用 <for ... by="id">,以便在重新排序时保留状态。
  • $signal 只能在 <script>/<lifecycle>/处理程序中使用(在服务器端其他位置使用会抛出错误)。在 await 之前捕获它。
  • <lifecycle>:从 onMount 返回一个对象(直接返回清理函数会被丢弃);不要覆盖 this 上的属性。
  • <return>:每个模板/内容只能有一个,且必须位于顶层;通过改变其 value= 来调整返回值。
原生标签与事件
  • 处理程序参数为 (event, element);不要使用 event.currentTarget(事件采用委托方式)——请使用第二个参数或 ref。Marko 处理程序在原生监听器之前运行;不会冒泡的事件只能在目标上处理。
  • <input value:=n> 会得到字符串 → 使用 value:parseFloat:=n / valueChange(v){ n = +v }。
  • <select value:=x>:每个 <option> 都需要 value=;不要使用 selected=。
  • style={} 的键使用连字符格式,不会推断单位;条件声明应放在数组层级。
  • 展开属性会独占属性(之后缺失时会被移除);请将固定属性写在展开属性之后。
  • <details open:=o>、<dialog open:=o>;showModal() 不会触发 openChange。
类型
  • export interface Input {}(单独的 interface 会导致解析错误)。Marko.Body<[params], { value }>, Marko.AttrTag<T>, Marko.HTML.Button 用于扩展原生标签,Marko.Global 用于扩充全局类型,Marko.NativeTags 用于自定义元素。请使用 mtc(@marko/type-check)进行类型检查,而不是 tsc。
发现与资源
  • 仅搜索 tags/ 目录(向上查找);同级文件需要 import。包通过 marko.json 的 exports 导出。相邻的 style.css / foo.style.css 会自动附加。
  • <style> 为全局样式,且只处理一次;<style/styles> 是 CSS Modules;<style> 中的 ${} → 自定义属性,仅限声明值,且必须位于所设置样式的内容上方。
  • 延迟加载:import X from "<x>" with { load: "visible#id | idle?timeout=2000" } 使用静态字符串;使用 facade 标签集中管理。
模板 API
  • T.render(input) 只能消费一次:使用 for await、.pipe(res)、.toReadable()、await、.toString()(仅同步)。T.mount(input, node, pos) → update/destroy/value。
  • 使用 $global.serializedGlobals 将全局变量暴露给客户端(绝不能包含机密);cspNonce;renderId/runtimeId 必须是标识符安全的。

已知 Agent 错误目录(已根据此仓库验证)

已通过 packages/shadcn/ui/ 中的真实组件和 notes/bug-marko-dynamic-tag-hydration-crash.md 验证。

  • <if>/<else> 必须是模板中的直接同级项——<else> 不能嵌套在包装元素中,而且 </if> 与 <else> 之间不能有任何内容(包括仅含空白的文本表达式),只能有其他 <else-if>/<else> 标签。

    
    <if=a>A</if>
    <else-if=b>B</else-if>
    <else>C</else>
    
    <div>
      <if=a>A</if>
    </div>
    <else>C</else>
    
  • 顶层裸写的 interface/type/const 会被解析为标签,而不是 TypeScript。 每个顶层 TS 声明都必须以 export 开头(运行时的 const/function 则使用 static)。

    
    interface Input { name: string }
    
    export interface Input { name: string }
    
    const PAGE_SIZE = 20;
    
    static const PAGE_SIZE = 20;
    
  • !/ 存在歧义——Marko 将 !(未转义 HTML 标记 $!{...})和 /(标签变量标记 <tag/name/>)作为相邻的单字符符号重载;手写 !/ 序列可能会被解析成错误的符号。此技能尚未通过编译器独立复现(发布前未找到最小复现案例——在验证前应视为可信度较低)——无论如何,安全规则是:始终使用显式的 $!{expr} 形式输出未转义内容;并且不要在未转义输出位置后直接放置标签变量,除非用空格隔开或调整属性顺序。

    
    <div>$!{input.articleHtml}</div>
    
  • 自递归标签导入 — 标签导入自身(例如,树节点组件渲染自身的子节点)时,必须通过相对文件自定义标签路径或显式导入其自身已编译文件的 PascalCase 名称来导入自身,绝不能像引用已是全局自定义标签那样引用自身的裸标签名;相对路径发现机制只会查找 tags/ 目录下的标签(参见 https://markojs.com/docs/reference/custom-tag),因此,同目录中的递归组件需要显式导入自身。

    
    import TreeNode from "./tree-node.marko";
    <li>
      ${input.label}
      <if=input.children?.length>
        <ul>
          <for|child| of=input.children>
            <TreeNode ...child/>
          </for>
        </ul>
      </if>
    </li>
    
  • SSR→hydration 边界可序列化契约(闭包封装规则)。 从服务端渲染的 <let>/响应式状态传到客户端的每个值,都必须是普通的可序列化数据——绝不能是带方法的活动对象(例如 Zag 服务、connect() 的结果,或持有闭包的任何类实例)。这是 Marko 可恢复性模型的 REQUIREMENT,并非需要绕过的 bug:如果恢复过程出现损坏,默认判断应是“仍有不可序列化的值跨越了边界”,而不是“Marko 出问题了”。修复方法是只序列化原始值,并在模板编写的闭包中重新派生任何有状态的内容(<zag/api=() => mod .../> 上的模块 getter,或 <zag>/<zag-machine> 上的 props=);这些闭包会在两端分别重新运行,绝不要将原始值通过 input 传递。

    
    <let/api=switchMachine.connect(service, normalizeProps)>
    
    <zag/api=() => switchMachine from=input/>
    
  • 遍历顺序导致的 <const> 隐患——已在上游 marko 6.3.46 中修复,无需变通方案。 当组件通过动态标签内容(<${input.content}/>) 挂载,且嵌套在某个祖先组件自身的 portal/动态标签挂载内时,该组件在遍历时读取自身由 <const> 派生的值——包括 <if>/<for> 分支选择器、属性展开,以及首次挂载遍历期间求值的任何内容——可能会在该新作用域中的 <const> 延迟/信号推迟值尚未计算时触发,进而抛出 TypeError: ... is not a function。这是 @marko/runtime-tags 中的真实缺陷,已由 marko-js/marko#4062 修复,并随 @marko/[email protected] 发布。普通的 <zag/api=...> 加上每个调用点的 api() 是正确的常规模式;不要重新引入 connectFresh 风格的静态函数镜像。

    
    <zag/api=() => machine from=input/>
    <span ...api().getRootProps()>
      <if=api().open>...</if>
    </span>
    

    历史说明:修复合入之前,calendar.marko/avatar.marko/radio-group.marko 各自包含一个模块级 static function connectFresh(...),它会直接针对原始服务调用 machine.connect(),完全绕过 <const>,作为变通方案。工作区将 marko 固定版本更新到 6.3.46 后(2026-08-26),这三个组件都恢复为普通的 api()。完整的事故说明(包括尝试过但仍会崩溃的镜像形式)见:notes/bug-marko-dynamic-tag-hydration-crash.md。

  • 死作用域闭包风险:在 SSR 时未渲染的内容会丢失捕获的变量——通过标签参数传递条目数据,不要使用闭包。 闭包捕获本身有效:<for|person|> 中的 <@item> 正文读取 ${person},在服务器端和常规的水合遍历中都能正确渲染(始终打开的控制结构已验证这一点)。风险仅与可恢复性有关:每个内容正文都会被序列化为对其所有者作用域的引用(即保存循环绑定的每次迭代 <for> 作用域);从未在 SSR 期间渲染的正文(例如位于关闭的 <if> 后面)不会获得序列化的所有者作用域,因此当它首次在仅客户端挂载时触发,任何通过闭包进行的读取——无论是 ${person} 占位符,还是 value=person 属性表达式——都会访问未定义的作用域并抛出 TypeError: Cannot read properties of undefined (reading 'person')(压缩后:(reading '2') / 'N')。标签参数从结构上规避了这个问题:数据在触发时通过渲染调用传递(<${entry.content}(entry.value)/> + <@item|name| value=person> 读取 ${name}),而不是通过序列化的作用域引用传递。因此,类似菜单的组件应始终通过参数向条目内容传递数据。已在 Marko playground(v6.3.45,SSR+水合)中通过对照组验证。说明和 playground 链接:notes/bug-marko-dynamic-tag-hydration-crash.md;最小复现:scratch/content-closures-repro/。

  • 按设计,JSON.stringify(input.item) 只显示第一个属性标签——重复属性标签对象是“第一项 + 可迭代对象”。stringify 只会序列化自身的可枚举属性。使用 [...input.item].length 计数,绝不要根据 Array.isArray(input.item) 进行分支判断(它永远不是数组——Array.isArray(x) ? x : [x] 会静默丢弃除第一个标签之外的所有标签;直接展开即可:[...(input.item ?? [])])。

4. 简洁模式与 HTML 模式的易错点

来源:https://markojs.com/docs/reference/concise-syntax

  • 简洁模式下,根级裸文本会被误解析为标签。 文本需要使用 -- 分隔符(或放在 HTML 模式标签的内容中)。

    
    Hello World
    Welcome to Marko
    
    -- Hello World
    -- Welcome to Marko
    
  • 简洁模式下跨行的属性列表使用行首逗号,按照惯例,每个续行的行首都要加逗号(而不是行尾)。

    
    div id="hello" class="world",
      style={ border: "1px solid red" }
    
  • 文件以简洁模式开始,并在遇到第一个 HTML 语法(带方括号)的标签时切换到 HTML 模式——文件级别允许混用(切换一次即可,不要逐个标签来回切换),但同一个标签实例必须完整地使用其中一种写法。

  • 简洁模式的属性值中,未括在括号内的 > 需要加括号——裸 > 会被解析为标签结束符;>=(两侧有空格)和 => 不会产生歧义,因此不受此限制。

    
    <if=count > 0>...</if>
    
    <if=(count > 0)>...</if>
    

已验证的示例

上文所有非平凡的 .marko 示例(有意写错的片段除外)都放入 apps/docs/ 下的临时文件中(以便解析 marko 自身的依赖树),并使用 node_modules/.bun/[email protected]/node_modules/@marko/compiler/dist/index.js 中的 compileFile 进行编译,再通过 bun 运行(普通的 node 无法解析 bun 的 .bun/ 存储布局中嵌套的 marko/translator 自引用——必须使用 bun <script>.mjs,与项目自身工具所依赖的编译器解析方式一致)。全部 17 个有效示例均编译成功:

OK: 01-class-id-shorthand.marko -> code length 293
OK: 02-dynamic-native-tag.marko -> code length 771
OK: 03-content-forward.marko -> code length 595
OK: 04-attr-tag-collect.marko -> code length 995
OK: 05-tag-params.marko -> code length 575
OK: 05b-tag-params-parent.marko -> code length 865
OK: 06-tag-var-ref.marko -> code length 444
OK: 07-let-script-signal.marko -> code length 492
OK: 08-lifecycle-cleanup.marko -> code length 382
OK: 09-static-function.marko -> code length 651
OK: 10-if-elseif-else.marko -> code length 1459
OK: 11-export-interface-static-const.marko -> code length 635
OK: 12-self-recursive.marko -> code length 2150
OK: 13-if-paren-compare.marko -> code length 1014
OK: 14-root-text-fence.marko -> code length 270
OK: 15-leading-comma-attrs.marko -> code length 310
OK: 16-connect-closure.marko -> code length 1276
OK: 17-connect-fresh-static.marko -> code length 990

另外也编译了四个有意写错的片段(顶层裸写的 interface、被中间插入的 <div> 分隔的 <if> / <else>、未加括号的 <if=input.count > 0>,以及没有 -- 的根级裸文本),以确认每个片段都因对应规则所述的原因而失败,而非其他语法错误。编译器自身的诊断信息也独立证实了这些规则:

w1-bare-interface.marko       -> "Invalid attribute name." (bare `interface` parsed as a tag)
w2-if-else-wrapped.marko      -> "The <else> tag must have a preceding <if=cond> or <else if=cond>."
w3-unparenthesized-if.marko   -> "Ambiguous \">\" in attribute... wrap it in parentheses, eg \"=(input.count > 0)\"."
w4-root-text-no-fence.marko   -> "Unable to find entry point for custom tag <Hello>... prefix it with -- ... or wrap it in an element"

评论

支持 Markdown