skills/ofajs-docs-full/SKILL.md
ofa.js 框架教程。当用户询问 ofa.js 的使用方法、组件开发、页面模块,或想要构建无需 Node.js/Webpack 的 Web 应用时使用。
npx skillsauth add ofajs/ofa.js ofajs-docsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
在使用本教程开发 ofa.js 应用时,请遵循以下规则:
computed 定义计算属性(ofa.js 使用 get 关键字)query 参数以外的路由参数获取方式attrs 和 data 中使用相同的 key<template page> + export default async () => ({...})<template component> + 返回对象中必须包含 tag 字段get xxx() {} 而非 computed$.stanz() 创建on:click="methodName" 或 on:click="count++"<o-fill> 组件<o-if> / <o-else-if> / <o-else> 组件:toKey="fromKey" 单向传递,sync:toKey="fromKey" 双向绑定| ❌ 错误写法 | ✅ 正确写法 | 说明 |
|------------|-----------|------|
| computed: { double() {} } | get double() {} | ofa.js 使用 getter 定义计算属性 |
| this.$route.query.id | { query } 参数 | 通过函数参数获取查询参数 |
| v-if="show" | <o-if :value="show"> | 使用 o-if 组件进行条件渲染 |
| v-for="item in list" | <o-fill :value="list"> | 使用 o-fill 组件进行列表渲染 |
| @click="handle" | on:click="handle" | 事件绑定使用 on: 前缀 |
| :class="{ active: isActive }" | class:active="isActive" | 动态类名使用 class: 语法 |
| v-model="value" | sync:value="value" | 双向绑定使用 sync: 语法 |
| props: { msg: String } | attrs: { msg: 'default' } | 组件属性使用 attrs 定义 |
| methods: { foo() {} } | proto: { foo() {} } | 方法定义在 proto 对象中 |
| data() { return { count: 0 } } | data: { count: 0 } | data 是对象而非函数 |
当用户需要开发功能时,按以下顺序判断:
是否需要共享数据?
├─ 是 → 是否跨多层组件?
│ ├─ 是 → 使用 o-provider/o-consumer
│ └─ 否 → 使用 sync: 双向绑定 或 : 单向传递
└─ 否 → 使用 data 定义本地数据
是否需要列表渲染?
├─ 是 → 使用 o-fill 组件
│ └─ 是否需要递归渲染?
│ ├─ 是 → 使用 name 属性定义模板
│ └─ 否 → 直接在 o-fill 内编写模板
└─ 否 → 正常编写模板
是否需要条件渲染?
├─ 是 → 使用 o-if/o-else-if/o-else 组件
└─ 否 → 正常编写模板
是否需要可复用的组件?
├─ 是 → 使用组件模块(<template component> + tag 字段)
└─ 否 → 使用页面模块(<template page>)
是否需要多页面应用?
├─ 是 → 使用 o-router + o-app
│ └─ 是否需要嵌套布局?
│ ├─ 是 → 父页面使用 <slot>,子页面导出 parent
│ └─ 否 → 独立页面
└─ 否 → 单页面应用
💡 AI 使用指引:本教程配套完整的示例源码位于
assets/目录。当遇到具体问题或需要深入了解某个功能时,请主动查看对应的示例文件,源码中包含更完整的实现细节和注释。文档描述与源码不一致时,请以源码为准。
ofa.js 是一个渐进式 Web 微前端框架,无需依赖 Node.js、Webpack 和 NPM 环境即可运行。
在 HTML 中直接引入 ofa.js 的 JS 文件,即可开始使用:
<script src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.min.mjs" type="module"></script>
本案例展示一个简单的计数器页面,涵盖数据绑定、事件处理、计算属性和样式定义等基础用法。
<template page> 标签定义,需要渲染的元素都写在 template 内,文件后缀为 .html。template 内的 <script> 中,通过 export default 函数返回对象,以此定义页面模块的数据和方法。有且只能存在一个 script 标签。export default 函数中接收 { query } 参数,用于获取 URL 查询参数。data 和 proto 中定义,分别对应页面模块的属性和方法。{{key}} 语法,直接将 data 中的数据渲染为文本。proto 上定义模块的方法后,可在模板节点上使用 on:xxx 进行事件绑定,可用事件参考 DOM 事件。num++。proto 上可以使用 get xxx 关键字定义计算属性(与 Vue 不同,请不要使用 computed)。template 内的 <style> 中。若要定义模块元素自身的样式,需使用 :host 选择器,可参考 Web Component 样式选择器。count++、isShow = !isShow。on:click="addNumber(5)",参数会传递给 proto 中定义的方法。$event 参数访问原生事件对象,如 handleClick($event)。_ 开头的属性名为非响应式属性,变化不会触发视图更新,适用于不需要触发 UI 更新的临时数据。_ 开头的属性名来存储,避免被转换为响应式数据。<script>
export default async () => ({
data: {
obj: {
// 实例上的对象数据,会自动转化为响应式状态数据,变化会触发视图更新
val: "hello world",
},
},
attached() {
const obj2 = {
val: "change val",
};
this.obj = obj2;
console.log(this.obj.val === obj2.val); // => true
console.log(this.obj === obj2); // => false,已经被转换为响应式状态数据,对象引用不同
// this.obj = new SomeClass(); // ❌ 不要这么做,会被自动为响应式状态数据
// this._obj = new SomeClass(); // ✅ 可以这么做,不会被自动为响应式状态数据
},
});
class SomeClass {
constructor() {}
}
</script>
本案例展示一个开关组件的实现,涵盖组件定义、属性传递、事件通信和插槽等核心功能。
<template component> 标签定义,需要渲染的元素都写在 template 内。tag 字段用于定义组件的标签名,且不能使用 query 参数。<l-m> 标签引入组件模块,src 属性为组件模块的路径。引入后,即可在模板中使用该组件的标签名。attrs 定义属性的默认值,这些值会实际渲染到组件的 attributes 上。通过 attrs 传递的属性会默认转为字符串。注意:attrs 和 data 的 key 不能重名。:tokey="fromkey" 语法可以将当前页面/组件模块的 fromkey 属性单向传递给目标组件模块的 tokey 属性,上层模块改动后会同步更新组件模块的属性值。sync:toKey="fromKey" 语法可以将当前页面/组件模块的 fromKey 属性与目标组件模块的 toKey 属性双向同步,任一方改动都会同步到另一方。class:className="fromKey" 语法,当当前页面/组件模块的 fromKey 属性为 true 时,会将该类名添加到目标组件模块上;反之则移除。attr:toKey="fromKey" 语法可以将当前页面/组件模块的 fromKey 属性传递给目标组件模块的 toKey 属性(attributes property);如果为 null,则移除该属性。<slot></slot> 标签用于定义组件的插槽,其他组件可以将内容插入到插槽中;也可以给 slot 添加 name 属性,与 Web Component 的命名插槽一致。this.emit("eventName", options) 方法触发自定义事件,其中 options 为参数对象,可使用:
options.data:添加自定义事件数据。options.bubbles:事件是否冒泡,默认为 true。options.composed:事件是否穿透 Shadow DOM 边界,默认为 false。cancelable 等。on:eventName="handler" 语法,监听目标组件模块的自定义事件;这个写法等同于 on:eventName="handler($event)",会传递Event对象给handler函数。本案例展示一个 Todo List 的实现,涵盖列表渲染、条件渲染和响应式状态数据等用法。
o-fill 组件进行列表渲染,$data 代表当前项的数据,$index 代表当前项的索引,$host 代表当前页面/组件模块的实例。在列表项上绑定事件时,需要调用 $host 上的方法,格式为 $host.methodName($data),通过 $data 将当前项数据传递给处理函数。o-if / o-else-if / o-else 组件进行条件渲染,:value="bool" 为布尔值,用于判断是否渲染组件节点。$.stanz() 可以创建响应式状态数据,例如 todos。状态数据支持以下监听方式:
watchTick 监听数据变化,返回监听ID wid。watch 同步监听数据变化(实时触发),返回监听ID wid。unwatch(wid) 可以取消监听。attached 生命周期中将响应式状态数据赋值给当前模块,在 detached 生命周期中清空引用。detached 生命周期中取消监听,否则内存泄漏。本案例展示一个文件管理器的实现,涵盖数据监听、跨层级数据同步、o-provider/o-consumer 上下文通信和递归列表渲染等高级用法。
<o-provider name="providerName" sync:custom-name="selfKey"> 标签包裹子组件,通过 sync:custom-name 将当前模块的数据同步到上下文中。上下文的属性变化会自动通知到所有子组件。o-consumer 组件会同步相同 name 的上层 o-provider 组件的属性变化(穿透 Shadow DOM 边界),再通过 watch:custom-name 监听将上下文属性变化同步到当前组件。o-provider 和 o-consumer 可大幅提升开发效率。this.getProvider(providerName) 获取上下文对象,直接修改 custom-name 上下文属性即可同步到其他子组件,无需逐层传递(记得修正驼峰命名)。watch 对象,为需要监听的属性定义对应的监听函数。当属性值发生变化时,监听函数将自动触发。o-fill 可通过 name 属性渲染指定名称的 template,该模板可递归渲染自身或其他 name 的模板,实现多级列表的嵌套渲染。详细文档:Provider 与上下文状态
本案例展示一个完整的单页应用,涵盖 o-router 路由、多级嵌套页面、页面切换动画和路由监听等高级用法。
<slot> 渲染子页面。<o-router> 包裹 <o-app> 构建单页应用。app-config.js 配置文件通过 export const home 定义应用首页,通过 export const pageAnime 定义页面切换动画。<slot></slot> 标签预留子页面渲染位置,子页面通过 export const parent 指定父页面路径建立嵌套关系。<a href="./about.html" olink>About</a>,也可以在 proto 方法中使用 this.goto("./about.html") 进行编程式跳转,两者都支持浏览器前进/后退和 URL 同步。{ query } 参数获取 URL 查询参数,如 export default async ({ query }) => {...}。routerChange 生命周期和 ready 生命周期监听路由变化,刷新导航高亮等状态。本案例展示 ofa.js 的同构渲染(Symphony Client-Server Rendering)模式,每个页面本身是完整的 HTML 文件,服务端直接返回该文件即可实现 SSR。
<o-app src="./app-config.js"><template page>...</template></o-app> 结构。template page 内容替换为实际的页面模块内容即可;不依赖特定服务器语言,任何后端环境都可以实现 SSR。Content-Type: text/html; charset=UTF-8。除模板语法外,ofa.js 提供丰富的 JS API,包括元素选择($、$.all)、节点操作(push、remove、wrap)、属性样式(text、html、css、attr)、事件处理(on、emit、off)、Shadow DOM 访问(shadow)、响应式数据($.stanz、watch)等。完整 API 列表请查看 ofa.js API 参考。
this.shadow.$() 访问影子节点加载模块与第三方库:export default 函数中 load 和 url 参数的用法
官方组件:replace-temp 和 inject-host 组件的用法
match-var 样式查询:根据 CSS 变量切换样式的用法
assets/01-start/page.html
<template page>
<style>
:host {
display: block;
color: Green;
}
h2 {
font-size: 18px;
}
</style>
<h2>{{val}} - {{doubleNum}}</h2>
<button on:click="handleButtonClick">Click me</button>
<!-- <button on:click="addNum">Add num</button> -->
<button on:click="num++">Add num</button>
<script>
export default async ({ query }) => {
return {
data: {
val: query.val || "Hello World",
num: 0,
},
proto: {
handleButtonClick() {
this.val = "Change the value";
},
// addNum() {
// this.num++;
// },
get doubleNum() {
return this.num * 2;
},
},
};
};
</script>
</template>
assets/01-start/demo.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.min.mjs"
type="module"
></script>
</head>
<body>
<o-page src="./page.html?val=Hello+ofa.js"></o-page>
</body>
</html>
assets/02-switch/switch.html
<template component>
<style>
:host {
display: inline-flex;
align-items: center;
vertical-align: middle;
}
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 22px;
cursor: pointer;
transition: opacity 0.2s;
}
.switch.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.switch-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.switch-slider {
position: absolute;
cursor: inherit;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.3s;
border-radius: 22px;
}
.switch-slider::before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.switch.checked .switch-slider {
background-color: #4caf50;
}
.switch.checked .switch-slider::before {
transform: translateX(22px);
}
.switch-label {
margin-left: 8px;
vertical-align: middle;
font-size: 14px;
color: #333;
}
.label {
margin-left: 8px;
vertical-align: middle;
font-size: 14px;
color: #333;
cursor: pointer;
}
:host(:empty) .label {
display: none;
}
</style>
<div
class="switch"
class:disabled="disabled !== null"
class:checked="checked"
on:click="toggle"
>
<input
type="checkbox"
class="switch-input"
attr:checked="checked"
attr:disabled="disabled"
/>
<span class="switch-slider"></span>
</div>
<div class="label" on:click="toggle">
<slot></slot>
</div>
<script>
export default async () => {
return {
tag: "ofa-switch",
attrs: {
disabled: null,
},
data: {
checked: false,
},
proto: {
toggle() {
if (this.disabled !== null) {
return;
}
this.checked = this.checked ? null : true;
this.emit("change", {
data: { checked: this.checked },
bubbles: true,
composed: true,
});
},
},
};
};
</script>
</template>
assets/02-switch/page.html
<template page>
<!-- 引入组件 -->
<l-m src="./switch.html"></l-m>
<style>
:host {
display: block;
}
</style>
<style>
h2 {
color: #333;
margin-bottom: 20px;
}
.demo-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fafafa;
}
.demo-section h3 {
margin-top: 0;
color: #666;
font-size: 14px;
margin-bottom: 15px;
}
.demo-row {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 15px;
}
.demo-row:last-child {
margin-bottom: 0;
}
.status-text {
font-size: 14px;
color: #666;
}
</style>
<h2>Switch 开关组件演示</h2>
<!-- 基础用法 -->
<div class="demo-section">
<h3>基础用法</h3>
<div class="demo-row">
<ofa-switch></ofa-switch>
<span class="status-text">默认关闭</span>
</div>
<div class="demo-row">
<ofa-switch :checked="true"></ofa-switch>
<span class="status-text">默认开启</span>
</div>
</div>
<!-- 带标签 -->
<div class="demo-section">
<h3>带标签</h3>
<div class="demo-row">
<ofa-switch>启用通知</ofa-switch>
</div>
<div class="demo-row">
<ofa-switch :checked="boolTrue">自动保存</ofa-switch>
</div>
</div>
<!-- 双向绑定 -->
<div class="demo-section">
<h3>双向绑定</h3>
<div class="demo-row">
<ofa-switch sync:checked="switchState"></ofa-switch>
<span class="status-text"
>当前状态: {{switchState ? '开启' : '关闭'}}</span
>
</div>
</div>
<!-- 禁用状态 -->
<div class="demo-section">
<h3>禁用状态</h3>
<div class="demo-row">
<ofa-switch disabled></ofa-switch>
<span class="status-text">禁用状态(关闭)</span>
</div>
<div class="demo-row">
<ofa-switch :checked="true" disabled></ofa-switch>
<span class="status-text">禁用状态(开启)</span>
</div>
</div>
<!-- 事件监听 -->
<div class="demo-section">
<h3>事件监听</h3>
<div class="demo-row">
<ofa-switch on:change="handleSwitchChange"></ofa-switch>
<span class="status-text">点击次数: {{clickCount}}</span>
</div>
</div>
<script>
export default async () => {
return {
data: {
boolTrue: true,
switchState: false,
clickCount: 0,
},
proto: {
handleSwitchChange(e) {
this.clickCount++;
console.log("Switch changed:", e.data.checked);
},
},
};
};
</script>
</template>
assets/02-switch/demo.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ofa.js Switch Component Demo</title>
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.min.mjs"
type="module"
></script>
<style>
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 40px;
max-width: 600px;
margin: 0 auto;
}
</style>
</head>
<body>
<o-page src="./page.html"></o-page>
<script type="module">
$("o-page").on("change", (e) => {
console.log("Switch changed:", e.data.checked);
});
</script>
</body>
</html>
assets/03-todolist/page.html
<template page>
<style>
:host {
display: block;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.input-row {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
background: #007bff;
color: white;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #0056b3;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
border: 1px solid #e0e0e0;
border-radius: 6px;
margin-bottom: 10px;
background: #fafafa;
}
.todo-item.completed {
background: #e8f5e9;
border-color: #c8e6c9;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #888;
}
.todo-text {
flex: 1;
}
.delete-btn {
padding: 5px 10px;
background: #dc3545;
font-size: 12px;
}
.delete-btn:hover {
background: #c82333;
}
.empty-tip {
text-align: center;
color: #888;
padding: 40px;
}
</style>
<h2>Todo List</h2>
<div class="input-row">
<input
type="text"
sync:value="inputText"
placeholder="输入待办事项..."
on:keydown="handleKeydown"
/>
<button on:click="addTodo">添加</button>
</div>
<o-if :value="todos.length === 0">
<p class="empty-tip">暂无待办事项,添加一个吧~</p>
</o-if>
<o-else-if :value="remainingCount === 0">
<p class="empty-tip">所有任务都已完成!🎉</p>
</o-else-if>
<o-else>
<p style="color: #666; margin-bottom: 10px">
共 {{todos.length}} 项任务,还有 {{remainingCount}} 项未完成
</p>
<ul class="todo-list">
<o-fill :value="todos">
<li class="todo-item" class:completed="$data.completed">
<input
type="checkbox"
:checked="$data.completed"
on:change="$host.toggleTodo($data,$index)"
/>
<span class="todo-text">{{$data.text}}</span>
<button class="delete-btn" on:click="$host.deleteTodo($index)">
删除
</button>
</li>
</o-fill>
</ul>
</o-else>
<script>
export default async ({ load }) => {
const { todos } = await load("./data.js");
return {
data: {
todos: [],
inputText: "",
},
proto: {
addTodo() {
const text = this.inputText.trim();
if (!text) return;
this.todos.push({ text, completed: false });
this.inputText = "";
},
handleKeydown(e) {
if (e.key === "Enter") {
this.addTodo();
}
},
toggleTodo(data, index) {
// const data = this.todos[index];
data.completed = !data.completed;
},
deleteTodo(index) {
this.todos.splice(index, 1);
},
get remainingCount() {
return this.todos.filter((t) => !t.completed).length;
},
},
attached() {
this.todos = todos;
},
detached() {
this.todos = [];
},
};
};
</script>
</template>
assets/03-todolist/demo.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ofa.js Todo List Demo</title>
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.min.mjs"
type="module"
></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 40px;
max-width: 600px;
margin: 0 auto;
}
</style>
</head>
<body>
<o-page src="./page.html"></o-page>
</body>
</html>
assets/04-filelist/demo.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ofa.js File List Demo</title>
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.min.mjs"
type="module"
></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 0;
height: 100vh;
}
</style>
</head>
<body>
<o-page src="./page.html"></o-page>
</body>
</html>
assets/04-filelist/page.html
<template page>
<style>
:host {
display: flex;
flex-direction: column;
height: 100vh;
}
</style>
<l-m src="./filelist.html"></l-m>
<l-m src="./editor.html"></l-m>
<div style="text-align: center; padding: 4px; border-bottom: 1px solid #ccc">
{{currentFile}}
</div>
<div style="display: flex; flex: 1">
<o-provider name="project-data" sync:active-path="currentFile">
<file-list></file-list>
<file-editor></file-editor>
</o-provider>
</div>
<script>
export default async () => {
return {
data: {
currentFile: null,
},
};
};
</script>
</template>
assets/04-filelist/filelist.html
<template component>
<style>
:host {
display: block;
width: 300px;
border-right: 1px solid #e0e0e0;
overflow: auto;
padding: 20px;
background: #fafafa;
}
.dir-item {
margin-bottom: 5px;
}
.dir-toggle {
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 5px;
}
.dir-toggle:hover {
background: #e0e0e0;
}
.dir-toggle::before {
content: "▶";
font-size: 10px;
transition: transform 0.2s;
}
.dir-toggle.opened::before {
transform: rotate(90deg);
}
.file-item {
padding-left: 14px;
cursor: pointer;
border-radius: 4px;
}
.file-item:hover {
background: #e0e0e0;
}
.file-item.active {
background: #007bff;
color: white;
}
</style>
<o-fill :value="filelist.children" name="file-tree"> </o-fill>
<template name="file-tree">
<div>
<o-if :value="$data.type === 'dir'">
<div class="dir-item">
<div
class="dir-toggle"
class:opened="$data.opened"
on:click="$host.toggle($data,$event)"
>
<span>{{$data.name}}</span>
</div>
<div
style="padding-left: 20px"
:style.display="$data.opened ? 'block' : 'none'"
>
<o-if :value="$data.children.length > 0">
<o-fill :value="$data.children" name="file-tree"></o-fill>
</o-if>
<o-else>
<div style="color: #aaa; padding: 5px 10px; font-size: 12px">
(empty)
</div>
</o-else>
</div>
</div>
</o-if>
<o-else-if :value="$data.type === 'file'">
<div class="file-item" on:click="$host.selectFile($data)">
{{$data.name}}
</div>
</o-else-if>
</div>
</template>
<script>
export default async ({ load }) => {
const { fileData } = await load("./data.js");
return {
tag: "file-list",
data: {
filelist: fileData,
},
proto: {
toggle(data, event) {
event.stopPropagation();
data.opened = !data.opened;
},
selectFile(file) {
const provider = this.getProvider("project-data");
provider.activePath = file.path;
},
},
};
};
</script>
</template>
assets/04-filelist/editor.html
<template component>
<style>
:host {
display: block;
flex: 1;
padding: 20px;
overflow: auto;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #888;
font-size: 18px;
}
.editor-content {
font-family: "Courier New", monospace;
white-space: pre-wrap;
line-height: 1.6;
}
.file-name {
font-size: 14px;
color: #666;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
</style>
<o-if :value="activePath"> {{fileContent}} </o-if>
<o-else>
<div class="placeholder">编辑器区域(等待联动效果)</div>
</o-else>
<o-consumer name="project-data" watch:active-path="activePath"></o-consumer>
<script>
export default async ({ load }) => {
const { getContent } = await load("./data.js");
return {
tag: "file-editor",
data: {
activePath: null,
fileContent: "",
},
watch: {
async activePath(path) {
if (!path) {
return;
}
this.fileContent = await getContent(path);
},
},
};
};
</script>
</template>
assets/04-filelist/data.js
export const fileData = {
name: "root",
type: "dir",
children: [
{
name: "src",
type: "dir",
path: "/src",
children: [
{
name: "components",
type: "dir",
children: [],
path: "/src/components",
},
{
name: "utils",
type: "dir",
children: [],
path: "/src/utils",
},
{
name: "index.js",
type: "file",
path: "/src/index.js",
content: "// Main entry point\nconsole.log('Hello World');",
},
],
},
{
name: "docs",
type: "dir",
path: "/docs",
children: [
{
name: "README.md",
type: "file",
path: "/docs/README.md",
content: "# Documentation\n\nThis is a sample documentation file.",
},
{
name: "CHANGELOG.md",
type: "file",
path: "/docs/CHANGELOG.md",
content: "# Changelog\n\n## v1.0.0\n- Initial release",
},
],
},
{
name: "config.json",
type: "file",
path: "/config.json",
content: '{\n "name": "my-project",\n "version": "1.0.0"\n}',
},
{
name: "package.json",
type: "file",
path: "/package.json",
content:
'{\n "name": "example-project",\n "scripts": {\n "dev": "node dev.js"\n }\n}',
},
{
name: "README.md",
type: "file",
path: "/README.md",
content: "# My Project\n\nA sample project for ofa.js demo.",
},
],
};
export const getContent = async (path) => {
const findFile = (items, targetPath) => {
for (const item of items) {
if (item.path === targetPath) {
return item;
}
if (item.children && item.children.length > 0) {
const found = findFile(item.children, targetPath);
if (found) {
return found;
}
}
}
return null;
};
const file = findFile(fileData.children, path);
return file?.content || "";
};
assets/05-routing/demo.html
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ofa.js Router Demo</title>
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.mjs"
type="module"
></script>
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
o-app {
height: 100%;
}
</style>
</head>
<body>
<l-m
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/libs/router/dist/router.min.mjs"
></l-m>
<o-router>
<o-app src="./app-config.js"></o-app>
</o-router>
</body>
</html>
assets/05-routing/app-config.js
export const home = "./home.html";
export const pageAnime = {
current: {
opacity: 1,
transform: "translate(0, 0)",
},
next: {
opacity: 0,
transform: "translate(30px, 0)",
},
previous: {
opacity: 0,
transform: "translate(-30px, 0)",
},
};
assets/05-routing/layout.html
<template page>
<style>
:host {
display: block;
height: 100%;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
}
header {
padding: 15px 20px;
background: #333;
color: #fff;
}
nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 20px;
}
nav li {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
nav li:hover {
background: #555;
}
nav li.active {
background: #007bff;
}
nav li a {
color: inherit;
text-decoration: none;
}
.main {
flex: 1;
padding: 20px;
}
</style>
<div class="container">
<header>
<nav>
<ul>
<li class:active="active1">
<a href="./home.html" olink>首页</a>
</li>
<li class:active="active2">
<a href="./list.html" olink>列表</a>
</li>
<li class:active="active3">
<a href="./detail.html?id=1" olink>详情</a>
</li>
</ul>
</nav>
</header>
<div class="main">
<slot></slot>
</div>
</div>
<script>
export default () => {
return {
data: {
active1: false,
active2: false,
active3: false,
},
routerChange() {
this.refreshActive();
},
ready() {
this.refreshActive();
},
proto: {
refreshActive() {
const { current } = this.app;
const path = new URL(current.src).pathname;
this.active1 = path.endsWith('home.html');
this.active2 = path.includes('list');
this.active3 = path.includes('detail');
},
},
};
};
</script>
</template>
assets/05-routing/home.html
<template page>
<style>
:host {
display: block;
}
h1 {
color: #333;
}
.card {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
margin-top: 15px;
}
</style>
<h1>欢迎使用 ofa.js</h1>
<div class="card">
<p>这是一个使用 o-router 和 o-app 的单页应用示例。</p>
<p>特点:</p>
<ul>
<li>使用 o-router 实现路由功能</li>
<li>支持浏览器前进/后退</li>
<li>支持页面切换动画</li>
<li>支持多级嵌套页面</li>
</ul>
</div>
<script>
export const parent = "./layout.html";
export default async () => {
return {
data: {},
};
};
</script>
</template>
assets/05-routing/list.html
<template page>
<style>
:host {
display: block;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.tab.active {
background: #007bff;
color: #fff;
border-color: #007bff;
}
.tab a {
color: inherit;
text-decoration: none;
}
h2 {
color: #333;
margin-bottom: 15px;
}
</style>
<h2>商品列表</h2>
<div class="tabs">
<div class="tab" class:active="subActive === 'page1'">
<a href="./list-page1.html" olink>推荐商品</a>
</div>
<div class="tab" class:active="subActive === 'page2'">
<a href="./list-page2.html" olink>热门商品</a>
</div>
</div>
<slot></slot>
<script>
export const parent = "./layout.html";
export default async () => {
return {
data: {
subActive: 'page1',
},
routerChange() {
this.refreshSubActive();
},
ready() {
this.refreshSubActive();
},
proto: {
refreshSubActive() {
const { current } = this.app;
const path = new URL(current.src).pathname;
this.subActive = path.includes('page1') ? 'page1' : 'page2';
},
},
};
};
</script>
</template>
assets/05-routing/list-page1.html
<template page>
<style>
:host {
display: block;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.item {
padding: 12px;
background: #fff;
border-radius: 4px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.item a {
color: #007bff;
text-decoration: none;
}
</style>
<div class="item">
<h3>推荐商品 1</h3>
<p>这是推荐商品的描述...</p>
<button on:click="goDetail(101)">查看详情</button>
</div>
<div class="item">
<h3>推荐商品 2</h3>
<p>这是推荐商品的描述...</p>
<button on:click="goDetail(102)">查看详情</button>
</div>
<div class="item">
<h3>推荐商品 3</h3>
<p>这是推荐商品的描述...</p>
<button on:click="goDetail(103)">查看详情</button>
</div>
<script>
export const parent = "./list.html";
export default async () => {
return {
data: {},
proto: {
goDetail(id) {
this.goto(`./detail.html?id=${id}`);
},
},
};
};
</script>
</template>
assets/05-routing/list-page2.html
<template page>
<style>
:host {
display: block;
padding: 15px;
background: #fff3cd;
border-radius: 8px;
}
.item {
padding: 12px;
background: #fff;
border-radius: 4px;
margin-bottom: 10px;
border-left: 4px solid #ffc107;
}
.item a {
color: #007bff;
text-decoration: none;
}
.badge {
display: inline-block;
padding: 2px 8px;
background: #dc3545;
color: #fff;
border-radius: 12px;
font-size: 12px;
}
</style>
<div class="item">
<h3>热门商品 1 <span class="badge">HOT</span></h3>
<p>这是热门商品的描述...</p>
<a href="./detail.html?id=201" olink>查看详情</a>
</div>
<div class="item">
<h3>热门商品 2 <span class="badge">HOT</span></h3>
<p>这是热门商品的描述...</p>
<a href="./detail.html?id=202" olink>查看详情</a>
</div>
<script>
export const parent = "./list.html";
export default async () => {
return {
data: {},
};
};
</script>
</template>
assets/05-routing/detail.html
<template page>
<style>
:host {
display: block;
}
.detail-card {
padding: 30px;
border: 1px solid #ddd;
border-radius: 8px;
background: #fff;
}
.back-btn {
padding: 10px 20px;
background: #6c757d;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: 20px;
}
.back-btn:hover {
background: #5a6268;
}
h1 {
color: #333;
margin-bottom: 20px;
}
.info {
color: #666;
line-height: 1.8;
}
</style>
<button class="back-btn" on:click="goBack">返回列表</button>
<div class="detail-card">
<h1>商品详情 {{id}}</h1>
<div class="info">
<p><strong>商品ID:</strong>{{id}}</p>
<p><strong>当前路由:</strong>{{src}}</p>
<p>这是商品详情页面,展示了嵌套路由的用法。</p>
</div>
</div>
<script>
export const parent = "./layout.html";
export default async ({ query }) => {
return {
data: {
id: query.id || '未知',
},
proto: {
goBack() {
this.back();
},
},
};
};
</script>
</template>
assets/06-scsr/home.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SCSR Home</title>
<link rel="stylesheet" href="./public.css" />
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.mjs"
type="module"
></script>
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/libs/scsr/dist/scsr.min.mjs"
type="module"
></script>
</head>
<body>
<o-app src="./app-config.js">
<!-- 服务端填充:替换整个 <template page> 内容 -->
<template page>
<style>
:host {
display: block;
padding: 20px;
}
nav a {
margin-right: 15px;
color: #007bff;
}
</style>
<h1>首页</h1>
<nav>
<a href="./home.html" olink>首页</a>
<a href="./about.html" olink>关于</a>
</nav>
<p>服务端渲染的首页内容(SEO 友好)。</p>
<button on:click="goto('./about.html')">跳转到关于</button>
<script>
export default function () {
return {
data: {},
};
}
</script>
</template>
</o-app>
</body>
</html>
assets/06-scsr/about.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SCSR About</title>
<link rel="stylesheet" href="./public.css" />
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.mjs"
type="module"
></script>
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/libs/scsr/dist/scsr.min.mjs"
type="module"
></script>
</head>
<body>
<o-app src="./app-config.js">
<!-- 服务端填充:替换整个 <template page> 内容 -->
<template page>
<style>
:host {
display: block;
padding: 20px;
}
nav a {
margin-right: 15px;
color: #007bff;
}
</style>
<h1>关于我们</h1>
<nav>
<a href="./home.html" olink>首页</a>
<a href="./about.html" olink>关于</a>
</nav>
<p>服务端渲染的关于页面内容。</p>
<button on:click="back()">返回</button>
<script>
export default function () {
return {
data: {},
};
}
</script>
</template>
</o-app>
</body>
</html>
assets/06-scsr/app-config.js
export const pageAnime = {
current: {
opacity: 1,
transform: "translate(0, 0)",
},
next: {
opacity: 0,
transform: "translate(30px, 0)",
},
previous: {
opacity: 0,
transform: "translate(-30px, 0)",
},
};
assets/06-scsr/public.css
html,
body {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
o-app {
display: block;
height: 100%;
}
assets/07-api/shadow-demo.html
<template component>
<style>
:host {
display: block;
border: 1px solid #007acc;
padding: 15px;
margin: 10px 0;
}
.label {
color: #666;
font-size: 14px;
}
button {
margin-top: 10px;
padding: 8px 16px;
background: #007acc;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background: #005999;
}
</style>
<p class="label">影子节点内的文本:</p>
<p id="shadow-text">原始文本</p>
<button on:click="changeShadowText('内部修改文本','red')">
点击修改影子节点文本
</button>
<script>
export default async () => {
return {
tag: "shadow-demo",
proto: {
changeShadowText(text, color) {
this.shadow.$("#shadow-text").text = text;
this.shadow.$("#shadow-text").style.color = color || "inherit";
},
},
};
};
</script>
</template>
assets/07-api/demo.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shadow DOM 操作示例 - ofa.js</title>
<script
src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.min.mjs"
type="module"
></script>
</head>
<body>
<l-m src="./shadow-demo.html"></l-m>
<shadow-demo id="myComponent"></shadow-demo>
<div style="margin: 20px">
<h2>Shadow DOM 操作示例</h2>
<div
style="
background: #f5f5f5;
padding: 15px;
margin: 10px 0;
border-left: 4px solid #007acc;
"
>
<p><strong>功能说明:</strong></p>
<ul>
<li>
组件内部按钮:点击后通过
<code>this.shadow.$("#shadow-text").text</code> 修改影子节点内的文本
</li>
<li>
外部调用:通过
<code>$("shadow-demo").changeShadowText()</code>
直接调用组件上的方法
</li>
</ul>
</div>
<div style="margin-top: 15px">
<button
onclick="
$('shadow-demo').changeShadowText('外面调用方法修改新文本', 'blue')
"
style="
background: #28a745;
color: white;
padding: 8px 16px;
border: none;
cursor: pointer;
"
>
外部调用组件方法
</button>
<button
onclick="
alert(
'影子节点内的文本:' +
$('shadow-demo').shadow.$('#shadow-text').text,
)
"
style="
background: #28a745;
color: white;
padding: 8px 16px;
border: none;
cursor: pointer;
"
>
获取影子节点文本
</button>
</div>
</div>
</body>
</html>
export default 函数可接收 { load, url } 参数,用于动态加载模块和第三方库。
url:当前模块完整 URLload:加载模块/资源,支持 JSON、第三方 ES Module,与 <l-m> 功能一致且共享缓存<template component>
<div :html="content"></div>
<script>
export default async ({ load, url }) => {
const { marked } = await load("https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.esm.js");
return {
tag: "md-view",
attrs: { src: null },
data: { content: "" },
watch: { src() { this.loadMd(); } },
proto: {
async loadMd() {
this.content = marked.parse(await (await fetch(this.src)).text());
}
}
};
};
</script>
</template>
<script>
export default async ({ load }) => {
const { store } = await load("./data.js");
return {
data: {
localStore: {},
},
attached() {
this.localStore = store;
},
detached() {
this.localStore = {};
},
};
};
</script>
<script>
export default async ({ load }) => {
const config = await load("./config.json");
return {
data: { settings: config },
};
};
</script>
load 方法与 <l-m> 标签功能一致,但可以确保目标组件完全加载完成后,才进入当前页面或组件模块的初始化流程。
<script>
export default async ({ load }) => {
// 等待 header 和 footer 组件完全加载后,才继续执行
await load("./components/header.html");
await load("./components/footer.html");
return {
data: {},
};
};
</script>
ofa.js 提供了一些官方组件,用于解决特定场景下的问题。
用途:在 select、table、tbody 等对内部标签结构有要求的场景中做列表渲染。
<select>
<template is="replace-temp">
<x-fill :value="items">
<option>{{$data}}</option>
</x-fill>
</template>
</select>
规则:
o-fill / x-fill 不能正常工作时再使用。用途:从组件内部向宿主注入样式,用于控制插槽内容里的深层元素样式。
优先级:
::slotted() 就先用 ::slotted()。::slotted() 不够用时再用 inject-host。<template component>
<inject-host>
<style>
user-list user-list-item .user-list-item-content {
color: red;
}
</style>
</inject-host>
<slot></slot>
<script>
export default async () => ({
tag: "user-list",
});
</script>
</template>
注意:
inject-host 会把内部样式注入宿主作用域。.content、div 这类通用选择器。推荐:
<inject-host>
<style>
user-list .list-item-content {
color: red;
}
</style>
</inject-host>
不推荐:
<inject-host>
<style>
.content {
color: red;
}
</style>
</inject-host>
match-var 适合根据 CSS 变量切换样式,常用于主题场景。
<template component>
<match-var theme="dark">
<style>
:host {
background: #333;
color: #fff;
}
</style>
</match-var>
<match-var theme="light">
<style>
:host {
background: #fff;
color: #333;
}
</style>
</match-var>
<slot></slot>
</template>
配合 data() 设置 CSS 变量:
<template page>
<style>
.wrap {
--theme: data(currentTheme);
}
</style>
<button onclick="changeTheme">切换主题</button>
<div class="wrap">
<theme-box></theme-box>
</div>
<script>
export default async () => ({
data: { currentTheme: "light" },
proto:{
changeTheme(value){
if(value && typeof value === "string"){
this.currentTheme = value;
return;
}
this.currentTheme = this.currentTheme === "light" ? "dark" : "light";
}
}
});
</script>
</template>
Firefox 浏览器在某些情况下可能无法自动检测到 CSS 变量的变化,此时可手动调用以下方法触发样式更新:
match-var本文档详细介绍 ofa.js 中 o-provider 和 o-consumer 上下文状态传递机制。
o-provider 组件用于定义共享数据的提供者。它通过 name 属性标识自己的名称,并通过属性来定义需要共享的数据。
<o-provider name="userInfo" custom-name="张三" custom-age="25">
<!-- 子组件可以消费这些数据 -->
<my-component></my-component>
</o-provider>
特性:
o-consumer 组件用于消费(使用)提供者的数据。它通过 name 属性指定要消费的提供者名称。
<o-consumer name="userInfo" watch:custom-name="userName" watch:custom-age="userAge"></o-consumer>
<script>
export default async () => {
return {
data:{
userName: "",
userAge: 0,
},
};
};
</script>
o-root-provider 是根级别的全局提供者,它的作用域是整个文档。即使在没有父级 provider 的情况下,消费者也可以获取到根提供者的数据。
<!-- 定义全局根提供者 -->
<o-root-provider name="globalConfig" custom-theme="dark" custom-language="zh-CN"></o-root-provider>
<!-- 在页面的任何位置都可以消费 -->
<o-consumer name="globalConfig" watch:custom-theme="theme"></o-consumer>
优先级规则:当同时存在同 name 的 provider 和 root-provider 时,离消费者最近的 provider 优先。
getProvider(name) 是一个实例方法,用于获取对应 name 的提供者元素。
<script>
export default async () => {
return {
proto:{
changeValue(){
const provider = this.getProvider("userInfo");
provider.customName = "new value";
}
}
};
};
</script>
也可以通过元素上获取 provider:
// 获取当前元素上级的 provider
const provider = $(".my-element").getProvider("userInfo");
// 直接获取全局 root-provider
const globalProvider = $.getRootProvider("globalConfig");
provider 可以派发事件给所有消费它的 consumer:
<o-provider name="chat" id="chatProvider" custom-messages='["欢迎来到聊天室"]'>
<chat-list></chat-list>
</o-provider>
<script>
// 派发事件
$("#chatProvider").dispatch("new-message", {
data: { text: "Hello World" }
});
</script>
消费者监听事件:
<o-consumer name="chat" on:new-message="addMessage"></o-consumer>
<script>
export default async () => {
return {
data: { messages: [] },
proto: {
addMessage(event) {
this.messages.push(event.data.text);
}
}
};
};
</script>
相比事件冒泡和属性逐层传递,当涉及多层嵌套的数据同步时,使用 o-provider 和 o-consumer 可大幅提升开发效率。
子组件中使用 this.getProvider(providerName) 获取上下文对象,直接修改 custom-name 上下文属性即可同步到其他子组件,无需逐层传递(记得修正驼峰命名)。
本文档提供 ofa.js 的完整 API 参考。
$()$("#target");
el.$("h3");
$('my-component').shadow.$("selector");
$(ele);
$("<div>hello</div>");
$({ tag: "div", text: "hello", css: { color: "red" } });
$.all()$.all("li").forEach((item, index) => {
item.text = `change item ${index}`;
});
$("#target").all("li");
ele - 原生 DOM 元素$("#target").ele.innerHTML = '<b>change</b>';
tag - 标签名$("#target").tag;
shadow - Shadow DOMthis.shadow.$("#target").text = 'change';
$("test-shadow").shadow.$("#target").text = 'change';
root - 根节点$("#target").root.ele === document;
this.shadow.$("#target").root === this.shadow;
host - 宿主组件this.host.sayHi();
$("#target").host;
hosts - 所有宿主链$("#target").hosts;
parent - 父元素$("#target").parent.css.color = 'blue';
parents - 所有祖先元素$("#target").parents.forEach(el => console.log(el.tag));
parentsUntil(expr) - 祖先元素直到条件$("#target").parentsUntil(".stop-container");
next - 后一个兄弟$("#target").next.text = "next";
nexts - 后面所有兄弟$("#target").nexts.forEach(el => el.css.color = 'blue');
prev - 前一个兄弟$("#target").prev.text = "prev";
prevs - 前面所有兄弟$("#target").prevs.forEach(el => el.css.color = 'red');
siblings - 所有兄弟$("#target").siblings.forEach(e => e.text = 'change');
index - 在父元素中的索引$("#target").index;
length / children - 子元素数量$('ul').length;
$('ul')[1];
push / unshift / pop / shift / splice$("ul").push("<li>new</li>");
$("ul").unshift("<li>first</li>");
$("ul").pop();
$("ul").shift();
$("ul").splice(1, 1, "<li>replace</li>");
before / after$("#target").before("<div>before</div>");
$("#target").after("<div>after</div>");
remove$("#target").remove();
wrap / unwrap$("#target").wrap("<div class='wrap'></div>");
$("#target").unwrap();
clone(bool)$("#target").clone();
is(expr) - 检测选择器匹配$("#target").is('li');
$("#target").is('[class]');
contains(expr) - 检测包含$("#target").contains('.child');
$("#target").contains(childEl);
composedPath() - 组合路径$("#target").composedPath();
text$("#target").text;
$("#target").text = "new text";
html$("#target").html;
$("#target").html = "<b>hello</b>";
attr(name) / attr(name, val)$("#target").attr('title');
$("#target").attr('title', 'tip');
css - 计算样式$("#target").css.color;
$('#target').css.color = 'red';
$("#target").css = { color: "blue", lineHeight: "5em" };
style - style 属性$("#target").style.color;
$('#target').style.color = 'red';
classList$("#target").classList.add('active');
$("#target").classList.remove('t-red');
$("#target").classList.toggle('active');
data - dataset$("#target").data.red;
$('#target').data.red = "1";
on(event, handler)$("#target").on("click", handler);
one(event, handler) - 一次性$("#target").one("click", () => console.log("once"));
emit(event, options)$("#target").emit("custom", {
data: { value: 1 },
bubbles: true,
composed: false,
});
off(event, handler)$("#target").off("click", handler);
$("o-app").src;
$("o-app").current;
$("o-app").routers;
$("o-app").goto("/page2.html");
$("o-app").replace("/new-page.html");
$("o-app").back();
this.goto("./page2.html");
this.replace("./new-page.html");
this.back();
formData()const formState = $("#myForm").formData();
formState.watch(() => console.log(formState.username));
formState.username = "Yao";
$.stanz()const store = $.stanz({ count: 0 });
store.watch(() => console.log(store.count));
store.count++;
watch / watchTick / unwatchconst tid = target.watch(() => {});
target.watchTick(() => {});
target.unwatch(tid);
_ 前缀target._cache = { done: true };
extend()$("#target").extend({
say() { return "hello"; },
});
$.fn.extend({
get good() { return "good"; },
say() { return 'hello'; }
});
refresh()this.refresh();
versionofa.version;
development
ofa.js 框架教程。当用户询问 ofa.js 的使用方法、组件开发、页面模块,或想要构建无需 Node.js/Webpack 的 Web 应用时使用。
development
Complete documentation knowledge base for ofa.js framework. Use when users ask about ofa.js usage, component development, page modules, routing configuration, state management, or want to build Web applications without Node.js/Webpack.
development
ofa.js 框架完整文档知识库。当用户询问 ofa.js 的使用方法、组件开发、页面模块、路由配置、状态管理,或想要构建无需 Node.js/Webpack 的 Web 应用时使用。
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.