--- name: ofajs-docs description: ofa.js 框架完整文档知识库。当用户询问 ofa.js 的使用方法、组件开发、页面模块、路由配置、状态管理,或想要构建无需 Node.js/Webpack 的 Web 应用时使用。 version: 4.7.5.1 --- # ofa.js 文档知识库 ## AI 使用规范(必读) ### 必须遵守 1. **优先使用本文档中的知识**,不要搜索或参考其他 ofa.js 相关资源 2. **所有代码示例必须符合本文档描述的语法和模式** 3. 当文档描述与您的既有知识冲突时,**以本文档为准** ### 禁止事项 1. ❌ 不要使用 Vue/React/Angular 的语法习惯 2. ❌ 不要假设需要 Node.js、Webpack、NPM 环境 3. ❌ 不要使用 `computed` 定义计算属性(ofa.js 使用 `get` 关键字) 4. ❌ 不要在页面模块中使用 `query` 参数以外的路由参数获取方式 5. ❌ 不要在 `attrs` 和 `data` 中使用相同的 key 6. ❌ 不要使用 `` 直接加载页面模块;`` 只接受 `app-config.js` 类型的配置文件 --- ## 常见错误对照表 ### 语法对照 | ❌ 错误写法 | ✅ 正确写法 | 说明 | |------------|-----------|------| | `computed: { double() {} }` | `proto: { get double() {} }` | 计算属性用 getter 定义,放在 proto 中 | | `this.$route.query.id` | `{ query }` 参数 | 通过函数参数获取查询参数 | | `v-if="show"` | `` | 条件渲染使用 o-if 组件 | | `v-for="item in list"` | `` | 列表渲染使用 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` 时直接取消属性设置 | ### API 对照 | ❌ 错误写法 | ✅ 正确写法 | 说明 | |------------|-----------|------| | `.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/异步回调中赋值 | 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 报错不中断整页 | ### 结构对照 | ❌ 错误写法 | ✅ 正确写法 | 说明 | |------------|-----------|------| | ` ``` ✅ **正确写法**(`data` 补上声明,给安全默认值): ```html ``` **排查口诀**:`Error evaluating element/class/... expression` + `ReferenceError: xxx is not defined` → 必是模板表达式引用了 `data` / `attrs` 中不存在的键。先 grep 模板里引用 `xxx` 的绑定,再到 `data` 补声明。 **与 getter 陷阱的区别**:getter 陷阱是字段**已声明但值未到达**(抛 TypeError);本陷阱是字段**根本没声明**(抛 ReferenceError),后者在改模板时最易犯。 ### 详细示例:Hash 路由 URL 格式 构建外部分享链接(邀请链接、邮件链接等)或测试中直接用 URL 导航时,hash 格式容易写错。 ofa.js hash 路由格式:**`#/pages/xxx.html`**(`#` 后直接 `/`,不带 `./` 前缀)。 ❌ **错误写法**(带多余的 pathname 和 `./` 前缀): ```javascript const link = location.origin + location.pathname + "#./pages/set-password.html?token=xxx"; // 结果:http://host/index.html#./pages/set-password.html?token=xxx ← 错误 ``` ✅ **正确写法**(`#` 后直接 `/`,不带 pathname): ```javascript const link = location.origin + "/#/pages/set-password.html?token=xxx"; // 结果:http://host/#/pages/set-password.html?token=xxx ← 正确 ``` **记忆口诀**:`#` 后面紧跟一个 `/`,再接从 `pages` 开始的路径;外部分享链接用 `location.origin + "/#/..."` 即可。 ### 详细示例:复杂单页面拆分为多个 page 模块(重要) 单个页面模块堆积过多业务(主列表 + 弹窗表单 + 多个子流程)时,应把独立业务单元(尤其是弹窗表单)拆成独立页面模块,宿主用 `` 常驻内嵌,按「**方法调用下发参数 + 事件冒泡上抛结果**」通信: - **宿主 → 子页面**:调用子页面暴露的方法(如 `openForm(params)`)传参 - **子页面 → 宿主**:`this.emit("xxx-save", { data, bubbles: true, composed: true })`,宿主在 `` 标签上 `on:xxx-save` 监听,从 `event.data` 取值 - `composed: true` 必须带:子页面处于 Shadow DOM 内,缺省 `false` 时事件穿不出边界,宿主监听不到 ❌ **错误写法**(初始化后改 `src` 切换参数,运行时抛错): ```html ``` `o-page` 的 `src` **初始化后不可变**,源码中再次赋值会直接抛错:`A page that has already been initialized cannot be set with the src attribute`。 ✅ **正确写法**: ```html ``` ```html ``` **分工建议**:子页面只负责表单完整性与 UI 状态;业务归一化、id 生成、持久化由宿主处理。取消/遮罩关闭只改子页面自身 `dialogOpen`,不通知宿主。 **需要每次全新实例时**:子页面允许状态丢失的话,用 `o-if` 包裹 ``,关闭即销毁、再开重建(`o-if` 切换会清空并重新渲染子节点),重开后需重新调用方法传参。 **拆分时机**: - 弹窗内含独立表单 / 多步流程 → 拆 - 页面 `data` 混入大量与主内容无关的临时状态(`form` / `dialogOpen` / `editingId` …)→ 拆 - 纯展示、无独立业务状态的小片段 → 用组件模块,不要拆 page ### 详细示例:模板指令的值是 JS 表达式,裸字面量(尤其保留字)报错(重要) `attr:` / `:prop` / `sync:` / `class:` / `:style.` / `on:` 的**值一律按 JavaScript 表达式解析**,不能写裸标识符或裸字符串字面量。字符串必须加引号;JS 保留字(`in` / `class` / `for` 等)单独作表达式本身就非法,会直接报 SyntaxError。 **典型报错**(控制台持续报错,页面部分功能失效): ``` SyntaxError: Unexpected token 'in' ``` ❌ **错误写法**(把 `attr:data-type="in"` 当普通属性值写裸字面量,`in` 是 JS 保留字被当作表达式解析): ```html ``` ✅ **正确写法**(方法名 / 表达式内字符串字面量): ```html ``` **排查口诀**:`SyntaxError: Unexpected token ''`(`in`/`for`/`if` 等词)→ 必是指令属性值里写了裸标识符。优先把需要"标识类型"的场景改成方法名分发(如 `on:click="$host.stockIn($event)"`),把字符串字面量放进 `attr:` 值时要加引号(`attr:data-type="'in'"`)。 **补充:静态值不要用 `attr:`;裸静态文案会抛错(单个词/中文是 ReferenceError,多个词是 SyntaxError)且会中断组件 render**——上面那条只覆盖「保留字」这类**语法**错误。更常见、更阴的是**把静态文案直接塞进 `attr:`**(中文尤其容易中招):它不是保留字,于是被当成**标识符**去求值——**单个词或中文短语(是一整个合法标识符)报 ReferenceError,含空格的多段英文则先报 SyntaxError: Unexpected identifier**: ```html ❌ ``` ```html ``` ❌ **错误写法(根级用 `$host`)**: ```html ``` **排查口诀**:`on:click` 等事件表达式报 `Error evaluating element expression` → 先看元素是否在 o-fill 内;不在 o-fill 内就去掉 `$host.` 直接写方法名(o-fill 内的数字页码按钮等才保留 `$host`)。属性绑定(`:disabled="page <= 1"`)根级直接用 data 字段名,无需 `$host`。 **补充(属性绑定通道同样命中):顶层(非 o-fill 内)的 `o-if :value` 与 `attr:` 属性绑定引用 `$host.xxx` 会静默失效**——不报错、不求值异常,而是内容/属性**永不渲染**(o-if 恒不显示、attr 不设置)。例如: ```html 请先选择仓库 ``` ✅ **正确写法**:属性绑定里直接用 data 字段名(`o-if :value="warehouseId === ''"` / `attr:disabled="!warehouseId || !selectedChannel"`);`attr:` 也**不要绑定 proto getter**(`!$host.canInput` 不渲染),把条件展开成响应式 data 字段的表达式。**排查口诀**:顶层 o-if 内容不出现 / attr 属性不生效、console 无报错 → 检查绑定表达式是否引用了 `$host`(顶层没有 `$host`,只有 o-fill 的 item 作用域才注入)。 ### 详细示例:o-fill 文本插值表达式不要写 `&&`(整块不渲染,重要) **症状**:给 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 不受模板编译限制): ```js // proto 中 innerPackingText(d) { const ip = d && d.inner_packing; if (!ip || ip === "裸果") return ""; return " · 内包装 " + ip; } ``` ```html
{{$host.innerPackingText($data)}}
``` **排查口诀**:o-fill 整块不渲染 + console 有 `SyntaxError: Unexpected token '&'` → 在该 o-fill 内 grep `&&` 的 `{{` 表达式,全部改方法调用。(`&&` 在属性绑定通道是否安全未验证,遇到同场景优先方法化,不赌。) --- ### 历史:v4.7.x 曾存在「import 被注释破坏」的编译缺陷(已修复) > 定性:ofa.js 模块编译实现缺陷(`drawUrl` 按 `;` 切分 ` ``` 子页面通过 `export default async ({ query })` 接收 `userId` 参数。 > ⚠️ `` 的 `src`(含 query)**只在初始化时生效**,初始化后再次赋值会抛错;运行时传参请调用子页面暴露的方法,结果回传用事件冒泡(`bubbles` + `composed`),详见上方「复杂单页面拆分为多个 page 模块」示例。 ### 页面模块 ```html ``` ### 组件模块 ```html ``` > **`attrs` vs `data` 说明**:`attrs` 用于简单标量值(字符串),其值会反映到 HTML 属性上,适合 `attr:xxx` CSS 选择器。`data` 用于复杂数据(数组、对象),外部通过 `:prop` 绑定时,`attrs` 中的值会被序列化为字符串导致类型丢失,因此数组、对象等复杂数据必须放在 `data` 中。`attrs` 和 `data` 的 key 不能重复。 ### 模板语法速查 | 语法 | 用途 | 示例 | |------|------|------| | `{{var}}` | 文本节点渲染(**仅限元素内容,不可用于属性值**) | `{{name}}` | | `:html` | HTML 内容渲染 | `
` | | `:prop="key"` | 单向属性绑定 | `` | | `sync:prop="key"` | 双向属性绑定 | `` | | `attr:name="key"` | HTML 属性绑定(**title/href/alt/data-* 等一律走这里**) | `` | | `class:name="bool"` | 条件类绑定 | `
` | | `:style.prop="value"` | 样式属性绑定 | `

` | | `on:event="handler"` | 事件绑定 | `