用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/ofajs/ofa.js --skill ofajs-docs命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | ofajs-docs |
| description | ofa.js 框架完整文档知识库。当用户询问 ofa.js 的使用方法、组件开发、页面模块、路由配置、状态管理,或想要构建无需 Node.js/Webpack 的 Web 应用时使用。 |
computed 定义计算属性(ofa.js 使用 get 关键字)query 参数以外的路由参数获取方式attrs 和 data 中使用相同的 key<o-app src="./page.html"> 直接加载页面模块;<o-app> 只接受 app-config.js 类型的配置文件| ❌ 错误写法 | ✅ 正确写法 | 说明 |
|---|---|---|
computed: { double() {} } | proto: { get double() {} } | 计算属性用 getter 定义,放在 proto 中 |
this.$route.query.id | { query } 参数 | 通过函数参数获取查询参数 |
v-if="show" | <o-if :value="show"> | 条件渲染使用 o-if 组件 |
v-for="item in list" | <o-fill :value="list" fill-key="id"> | 列表渲染使用 o-fill 组件;fill-key 为选填,但只要列表项有唯一字段(如 id),编写代码时应始终加上 |
@click="handle" | on:click="handle" | 事件绑定使用 on: 前缀 |
:class="{ active: isActive }" | class:active="isActive" | 动态类名使用 class: 语法 |
style="width: {{val}}" | :style.width="val" | 内联样式绑定使用 :style. 前缀 |
v-model="value" | sync:value="value" | 双向绑定使用 sync: 语法 |
props: { msg: String } | attrs: { msg: '默认值' } | 简单标量值(字符串)用 attrs;复杂数据(数组/对象)用 data |
methods: { foo() {} } | proto: { foo() {} } | 方法定义在 proto 对象中 |
data() { return { count: 0 } } | data: { count: 0 } | data 是对象而非函数 |
attrs 和 data 同名 key | 保持唯一 | attrs 和 data 的 key 不能重复 |
{{item.text}} | {{$data.text}} | o-fill 内必须使用 $data 访问数据 |
{{element.name}} | {{$data.name}} | o-fill 内必须使用 $data 访问数据 |
{{row.price}} | {{$data.price}} | o-fill 内必须使用 $data 访问数据 |
:class="item.type" | attr:type="$data.type" | 属性绑定也必须使用 $data |
proto: { $formatBytes() {} } | proto: { formatBytes() {} } | 自定义方法不加 $ 前缀 |
proto: { back() {} } / data: { back: "" }(与内置保留名重名) | 自定义方法 / 字段避开 back / goto / replace / pageAnime / pageIsReady / src 及 $.fn 上的方法名 | 这些名称已被 ofa.js 占用:back() / goto() / replace() 是页面实例自带的导航方法(back() 等价 this.app.back()),src 是页面地址属性;$.fn 上的通用方法(on / emit / $ / text 等)同样不可用。重名时新版直接报「注册参数有误,'proto'上的'xxx'已被占用」导致整页注册失败;data 字段冲突则直接 throw,详见下方详细示例 |
title="{{name}}" / :title="name" | attr:title="name" | 属性值内 {{...}} 不解析,动态属性必须用 attr: |
attr:style="width: {{pct}}%" | :style.width="pct + '%'" | 属性值内一律不解析 {{...}},动态样式用 :style. |
:disabled="isLoading"(disabled/checked/readonly 等布尔属性) | attr:disabled="isLoading" | :prop 会把 false 渲染成属性字符串 "false",HTML 布尔属性只要存在就生效,按钮永远禁用;attr: 在值为 false 时直接取消属性设置 |
| ❌ 错误写法 | ✅ 正确写法 | 说明 |
|---|---|---|
.click(handler) | .on("click", handler) | 事件绑定使用 .on() 方法 |
.hide() .show() | .style.display = "none" / "" | 没有 jQuery 风格的 show/hide 方法 |
.html("xxx") .text("xxx") | .html = "xxx" .text = "xxx" | 直接设置属性而非调用方法 |
ofaElement.addEventListener() | ofaElement.on() | ofa.js 对象使用 on() 方法 |
this.shadow.getElementById("id") | this.shadow.$("#id") | shadow 是 ofa.js 对象,使用 $() 方法 |
this.shadow.querySelector(".class") | this.shadow.$(".class") | 使用 $() 方法选择元素 |
ofaElement.scrollTop 等 | ofaElement.ele.scrollTop | ofa.js 对象通过 .ele 访问原生属性 |
document.querySelector("#id") | $("#id") | 全局获取元素实例使用 $(),document.querySelector 返回原生元素,缺少 ofa.js 增强方法和响应式特性 |
document.querySelector("o-app").goto(...) | $("o-app").goto(...) 或 this.app.goto(...) | goto()/replace() 等导航方法只存在于 $() 包装对象上,原生 DOM 元素上没有;页面模块内部用 this.app.goto(...) |
$("o-app").current.shadowRoot | $("o-app").current.ele.shadowRoot | $("o-app").current 返回的也是 ofa.js 包装对象,原生属性(shadowRoot、querySelector 等)必须通过 .ele 中转;ofa.js 自身属性(如 .src、.data、.app)可直接访问 |
get xxx() { return this.obj.field } + 模板 {{xxx}}(依赖异步数据) | data 中预定义 xxx: "",在 ready/异步回调中赋值 |
| ❌ 错误写法 | ✅ 正确写法 | 说明 |
|---|---|---|
<script> 在 <template> 外部 | <script> 在 <template> 内部 | script 必须放在 template 标签内部 |
export default async () => ({...}) | export default async ({ query }) => ({...}) | 页面模块应使用参数形式接收 query |
<o-fill><template><div>...</div></template></o-fill> | <o-fill><div>...</div></o-fill> | 直接渲染不需要 template 包裹 |
<template> 在 o-fill 内部 | <template> 在 o-fill 外部 + name 属性 | 模板渲染时 template 必须在外部 |
<o-app src="./page.html?key=val"> 在页面内嵌入子页面 | <o-page src="./page.html?key=val"> | 嵌入页面模块用 <o-page>;<o-app> 仅用于加载 app-config.js 的微应用 |
HTML 中使用 autoInstall | HTML 中使用 auto-install | 组件 attrs 定义时用 camelCase,但在 HTML 中使用时必须转为 kebab-case(横杠命名) |
location.origin + location.pathname + "#./pages/x.html" | location.origin + "/#/pages/x.html" | hash 路由格式为 #/pages/xxx.html(# 后直接 /,不带 ./ 前缀,也不带多余 pathname);构建外部分享链接用 location.origin + "/#/..." |
<o-page> 初始化后再次设置 src(含 :src="url" 动态绑定、改 query 传参) | 常驻 <o-page> + 宿主调用子页面方法传参 | o-page 的 src 初始化后不可变,再赋值直接抛错 A page that has already been initialized cannot be set with the src attribute;确需销毁重建时外层包 o-if 切换 |
{{...}} 的适用范围(重要){{expr}} 只在元素文本内容中生效。写进 HTML 属性值里 不会被解析,浏览器会把整段花括号当成字符串原样显示。
❌ 错误写法(属性值内使用 {{}}):
<span title="{{$data.appId}}">{{$data.appId}}</span>
<a href="{{url}}">链接</a>
<img alt="{{name}}" src="/x.png">
<div data-id="{{id}}"></div>
✅ 正确写法(属性一律用 attr: / :prop / class: / :style.):
<span attr:title="$data.appId">{{$data.appId}}</span>
<a attr:href="url">链接</a>
<img attr:alt="name" src="/x.png">
<div attr:data-id="id"></div>
记忆口诀:{{}} 只放尖括号 >...< 之间;尖括号里面的一切动态值都用 attr: / :prop / class: / :style. 系列指令。
为什么属性值不能用 {{}}?
{{}}>...< 之间的内容)才会被 ofa.js 正确解析和响应式更新attr:(重要)disabled / checked / readonly / hidden / open 这类 HTML 布尔属性是「存在即生效」的——属性值是什么无所谓,只要属性存在就算启用。给这类属性绑定布尔状态时必须用 attr:,不能用 :prop。
❌ 错误写法(:prop 把 false 渲染成属性字符串 "false",属性依然存在,按钮永远禁用):
<p-button color="primary" :disabled="analyzing">AI 识别</p-button>
<!-- analyzing === false 时渲染出 disabled="false",照样禁用 -->
✅ 正确写法(attr: 渲染语法判断到 false 会直接取消该属性的设置):
<p-button color="primary" attr:disabled="analyzing">AI 识别</p-button>
<!-- analyzing === false → 不设置 disabled 属性;analyzing === true → 属性存在,禁用 -->
为什么 :prop 会坑?
:prop 绑定的 false 会被序列化成字符串 "false" 落到属性上disabled="false" 与 disabled="true" 都算存在、都禁用attr: 指令对 false 有特殊处理:直接移除属性,属性不存在即恢复可用适用范围:所有「有则生效、无则失效」的原生布尔属性,以及组件内用 attrs 定义、shadow 模板里以 attr:xxx="xxx" 转发的布尔型组件属性(如 punch-ui 的 p-button 的 disabled)。
数据固有属性(如 type、status、level)应使用 attr: + 属性选择器,样式状态切换(如 active、disabled)才使用 class: + 类名选择器。
❌ 错误写法(将数据属性作为类名):
<div class="message" :class="$data.type">
{{$data.text}}
</div>
<style>
.message.sent { color: blue; }
.message.received { color: green; }
</style>
✅ 正确写法(使用属性绑定):
<div class="message" attr:type="$data.type">
{{$data.text}}
</div>
<style>
.message[type="sent"] { color: blue; }
.message[type="received"] { color: green; }
</style>
为什么这样更好?
type 是消息类型的属性,不是样式类通过 $() 获取的是 ofa.js 包装对象,提供增强方法和响应式特性;通过 .ele 属性访问原生 DOM 元素。
shadow 对象的选择器方法:this.shadow 返回的是 ofa.js 实例化的对象,不是原生 ShadowRoot。
❌ 错误写法(使用原生 API):
const messagesDiv = this.shadow.getElementById("messages");
const element = this.shadow.querySelector(".class");
✅ 正确写法(使用 ofa.js API):
const messagesDiv = this.shadow.$("#messages");
const element = this.shadow.$(".class");
原生 DOM 属性访问:element.$() 返回 ofa.js 包装对象,原生属性需通过 .ele 访问。
❌ 错误写法(直接操作 ofa.js 对象):
const messagesDiv = this.shadow.$("#messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight; // scrollTop 是原生属性
✅ 正确写法(通过 .ele 访问原生属性):
const messagesDiv = this.shadow.$("#messages");
messagesDiv.ele.scrollTop = messagesDiv.ele.scrollHeight;
使用场景:
.on(), .text, .html 等).ele 访问原生 DOM 属性(如 .scrollTop, .scrollHeight, .clientWidth 等)Playwright 测试 / 浏览器控制台中的高频踩坑点:$("o-app").current 返回的也是 ofa.js 包装对象,不是原生 DOM 元素。访问 shadow DOM 时容易写错。
❌ 错误写法(直接在包装对象上访问原生属性):
// Playwright 测试或浏览器控制台中
const cur = $("o-app").current;
cur.shadowRoot // → undefined(shadowRoot 是原生属性)
cur.shadowRoot.querySelector(...) // → 报错 not a function
✅ 正确写法(通过 .ele 中转访问原生属性;ofa.js 自身属性可直接访问):
const cur = $("o-app").current;
cur.ele.shadowRoot // ✅ 通过 .ele 访问原生 shadowRoot
cur.ele.shadowRoot.querySelector(".item") // ✅ 原生查询
cur.src // ✅ ofa.js 包装对象的属性可直接访问
cur.data // ✅ ofa.js data 可直接访问
| 场景 | ❌ 错误写法 | ✅ 正确写法 |
|---|---|---|
| Playwright/浏览器中获取当前页面 shadow DOM | $("o-app").current.shadowRoot | $("o-app").current.ele.shadowRoot |
| 测试中查询当前页面内部元素 | $("o-app").current.shadowRoot.querySelector(...) | $("o-app").current.ele.shadowRoot.querySelector(...) |
记忆口诀:$() 返回 ofa.js 包装对象,.current 也是包装对象;ofa.js 自己加的属性(.src/.data/.app)直接用,浏览器原生的属性和方法(.shadowRoot/.querySelector/.scrollTop)一律走 .ele。
$ 是 ofa.js 内置特殊变量的保留前缀($data、$index、$host、$event),自定义 proto 方法禁止使用 $ 前缀。
❌ 错误写法(方法名加 $ 前缀):
export default async () => {
return {
tag: "my-component",
data: { size: 1024 },
proto: {
$formatBytes(val) {
return (val / 1024).toFixed(2) + " KB";
}
}
};
};
<span>{{$formatBytes(size)}}</span>
✅ 正确写法(直接使用无前缀命名):
export default async () => {
return {
tag: "my-component",
data: { size: 1024 },
proto: {
formatBytes(val) {
return (val / 1024).toFixed(2) + " KB";
}
}
};
};
<span>{{formatBytes(size)}}</span>
o-fill 内通过 $host 调用时同样不加 $:
<o-fill :value="files" fill-key="id">
<span>{{$host.formatBytes($data.size)}}</span>
</o-fill>
页面模块的 proto 方法与 data 字段不能使用 ofa.js 已占用的内置名称,否则模块注册直接失败(整页无法渲染)。新版控制台报错:
页面 http://.../xxx.html 的注册参数有误,'proto'上的'back'已被占用,请将'back'改为其他名字。
已占用的内置名称(页面实例自带):
back()(后退,等价 this.app.back())、goto()、replace()src(页面地址)、pageAnime(切换动画)、pageIsReady$.fn 上的通用方法(on / off / emit / $ / text / html / css / data 等)data 里的字段与这些保留名冲突时会直接 throw(page_invalid_key);proto 里的方法重名在较新版本直接报注册错误,旧版本虽只是 console.warn 但方法会被内置实现覆盖,行为同样不可靠。
❌ 错误写法(自定义 back 与内置后退方法重名):
export default async () => ({
data: { dialogOpen: false },
proto: {
back() { // ❌ 与内置后退导航 back() 重名
this.phase = "input";
},
},
});
✅ 正确写法(改用不冲突的语义化命名):
export default async () => ({
data: { dialogOpen: false },
proto: {
backToInput() { // ✅ 语义化命名,避免与内置 back() 冲突
this.phase = "input";
},
},
});
排查口诀:报错出现「'proto' 上的 'xxx' 已被占用」→ 该名字必是内置保留名。先规避 back / goto / replace / src / pageAnime / pageIsReady 及 $.fn 上的通用方法名(具体内置实现见 packages/ofa/page.mjs 的 proto 定义);自定义方法尽量用业务语义命名(如 openXxx / saveXxx / backToInput)。
属性值内一律不解析 {{...}}。需要动态值时:
attr:属性名="表达式":属性名="表达式" / sync:属性名="表达式"class:类名="布尔表达式":style.属性名="表达式"❌ 错误写法(属性值内使用 {{}},不会被解析):
<div attr:style="width: {{pct}}%"></div>
✅ 正确写法(使用 :style. 绑定单个样式属性):
<div :style.width="pct + '%'"></div>
为什么这样更好?
{{...}} 不会被解析,必须使用指令绑定:style. 的值是 JavaScript 表达式,可自由拼接字符串页面模块中用 get xxx() {} 定义计算属性供模板 {{xxx}} 使用时,getter 会在模块初始化阶段被立即求值,此时 ready() 尚未执行。如果 getter 内部访问了 this.data 中尚未初始化的对象/数组字段(尤其是 null/undefined 或深层链式访问),就会抛 TypeError 导致整页渲染崩溃。
典型报错:
Error: Error evaluating text expression: 'roleText'
❌ 错误写法(getter 依赖异步获取的数据):
export default async ({ query }) => {
return {
data: {
userInfo: {}, // 初始为空对象
},
get roleText() {
// 模板初始化时立刻求值,此时 userInfo 还是 {}
// 若 userInfo 是 null/undefined 或做深层链式访问就会 TypeError
return ROLE_TEXT[this.userInfo.role] || "";
},
ready() {
this.loadInfo(); // 异步赋值 userInfo,但来不及
},
proto: {
async loadInfo() { /* ... */ }
}
};
};
// 模板:{{roleText}}
✅ 正确写法(用 data 字段预定义安全默认值,在异步回调中赋值):
export default async ({ query }) => {
return {
data: {
userInfo: {},
roleText: "", // 预定义为安全默认值
},
ready() {
this.loadInfo();
},
proto: {
async loadInfo() {
const info = await api.getInfo();
this.userInfo = info;
this.roleText = ROLE_TEXT[info.role] || info.role || ""; // 异步回调中赋值
},
},
};
};
// 模板:{{roleText}}
getter 适用边界:
get double() { return this.count * 2 }(count 有初始值 0)为什么 getter 会立即求值?
{{xxx}} 表达式并建立响应式依赖this.xxx 的访问,建立依赖追踪ready() 在初始化完成后才执行,异步数据此时还未到达模板中所有表达式({{xxx}}、:prop、sync:、class:、:style.、attr:)在模块初始化阶段立即求值,引用的每个键都必须在 data / attrs 中已声明。引用未声明的变量不会得到 undefined,而是直接抛错并中断整页渲染:
Error: Error evaluating element expression: ':value="flag"', from file: ...
Caused by: ReferenceError: flag is not defined
典型场景:给已有页面新增功能时,模板加了新绑定,忘了在 data 里补字段。报错在首次渲染时出现,且该页面/组件整体渲染失败。
❌ 错误写法(模板用了 noBg,data 没声明):
<x-if :value="noBg === 'off'">...</x-if>
<p-switch sync:value="noBg">无底色</p-switch>
<script>
export default async () => ({
data: { dialogOpen: false }, // ❌ 缺 noBg 声明
});
</script>
✅ 正确写法(data 补上声明,给安全默认值):
<script>
export default async () => ({
data: { dialogOpen: false, noBg: "off" }, // ✅ 模板引用的键全部声明
});
</script>
排查口诀:Error evaluating element/class/... expression + ReferenceError: xxx is not defined → 必是模板表达式引用了 data / attrs 中不存在的键。先 grep 模板里引用 xxx 的绑定,再到 data 补声明。
与 getter 陷阱的区别:getter 陷阱是字段已声明但值未到达(抛 TypeError);本陷阱是字段根本没声明(抛 ReferenceError),后者在改模板时最易犯。
构建外部分享链接(邀请链接、邮件链接等)或测试中直接用 URL 导航时,hash 格式容易写错。
ofa.js hash 路由格式:#/pages/xxx.html(# 后直接 /,不带 ./ 前缀)。
❌ 错误写法(带多余的 pathname 和 ./ 前缀):
const link = location.origin + location.pathname + "#./pages/set-password.html?token=xxx";
// 结果:http://host/index.html#./pages/set-password.html?token=xxx ← 错误
✅ 正确写法(# 后直接 /,不带 pathname):
const link = location.origin + "/#/pages/set-password.html?token=xxx";
// 结果:http://host/#/pages/set-password.html?token=xxx ← 正确
记忆口诀:# 后面紧跟一个 /,再接从 pages 开始的路径;外部分享链接用 location.origin + "/#/..." 即可。
单个页面模块堆积过多业务(主列表 + 弹窗表单 + 多个子流程)时,应把独立业务单元(尤其是弹窗表单)拆成独立页面模块,宿主用 <o-page> 常驻内嵌,按「方法调用下发参数 + 事件冒泡上抛结果」通信:
openForm(params))传参this.emit("xxx-save", { data, bubbles: true, composed: true }),宿主在 <o-page> 标签上 on:xxx-save 监听,从 event.data 取值composed: true 必须带:子页面处于 Shadow DOM 内,缺省 false 时事件穿不出边界,宿主监听不到❌ 错误写法(初始化后改 src 切换参数,运行时抛错):
<o-page :src="'./form.html?id=' + editingId"></o-page>
o-page 的 src 初始化后不可变,源码中再次赋值会直接抛错:A page that has already been initialized cannot be set with the src attribute。
✅ 正确写法:
<!-- 宿主页面 -->
<template page>
<o-page id="form-page" src="./form.html" on:form-save="onSave"></o-page>
<script>
export default async () => ({
proto: {
openForm(item) {
// $() 拿到的是 ofa.js 包装对象,可直接调用子页面方法
this.shadow.$("#form-page")?.openForm(item);
},
onSave(event) {
console.log(event.data); // 子页面上抛的表单值
},
},
});
</script>
</template>
<!-- 子页面 form.html:自带 p-dialog,暴露 openForm 供宿主打开 -->
<template page>
<p-dialog sync:open="dialogOpen" auto-close><!-- 表单控件 sync:value="form.xxx" --></p-dialog>
<script>
export default async () => ({
data: { dialogOpen: false, form: {} },
proto: {
openForm(params) {
Object.assign(this.form, params); // 回填参数
this.dialogOpen = true;
},
save() {
if (!this.form.name.trim()) return; // 子页面只管非空校验
this.emit("form-save", {
data: { ...this.form },
bubbles: true,
composed: true, // 穿透 Shadow DOM,宿主才能监听
});
. = ;
},
},
});
分工建议:子页面只负责表单完整性与 UI 状态;业务归一化、id 生成、持久化由宿主处理。取消/遮罩关闭只改子页面自身 dialogOpen,不通知宿主。
需要每次全新实例时:子页面允许状态丢失的话,用 o-if 包裹 <o-page>,关闭即销毁、再开重建(o-if 切换会清空并重新渲染子节点),重开后需重新调用方法传参。
拆分时机:
data 混入大量与主内容无关的临时状态(form / dialogOpen / editingId …)→ 拆attr: / :prop / sync: / class: / :style. / on: 的值一律按 JavaScript 表达式解析,不能写裸标识符或裸字符串字面量。字符串必须加引号;JS 保留字(in / class / for 等)单独作表达式本身就非法,会直接报 SyntaxError。
典型报错(控制台持续报错,页面部分功能失效):
SyntaxError: Unexpected token 'in'
❌ 错误写法(把 attr:data-type="in" 当普通属性值写裸字面量,in 是 JS 保留字被当作表达式解析):
<button attr:data-type="in">入库</button>
<!-- ofa.js 将值 "in" 当作表达式 → SyntaxError: Unexpected token 'in' -->
✅ 正确写法(方法名 / 表达式内字符串字面量):
<button on:click="$host.stockIn($event)">入库</button>
<!-- 事件绑到方法名,避免在指令值里写裸字面量 -->
<button attr:data-type="'in'">入库</button>
<!-- 确需传字面量时加引号,作为字符串表达式 -->
排查口诀:SyntaxError: Unexpected token '<xxx>'(in/for/if 等词)→ 必是指令属性值里写了裸标识符。优先把需要"标识类型"的场景改成方法名分发(如 on:click="$host.stockIn($event)"),把字符串字面量放进 attr: 值时要加引号(attr:data-type="'in'")。
ofa.js 对已加载的页面模块有内存级模块缓存(同 URL 复用组件/页面模块定义),且页面通过 fetch 拉取模板文件——若静态服务器带 HTTP 缓存(如 http-server 不带 -c-1),浏览器还会命中磁盘缓存。两个缓存叠加的表现:改了页面文件,但 hash 导航(不整页刷新)仍渲染旧版本,console 无任何报错,极易误判为"代码没改对"而浪费时间排查。
典型场景:Playwright 测试或浏览器里用 #/pages/xxx.html 直接导航调试,反复修改页面模板后效果不变;甚至把文件改坏成明显错误的版本,页面仍正常渲染旧逻辑。
✅ 正确做法:
http-server . -p 5173 -c-1(-c-1 = 禁用缓存;npm run dev 已内置,npm start 不带)。http://localhost:5173/index.html?t=v1#/pages/xxx.html——query 变了 fetch 视为新 URL,绕过缓存。page.goto(url) 整页加载。排查口诀:改代码不生效 + console 无报错 → 先怀疑缓存(模块缓存 / HTTP 缓存),用带 query 的 URL 强刷排除;不要先用二分法怀疑自己的代码。
$host / $data 只在 o-fill 的 item 作用域可用,根级元素直接用方法名(重要)$host / $data 由 ofa.js 在 x-fill(o-fill)渲染列表项时注入到 item 作用域(createItem 创建 { $data, $host, $index })。根级(页面模板顶层、非 o-fill 内)作用域没有 $host / $data,on:click="$host.xxx()" 会抛 Error evaluating element expression: 'on:click="$host.xxx()"',点击无反应且 console 报错。
✅ 正确写法:
<!-- 根级:直接写方法名(proto 方法挂在页面实例上) -->
<button on:click="openStockHelp()">?</button>
<button on:click="goToPage(currentPage - 1)">上一页</button>
<!-- o-fill 内:才有 $data / $host / $index -->
<o-fill :value="rows" fill-key="id">
<button on:click="$host.deleteRow($data.id)">{{$data.name}}</button>
</o-fill>
❌ 错误写法(根级用 $host):
<button on:click="$host.openStockHelp()">?</button> <!-- 报错 -->
排查口诀:on:click 等事件表达式报 Error evaluating element expression → 先看元素是否在 o-fill 内;不在 o-fill 内就去掉 $host. 直接写方法名(o-fill 内的数字页码按钮等才保留 $host)。属性绑定(:disabled="page <= 1")根级直接用 data 字段名,无需 $host。
&&(整块不渲染,重要)症状:给 o-fill 内某条文本插值加 && 表达式(如 {{ $data.x && $data.x !== '裸果' ? ' · 内包装 ' + $data.x : '' }})后,整个 o-fill 区块不渲染(列表项全消失),页面其它区域(标题/工具条/分页)正常,无整页报错,仅 console 有一条 SyntaxError: Unexpected token '&'(new Function 编译时抛错)。
最小复现对照({{}} 文本插值通道):
{{ $data.pack && $data.pack ? ... : '' }}(含 &&)→ ❌ 整块 o-fill 不渲染{{ $data.pack ? '·内 ' + $data.pack : '' }}(三元 + 拼接)→ ✅{{ $data.pack === '裸果' ? '' : ... }}(===)→ ✅{{ $data.pack !== '裸果' ? ... }}(!==)→ ✅{{$host.xxx($data)}}(方法调用)→ ✅根因:o-fill 的 item 模板把 {{}} 表达式经 encodeURIComponent 编码写入 expr 属性再取回编译,&& 在此链路中损坏(残留单个 &),new Function 编译失败;且失败发生在该 o-fill 的渲染循环中,导致整个区块中断。!==、===、三元、字符串拼接均不受影响。
修复:文本插值里避免 &&,抽成 $host 方法(方法内 JS 不受模板编译限制):
// proto 中
innerPackingText(d) {
const ip = d && d.inner_packing;
if (!ip || ip === "裸果") return "";
return " · 内包装 " + ip;
}
<!-- 模板中 -->
<div>{{$host.innerPackingText($data)}}</div>
排查口诀:o-fill 整块不渲染 + console 有 SyntaxError: Unexpected token '&' → 在该 o-fill 内 grep && 的 {{ 表达式,全部改方法调用。(&& 在属性绑定通道是否安全未验证,遇到同场景优先方法化,不赌。)
<template page> 内包含 <style>、模板内容和 <script>,script 必须在 template 内部<template component> 内包含 <style>、模板内容和 <script>,script 必须在 template 内部,返回对象中必须包含 tag 字段| 标签 | 用途 | src 指向 |
|---|---|---|
<o-page> | 在入口 HTML 或其他页面模板中嵌入一个页面模块 | 直接指向页面模块文件(.html) |
<o-app> | 创建微应用,管理多页面导航和切换 | 指向应用配置文件(app-config.js) |
关键区别:
<o-page> 是"页面级组件",用于加载和渲染页面模块。可在入口 HTML 中使用,也可在另一个页面的模板中嵌入子页面。<o-app> 是"微应用容器",用于创建独立的应用实例,通过加载 app-config.js 配置首页和页面切换动画。不要用 <o-app> 直接加载页面模块文件。嵌入子页面示例(在页面模板内嵌入另一个页面模块):
<template page>
<p-dialog>
<o-page src="./user-traffic-page.html?userId=123"></o-page>
</p-dialog>
<script>
export default async () => {
return {
data: { ... }
};
};
</script>
</template>
子页面通过 export default async ({ query }) 接收 userId 参数。
⚠️
<o-page>的src(含 query)只在初始化时生效,初始化后再次赋值会抛错;运行时传参请调用子页面暴露的方法,结果回传用事件冒泡(bubbles+composed),详见上方「复杂单页面拆分为多个 page 模块」示例。
<template page>
<style>
:host { display: block; }
</style>
<div>{{message}}</div>
<script>
export default async ({ query }) => {
return {
data: { message: "Hello" },
proto: { handleClick() {} }
};
};
</script>
</template>
<template component>
<style>
:host { display: block; }
</style>
<div>{{value}}</div>
<script>
export default async () => {
return {
tag: "my-component",
attrs: { value: "default" },
data: { count: 0 },
proto: { increment() {} }
};
};
</script>
</template>
attrsvsdata说明:attrs用于简单标量值(字符串),其值会反映到 HTML 属性上,适合attr:xxxCSS 选择器。data用于复杂数据(数组、对象),外部通过:prop绑定时,attrs中的值会被序列化为字符串导致类型丢失,因此数组、对象等复杂数据必须放在data中。attrs和data的 key 不能重复。
| 语法 | 用途 | 示例 |
|---|---|---|
{{var}} | 文本节点渲染(仅限元素内容,不可用于属性值) | <span>{{name}}</span> |
:html | HTML 内容渲染 | <div :html="htmlContent"></div> |
:prop="key" | 单向属性绑定 | <input :value="name"> |
sync:prop="key" | 双向属性绑定 | <input sync:value="name"> |
attr:name="key" | HTML 属性绑定(title/href/alt/data- 等一律走这里*) | <a attr:href="url" attr:title="tip"> |
class:name="bool" | 条件类绑定 | <div class:active="isActive"> |
:style.prop="value" | 样式属性绑定 | <p :style.color="textColor"> |
on:event="handler" | 事件绑定 | <button on:click="handleClick"> |
on:event="expr" | 表达式事件 | <button on:click="count++"> |
$event | 事件对象 | on:click="handle($event)" |
$("#id") | 获取元素实例 | const el = $("#myComponent") |
proto 中使用 get xxx() {} 而非 computed$.stanz() 创建<o-fill> 组件;fill-key 虽为选填,但只要列表项存在唯一标识字段(如 id),编写代码时应始终添加 fill-key="字段名",以保证数组增删、排序时正确复用和更新列表项<o-if> / <o-else-if> / <o-else> 组件<x-if> / <x-fill> 功能相同但不渲染到 DOM:toKey="fromKey" 单向,sync:toKey="fromKey" 双向watch: { prop() {} }ready() attached() detached()this.emit('event-name', { data: {...} })<slot></slot> 接收外部内容是否需要可复用的组件?
├─ 是 → 使用组件模块(<template component> + tag 字段)
└─ 否 → 使用页面模块(<template page>)
单页面业务是否过重(主列表 + 弹窗表单 + 多个子流程混在一个模块)?
├─ 是 → 拆分成多个页面模块:宿主 <o-page> 常驻内嵌子页面
│ ├─ 首次初始化传参:src URL 带 query,如 src="./sub-page.html?userId=123"
│ ├─ 运行时传参:宿主调用子页面暴露的方法(src 初始化后不可变,禁止改 src/query)
│ └─ 结果回传:子页面 emit 事件冒泡(bubbles + composed),宿主 on:事件名 监听
└─ 否 → 保持单页面模块
是否需要共享数据?
├─ 是 → 是否跨多层组件?
│ ├─ 是 → 使用 o-provider/o-consumer
│ └─ 否 → 使用 sync: 双向绑定 或 : 单向传递
└─ 否 → 使用 data 定义本地数据
定义组件属性时,该值应该放在 attrs 还是 data?
├─ 简单标量值(字符串)→ 放在 attrs
│ └─ 会反映到 HTML 属性上,可用 attr:xxx 在 CSS 中选择
├─ 复杂数据(数组、对象)→ 放在 data
│ └─ 外部通过 :prop 绑定时,attrs 会序列化为字符串导致类型丢失
└─ 示例:<n-line-chart :points="someArray"> → points 是数组,必须放在 data 中
列表渲染?
├─ 是 → 使用 o-fill 组件
│ ├─ 列表项有唯一标识字段(如 id)→ 加上 fill-key="id"(选填属性,但编写代码时应始终填写)
│ ├─ 直接渲染(简单结构)→ 模板内容直接写在 o-fill 内部,不需要 <template> 包裹
│ └─ 模板渲染(复杂结构/复用)→ <template> 定义在 o-fill 外部,使用 name 属性绑定
└─ 否 → 正常编写模板
条件渲染?
├─ 是 → 使用 o-if/o-else-if/o-else 组件
└─ 否 → 正常编写模板
o-fill 直接渲染(推荐用于简单结构):
<o-fill :value="messages" fill-key="id">
<div class="message" attr:type="$data.type">
[{{$data.time}}] {{$data.text}}
</div>
</o-fill>
$data、$index、$host 访问数据id)时应加上 fill-key(选填,但建议始终填写)o-fill 模板渲染(用于复杂结构或复用):
<o-fill :value="products" name="product-template" fill-key="id"></o-fill>
<template name="product-template">
<div class="product-card">{{$data.name}} - ¥{{$data.price}}</div>
</template>
需要根据数据设置样式?
├─ 数据固有属性(如 type、status、level)→ 使用 attr: + 属性选择器
└─ 样式状态切换(如 active、disabled)→ 使用 class: + 类名选择器
是否需要多页面应用?
├─ 是 → 使用 o-router + o-app
│ └─ 是否需要嵌套布局?
│ ├─ 是 → 父页面使用 <slot>,子页面导出 parent
│ └─ 否 → 独立页面
└─ 否 → 单页面应用
| 文档 | 说明 |
|---|---|
| 模板语法案例与语法说明 | 所有模板语法的完整案例和详细说明(最高优先级) |
| 快速参考表 | API 和语法速查表 |
| API 参考手册 | 完整 API 文档 |
| 常见模式与最佳实践 | 常用代码模式(含单页面业务拆分/内嵌子页面模式) |
| 文档 | 说明 |
|---|---|
| 介绍 | 框架核心概念和优势 |
| 脚本引用 | 引入方式 |
| 快速上手 | 快速入门 |
| 创建第一个应用 | 使用 OFA Studio 创建项目 |
| 生产与部署 | 开发环境、生产部署、压缩混淆 |
| 速查语法 | 文档 |
|---|---|
{{变量}} :html | 内容渲染 |
on:click="handler" | 事件绑定 |
:prop="value" sync:prop="value" | 属性绑定 |
class:active="isActive" :style.width="val" | 类/样式绑定 |
<o-if :value="condition"> | 条件渲染 |
<o-fill :value="list" fill-key="id"> | 列表渲染 |
get computedProp() {} | 计算属性 |
watch: { prop() {} } | 侦听器 |
ready() attached() detached() | 生命周期 |
| 速查语法 | 文档 |
|---|---|
<template component> tag attrs | 创建组件 |
export default async ({ load, url, query }) | 模块返回对象属性 |
<slot></slot> | 插槽 |
this.emit('event') | 自定义事件 |
attrs: { msg: 'default' } | 传递特征属性 |
:toProp="fromProp" | 领悟属性绑定 |
{{obj.nested.prop}} | 属性响应 |
<inject-host> | 注入宿主样式 |
<x-if> <x-fill> | 非显式组件 |
<template is="replace-temp"> | 替换模板 |
<match-var> | 样式查询 |
| 速查语法 | 文档 |
|---|---|
o-provider o-consumer | 上下文状态 |
$.stanz() | 状态管理 |
o-app o-router | 路由 |
父页面 <slot> 子页面 parent | 嵌套页面/路由 |
app-config.js | 应用配置 |
o-app 微应用 | 微应用 |
| SCSR 同构渲染 | SSR 与同构渲染 |
| 案例 | 功能要点 | 入口 | 关键文件 |
|---|---|---|---|
| 计数器 | 数据绑定、事件、计算属性、样式 | demo.html | page.html |
| 开关组件 | 组件定义、属性传递、事件、插槽 | demo.html | switch.html, page.html |
| 待办列表 | 数据持久化、列表渲染、状态管理 | demo.html | page.html, data.js |
| 文件编辑器 | 嵌套组件通信、o-provider、依赖注入 | demo.html | page.html, filelist.html, editor.html |
| SPA 路由 | o-router、o-app、页面动画 | demo.html | app-config.js, layout.html |
| SCSR 渲染 | 服务端渲染、SEO、同构应用 | home.html | app-config.js |
| Shadow DOM | shadow 操作、组件方法定义 | demo.html | shadow-demo.html |
| getter 在模板初始化阶段(ready 执行前)就被求值,若依赖的 data 字段尚未赋值(尤其 null/undefined 链式访问)会抛 TypeError 导致整页渲染崩溃;getter 仅适合依赖同步已有数据(有初始值)的简单计算 |
模板表达式引用未声明的变量({{flag}} / :value="flag" / class:active="flag"…) | 所有模板引用的键先在 data / attrs 中声明(给安全默认值) | 未声明的键不是 undefined,初始化求值直接抛 Error evaluating element expression ... ReferenceError: flag is not defined,整页渲染中断;常见于改模板加新绑定、忘了同步 data |
o-fill 文本插值里写 &&(如 {{ $data.a && $data.b ? ... : '' }}) | 抽成 $host.xxx($data) 方法;或改用嵌套三元 / === / !== 形式 | o-fill 的 {{}} 表达式编译时 && 会抛 SyntaxError: Unexpected token '&',整个 o-fill 区块不渲染(列表项全消失、页面其它区域正常),仅 console 报错不中断整页 |