skills/ofajs-docs-tiny/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.
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
│ └─ 否 → 独立页面
└─ 否 → 单页面应用
在 HTML 中直接引入 ofa.js:
<script src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.min.mjs" type="module"></script>
使用页面模块:
<o-page src="./page.html"></o-page>
使用 <template 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="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";
},
get doubleNum() {
return this.num * 2;
},
},
};
};
</script>
</template>
要点:
<script> 中,export default 函数返回对象,有且只能存在一个 script 标签{ query } 参数获取 URL 查询参数data 中定义,方法在 proto 中定义{{key}} 语法渲染数据为文本on:xxx 绑定事件,可用事件参考 DOM 事件get xxx 定义计算属性(不是 computed)<style> 中,:host 选择器定义模块自身样式count++、isShow = !isShow 可直接在事件属性中编写on:click="addNumber(5)"$event 参数,如 handleClick($event)_ 前缀属性为非响应式属性,变化不会触发视图更新_ 前缀存储:<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(); // ✅ 不会被转换
},
});
</script>
使用 <template component> 定义,返回对象中必须包含 tag 字段。
<template component>
<style>
:host { display: inline-flex; align-items: center; }
.switch { position: relative; display: inline-block; width: 44px; height: 22px; cursor: pointer; }
.switch-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; transition: 0.3s; border-radius: 22px; }
.switch-slider::before { position: absolute; content: ""; height: 18px; width: 18px; left: 2px; bottom: 2px; background: white; transition: 0.3s; border-radius: 50%; }
.switch.checked .switch-slider { background-color: #4caf50; }
.switch.checked .switch-slider::before { transform: translateX(22px); }
</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>
使用组件:
<template page>
<l-m src="./switch.html"></l-m>
<ofa-switch :checked="true"></ofa-switch>
<ofa-switch sync:checked="switchState"></ofa-switch>
<ofa-switch on:change="handleSwitchChange"></ofa-switch>
<script>
export default async () => ({
data: { switchState: false },
proto: {
handleSwitchChange(e) {
console.log("Switch changed:", e.data.checked);
},
},
});
</script>
</template>
要点:
<l-m src="path"> 引入组件模块,引入后即可使用标签名attrs 定义属性默认值,会渲染到组件的 attributes 上,通过 attrs 传递的属性会默认转为字符串attrs 和 data 的 key 不能重名query 参数| 语法 | 说明 |
|------|------|
| :toKey="fromKey" | 单向传递,上层改动后同步到组件 |
| sync:toKey="fromKey" | 双向绑定,任一方改动都会同步到另一方 |
| class:className="fromKey" | 当 fromKey 为 true 时添加类名,反之移除 |
| attr:toKey="fromKey" | 传递到 attributes 属性,null 则移除 |
<child-comp :value="parentValue"></child-comp>
<child-comp sync:value="parentValue"></child-comp>
<div class:active="isActive"></div>
<input attr:value="inputValue" attr:disabled="isDisabled">
<slot></slot> 定义默认插槽,<slot name="xxx"> 定义命名插槽,与 Web Component 一致。
<template component>
<div class="header"><slot name="header"></slot></div>
<div class="content"><slot></slot></div>
<script>export default async () => ({ tag: "my-card" });</script>
</template>
<!-- 使用 -->
<my-card>
<span slot="header">标题</span>
<p>内容</p>
</my-card>
this.emit("eventName", options) 触发自定义事件:
options.data:事件数据options.bubbles:是否冒泡(默认 true)options.composed:是否穿透 Shadow DOM(默认 false)上层监听:on:eventName="handler" 等同于 on:eventName="handler($event)",会传递 Event 对象。
使用 <o-fill :value="list"> 组件。
内置变量:$data(当前项数据)、$index(当前项索引)、$host(当前页面/组件模块实例)
<ul>
<o-fill :value="todos">
<li class:completed="$data.completed">
<input type="checkbox" :checked="$data.completed"
on:change="$host.toggleTodo($data,$index)" />
<span>{{$data.text}}</span>
<button on:click="$host.deleteTodo($index)">删除</button>
</li>
</o-fill>
</ul>
递归渲染(通过 name 属性定义模板,模板可递归引用自身):
<o-fill :value="tree.children" name="file-tree"></o-fill>
<template name="file-tree">
<div>
<o-if :value="$data.type === 'dir'">
<div on:click="$host.toggle($data,$event)">{{$data.name}}</div>
<div :style.display="$data.opened ? 'block' : 'none'">
<o-fill :value="$data.children" name="file-tree"></o-fill>
</div>
</o-if>
<o-else-if :value="$data.type === 'file'">
<div on:click="$host.selectFile($data)">{{$data.name}}</div>
</o-else-if>
</div>
</template>
动态 style 绑定::style.display="expr" 可动态设置 style 属性。
使用 <o-if> / <o-else-if> / <o-else> 组件,:value="bool" 为布尔值。
<o-if :value="todos.length === 0">
<p>暂无待办事项</p>
</o-if>
<o-else-if :value="remainingCount === 0">
<p>所有任务都已完成!</p>
</o-else-if>
<o-else>
<p>共 {{todos.length}} 项,还有 {{remainingCount}} 项未完成</p>
</o-else>
$.stanz() 创建响应式状态数据:
const store = $.stanz({ count: 0, items: [] });
const tid = store.watchTick(() => console.log('tick:', store.count)); // 下一帧触发
const wid = store.watch(() => console.log('change:', store.count)); // 实时触发
store.unwatch(wid); // 取消监听
使用外部响应式数据:在 attached 中赋值,在 detached 中清空引用并取消监听,否则内存泄漏。
export default async ({ load }) => {
const { todos } = await load("./data.js");
return {
data: { todos: [] },
attached() { this.todos = todos; },
detached() { this.todos = []; },
};
};
非响应式数据:_ 前缀属性为非响应式属性,变化不会触发视图更新。
this._cache = { done: true };
this._instance = new SomeClass(); // 自定义类实例用 _ 前缀
在模块参数中添加 watch 对象,为属性定义监听函数,属性值变化时自动触发。
export default async () => ({
data: { activePath: null, fileContent: "" },
watch: {
async activePath(path) {
if (!path) return;
this.fileContent = await getContent(path);
},
},
});
attached() - 元素添加到 DOM 时detached() - 元素从 DOM 移除时routerChange() - 路由变化时(布局页面使用)ready() - 页面加载完成时<!doctype html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/dist/ofa.mjs" type="module"></script>
<l-m src="https://cdn.jsdelivr.net/gh/ofajs/ofa.js/libs/router/dist/router.min.mjs"></l-m>
</head>
<body>
<o-router><o-app src="./app-config.js"></o-app></o-router>
</body>
</html>
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)" },
};
父页面使用 <slot></slot> 预留子页面渲染位置。
<template page>
<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>
</ul></nav></header>
<div class="main"><slot></slot></div>
<script>
export default () => ({
data: { active1: false, active2: 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');
},
},
});
</script>
</template>
子页面通过 export const parent 指定父页面路径建立嵌套关系。
<template page>
<h1>首页</h1>
<button on:click="goDetail">查看详情</button>
<script>
export const parent = "./layout.html";
export default async ({ query }) => ({
data: { id: query.id || '未知' },
proto: {
goDetail() { this.goto(`./detail.html?id=101`); },
},
});
</script>
</template>
<a href="./page.html" olink>链接</a>this.goto("./page.html")this.replace("./new.html")this.back(){ query } 参数获取 URL 查询参数用 name 标识,所有非保留属性作为共享数据,响应式更新。
<o-provider name="project-data" sync:active-path="currentFile">
<file-list></file-list>
<file-editor></file-editor>
</o-provider>
通过 name 匹配上层 provider,watch:xxx 监听上下文属性变化并同步到当前组件。
<o-consumer name="project-data" watch:active-path="activePath"></o-consumer>
export default async () => ({
data: { activePath: null },
watch: {
async activePath(path) {
if (!path) return;
this.fileContent = await getContent(path);
},
},
});
全局作用域,即使没有父级 provider 也可获取。同 name 时,离消费者最近的 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>
获取提供者对象,直接修改属性即可同步到其他子组件(注意驼峰命名)。
const provider = this.getProvider("project-data");
provider.activePath = "new-path";
// 获取全局根提供者
const globalProvider = $.getRootProvider("globalConfig");
provider 可派发事件给所有消费它的 consumer:
$("#chatProvider").dispatch("new-message", { data: { text: "Hello" } });
消费者监听:on:new-message="handler"
每个页面本身是完整的 HTML 文件,服务端直接返回即可实现 SSR。不依赖特定服务器语言。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<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>
<style>:host { display: block; padding: 20px; }</style>
<h1>首页</h1>
<nav><a href="./home.html" olink>首页</a><a href="./about.html" olink>关于</a></nav>
<script>
export default function () { return { data: {} }; }
</script>
</template>
</o-app>
</body>
</html>
要点:
<o-app><template page>...</template></o-app> 结构template page 内容替换为实际页面模块内容Content-Type: text/html; charset=UTF-8export default 函数可接收 { load, url } 参数。
url:当前模块完整 URLload:加载模块/资源,支持 JSON、第三方 ES Module,与 <l-m> 功能一致且共享缓存export default async ({ load, url }) => {
// 加载第三方库
const { marked } = await load("https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.esm.js");
// 加载本地模块
const { store } = await load("./data.js");
// 加载 JSON
const config = await load("./config.json");
// 等待组件加载完成后才继续初始化
await load("./components/header.html");
return { data: {} };
};
在 select、table、tbody 等对内部标签结构有要求的场景中做列表渲染。只有普通 o-fill / x-fill 不能正常工作时再使用。
<select>
<template is="replace-temp">
<x-fill :value="items"><option>{{$data}}</option></x-fill>
</template>
</select>
从组件内部向宿主注入样式,用于控制插槽内容里的深层元素样式。优先用 ::slotted(),不够时再用 inject-host。推荐使用带组件名前缀的具体选择器,避免通用选择器污染。
<template component>
<inject-host>
<style>
user-list .list-item-content { color: red; }
</style>
</inject-host>
<slot></slot>
<script>export default async () => ({ tag: "user-list" });</script>
</template>
根据 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>
配合 CSS 变量 data() 设置:
<template page>
<style>
.wrap { --theme: data(currentTheme); }
</style>
<button on:click="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 变量变化,需手动触发样式更新。
$("#target"); // 选择单个元素
el.$("h3"); // 在 el 内选择
$('my-comp').shadow.$("#target"); // 选择影子节点内元素
$("<div>hello</div>"); // 创建元素
$({ tag: "div", text: "hello", css: { color: "red" } }); // 创建带属性元素
$.all("li"); // 选择所有匹配元素
$.all("li", el); // 在 el 内选择所有
ele; // 原生 DOM 元素
tag; // 标签名
shadow; // Shadow DOM(this.shadow.$("#id") 访问影子节点)
root; // 根节点
host; // 宿主组件
hosts; // 所有宿主链
parent; // 父元素
parents; // 所有祖先元素
parentsUntil(expr); // 祖先元素直到条件
next; prev; // 后/前一个兄弟
nexts; prevs; // 后/前所有兄弟
siblings; // 所有兄弟
index; // 在父元素中的索引
length; children; // 子元素数量(通过 [n] 访问)
$("ul").push("<li>new</li>"); // 添加到末尾
$("ul").unshift("<li>first</li>"); // 添加到开头
$("ul").pop(); // 移除末尾
$("ul").shift(); // 移除开头
$("ul").splice(1, 1, "<li>replace</li>"); // 替换
$("#target").before("<div>before</div>"); // 在前插入
$("#target").after("<div>after</div>"); // 在后插入
$("#target").remove(); // 移除元素
$("#target").wrap("<div class='wrap'></div>"); // 包裹
$("#target").unwrap(); // 解包
$("#target").clone(); // 克隆
$("#target").is('li'); // 检测选择器匹配
$("#target").contains('.child'); // 检测包含
el.text; // 获取文本
el.text = "new text"; // 设置文本
el.html; // 获取 HTML
el.html = "<b>hello</b>"; // 设置 HTML
el.attr('title'); // 获取属性
el.attr('title', 'tip'); // 设置属性
el.css.color; // 获取计算样式
el.css.color = 'red'; // 设置样式
el.css = { color: "blue" }; // 批量设置样式
el.style.color; // style 属性
el.classList.add('active'); // 类名操作(add/remove/toggle)
el.data.red; // dataset
el.data.red = "1"; // 设置 dataset
el.on("click", handler); // 绑定事件
el.one("click", () => console.log("once")); // 一次性事件
el.emit("custom", { data: { value: 1 }, bubbles: true, composed: false }); // 触发事件
el.off("click", handler); // 解绑事件
$("o-app").src; // 配置文件路径
$("o-app").current; // 当前页面
$("o-app").routers; // 路由历史
$("o-app").goto("/page2.html"); // 跳转
$("o-app").replace("/new.html"); // 替换
$("o-app").back(); // 后退
this.goto("./page2.html"); // 页面内跳转
this.replace("./new.html"); // 页面内替换
this.back(); // 后退
const formState = $("#myForm").formData();
formState.watch(() => console.log(formState.username));
formState.username = "Yao";
$.stanz({ count: 0 }); // 创建响应式数据
store.watch(callback); // 实时监听
store.watchTick(callback); // 下一帧监听
store.unwatch(wid); // 取消监听
el.extend({ say() { return "hello"; } });
$.fn.extend({ get good() { return "good"; }, say() { return 'hello'; } });
this.refresh(); // 刷新组件
ofa.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.