← 返回 Skills 市场
zhaobod1

Huo15 Owl Dev

作者 Job Zhao · GitHub ↗ · v1.0.0 · MIT-0
cross-platform ✓ 安全检测通过
91
总下载
0
收藏
0
当前安装
1
版本数
在 OpenClaw 中安装
/install huo15-owl-dev
功能描述
OWL (Odoo Web Library) 开发技能 — Odoo 前端组件开发框架,基于 QWeb 模板、响应式状态管理、OWL 组件生命周期
使用说明 (SKILL.md)

SKILL.md — huo15-owl-dev

触发条件

用户提到以下内容时激活本技能:

  • OWL 组件开发
  • Odoo 前端 / Web 开发
  • QWeb 模板
  • OWL 组件生命周期
  • Odoo JS/Web 组件
  • odoo.define / owl.component
  • Odoo 15+ 前端架构

OWL 核心概念

什么是 OWL?

OWL(Odoo Web Library)是 Odoo 15+ 引入的新一代前端框架,用于替代旧的 web.old(backbone-based)架构。它基于:

  • QWeb 模板(XML)
  • 响应式状态管理(PoP/Props)
  • TypeScript(原生支持)

OWL vs 旧架构

特性 OWL(新) web.old(旧)
模板 QWeb XML QWeb XML
状态 useState / useRefs 不存在
组件 类 + 装饰器 Widget 基类
生命周期 PoP (Props on Props) 不存在
TypeScript 原生支持 不支持
响应式 自动追踪依赖 手动绑定
性能 虚拟DOM + 懒加载 完整重渲染

OWL 组件结构

最小组件模板

/** @odoo-module **/

import { Component, useState, useEnv } from "@odoo/owl";

export class MyComponent extends Component {
    static template = "my_module.MyComponent";

    // 响应式状态
    state = useState({
        value: 0,
        items: [],
        loading: false,
    });

    // 组件挂载时调用
    onWillMount() {
        this.loadData();
    }

    async loadData() {
        this.state.loading = true;
        try {
            const result = await this.rpc("/my_module/data");
            this.state.items = result;
        } finally {
            this.state.loading = false;
        }
    }

    increment() {
        this.state.value++;
    }

    onClick() {
        this.trigger("custom-event", { payload: this.state.value });
    }
}

目录结构

my_module/
├── static/
│   └── src/
│       ├── js/
│       │   ├── components/
│       │   │   ├── my_component/
│       │   │   │   ├── my_component.xml    # QWeb 模板
│       │   │   │   └── my_component.ts     # 组件逻辑
│       │   │   └── other_component/
│       │   │       ├── other_component.xml
│       │   │       └── other_component.ts
│       │   └── pages/
│       │       └── my_page.xml
│       └── css/
│           └── my_component.scss
├── views/
│   └── templates.xml                        # Web 入口
└── __manifest__.py

组件注册与挂载

__manifest__.py 配置

{
    'name': '我的模块',
    'depends': ['web'],
    'assets': {
        'web.assets_backend': [
            ('include', 'my_module.static.src.js.components.my_component.my_component_xml'),
            'my_module/static/src/js/components/my_component/my_component.ts',
            'my_module/static/src/css/my_component.scss',
        ],
    },
}

XML 入口(views/templates.xml)

\x3C?xml version="1.0" encoding="utf-8"?>
\x3Codoo>
    \x3Ctemplate id="assets_backend" name="my_module assets" inherit_id="web.assets_backend">
        \x3Cxpath expr="." position="inside">
            \x3Clink rel="stylesheet" href="/my_module/static/src/css/my_component.scss"/>
            \x3Cscript type="module" src="/my_module/static/src/js/components/my_component/my_component.es.js"/>
        \x3C/xpath>
    \x3C/template>
\x3C/odoo>

注册到 OwlComponentStore

// my_component.ts
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";

export class MyComponent extends Component {
    static template = "my_module.MyComponent";
}

registry.category("components").add("my_component", MyComponent);

在另一个组件中使用

\x3C-- parent.xml -->
\x3CMyComponent t-component="'my_component'" />

Props(属性传递)

定义 Props 接口

import { Component } from "@odoo/owl";
import { Props } from "@odoo/owl";

interface MyProps {
    title: string;
    count: number;
    onUpdate: (value: number) => void;
}

export class MyComponent extends Component\x3CProps & MyProps> {
    static template = "my_module.MyComponent";

    static props = {
        title: { type: String, optional: true },
        count: { type: Number },
        onUpdate: { type: Function },
    };

    updateCount(newCount: number) {
        this.props.onUpdate(newCount);
    }
}

Props 类型

// 简单类型
static props = {
    name: String,           // string
    active: Boolean,        // boolean
    count: Number,          // number
    items: Array,           // any[]
}

// 可选
static props = {
    title: { type: String, optional: true },
}

// 对象类型
static props = {
    config: { type: Object },
}

// 函数(回调)
static props = {
    onChange: Function,
    onSubmit: { type: Function, optional: true },
}

// 数组对象
static props = {
    items: {
        type: Array,
        validate: (items) => items.every(i => typeof i === 'object'),
    },
}

状态管理(State)

useState

import { useState } from "@odoo/owl";

export class MyComponent extends Component {
    state = useState({
        // 原始类型
        count: 0,
        name: "",
        active: false,
        
        // 嵌套对象
        user: {
            name: "张三",
            age: 30,
        },
        
        // 数组
        items: [],
        
        // 特殊值
        loading: false,
        error: null,
    });
}

状态更新

// 直接赋值(OWL 自动追踪)
this.state.count = 5;
this.state.user.name = "李四";
this.state.items.push({ id: 1, name: "item" });

// 数组操作
this.state.items = [...this.state.items, newItem];  // 替换触发更新
this.state.items.filter(i => i.id !== deleteId);    // 删除

// 批量更新(函数式)
this.state = {
    ...this.state,
    count: this.state.count + 1,
    loading: false,
};

生命周期(Lifecycle)

完整生命周期钩子

export class MyComponent extends Component {
    // ===== 创建阶段 =====
    
    /** 组件即将挂载(异步) */
    async onWillMount() {
        // 可返回 Promise
    }
    
    /** 组件即将渲染 */
    onWillRender() {}
    
    // ===== 挂载阶段 =====
    
    /** 组件 DOM 已挂载 */
    onMounted() {
        // 可添加 DOM 事件监听
        const el = this.el;
    }
    
    /** 组件渲染完成 */
    onRendered() {}
    
    // ===== 更新阶段 =====
    
    /** Props 或 State 即将更新 */
    onWillUpdateProps(nextProps) {
        // 准备数据
    }
    
    /** 组件即将重新渲染 */
    onWillPatch() {}
    
    /** 组件 DOM patch 完成 */
    onPatched() {
        // DOM 更新后执行
    }
    
    // ===== 卸载阶段 =====
    
    /** 组件即将卸载 */
    onWillUnmount() {
        // 清理定时器/事件监听
    }
    
    /** 组件已卸载 */
    onUnmounted() {}
}

生命周期图

组件创建 → onWillMount → 渲染 → onMounted
                    ↓
props/state 变化 → onWillUpdateProps → onWillPatch → patch → onPatched
                    ↓
组件卸载 → onWillUnmount → onUnmounted

QWeb 模板

基础语法

\x3Ctemplates>
    \x3Ct t-name="my_module.MyComponent">
        \x3Cdiv class="my-component">
            \x3Ch1 t-esc="props.title"/>
            
            \x3C!-- 条件渲染 -->
            \x3Cspan t-if="state.loading">加载中...\x3C/span>
            
            \x3C!-- 循环 -->
            \x3Cul>
                \x3Cli t-foreach="state.items" t-as="item" t-key="item.id">
                    \x3Cspan t-esc="item.name"/>
                \x3C/li>
            \x3C/ul>
            
            \x3C!-- 事件绑定 -->
            \x3Cbutton t-on-click="increment">点击\x3C/button>
            \x3Cbutton t-on-click.self="doSomething">仅自身\x3C/button>
            
            \x3C!-- 子组件 -->
            \x3CSubComponent t-component="'sub_component'" t-props="subProps"/>
        \x3C/div>
    \x3C/t>
\x3C/templates>

常用指令

指令 用法 说明
t-esc \x3Cspan t-esc="value"/> 转义文本
t-raw \x3Cspan t-raw="html"/> 原始 HTML
t-if \x3Cdiv t-if="state.show"/> 条件渲染
t-else \x3Cdiv t-elif="state.show2"/> 否则
t-foreach \x3Cli t-foreach="arr" t-as="i"/> 循环
t-key t-key="item.id" 循环 key
t-on-* t-on-click="handler" 事件绑定
t-model t-model="state.inputValue" 双向绑定
t-ref \x3Cdiv t-ref="myDiv"/> DOM 引用
t-set \x3Ct t-set="val" t-value="123"/> 设置变量
t-call \x3Ct t-call="other_template"/> 引用模板
t-translation t-translation="on" 翻译标记

事件绑定

\x3C!-- 基础 -->
\x3Cbutton t-on-click="onClick">点击\x3C/button>

\x3C!-- 阻止冒泡 -->
\x3Cbutton t-on-click.stop="onClick">阻止冒泡\x3C/button>

\x3C!-- 阻止默认 -->
\x3Ca href="#" t-on-click.prevent="onLink">阻止默认\x3C/a>

\x3C!-- 修饰符组合 -->
\x3Cbutton t-on-click.stop.prevent="onClick">组合\x3C/button>

\x3C!-- 传参数 -->
\x3Cbutton t-on-click="(ev) => onClick(42, ev)">传参\x3C/button>
\x3Cbutton t-on-click="() => onClick(42)">箭头函数\x3C/button>

\x3C!-- 事件对象 -->
\x3Cbutton t-on-click="onClick">访问事件: ev.target\x3C/button>

双向绑定(t-model)

\x3C!-- 输入框 -->
\x3Cinput type="text" t-model="state.inputValue"/>

\x3C!-- 多行文本 -->
\x3Ctextarea t-model="state.description"/>

\x3C!-- 复选框 -->
\x3Cinput type="checkbox" t-model="state.checked"/>

\x3C!-- 下拉选择 -->
\x3Cselect t-model="state.selectedId">
    \x3Coption value="">请选择\x3C/option>
    \x3Coption t-foreach="state.options" 
            t-as="opt" 
            t-att-value="opt.id"
            t-esc="opt.name"/>
\x3C/select>

Hooks(钩子)

内置 Hooks

import { 
    useState,        // 响应式状态
    useRef,          // DOM 引用
    useEffect,       // 副作用
    useEnv,          // 环境(env)
    useDispatch,     // dispatch (action-service)
    useSelectors,    // selectors
} from "@odoo/owl";

useRef(DOM 引用)

import { useRef } from "@odoo/owl";

export class MyComponent extends Component {
    myInput = useRef("myInput");  // 声明 ref
    
    onMounted() {
        // 访问 DOM
        this.myInput.el.focus();
    }
    
    getValue() {
        return this.myInput.el.value;
    }
}

// 模板中使用
\x3Cinput t-ref="myInput" type="text"/>

useEffect(副作用)

import { useEffect } from "@odoo/owl";

export class MyComponent extends Component {
    state = useState({ data: null });
    
    setup() {
        // 每次渲染后执行
        useEffect(
            () => console.log("count changed:", this.state.count),
            () => [this.state.count]  // 依赖数组
        );
        
        // 清理函数
        useEffect(
            () => {
                const timer = setInterval(() => this.tick(), 1000);
                return () => clearInterval(timer);  // 返回清理函数
            },
            () => []
        );
    }
}

useEnv(获取环境)

import { useEnv } from "@odoo/owl";

export class MyComponent extends Component {
    setup() {
        const env = useEnv();
        
        // 访问 Odoo env
        const user = env.session.user;
        const db = env.session.db;
    }
}

Service(服务)

使用 ActionService

import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";

export class MyComponent extends Component {
    setup() {
        this.actionService = useService("action");
        this.ormService = useService("orm");
    }
    
    async doAction() {
        // 触发窗口动作
        await this.actionService.doAction("my_module.action_wizard");
    }
    
    async searchData() {
        const result = await this.ormService.call(
            "my.model",      // 模型
            "search_read",   // 方法
            [[['active', '=', true]], ['name', 'id']],  // domain, fields
            { context: { ... } }
        );
        return result;
    }
}

常用服务

// Action 服务
this.actionService.doAction({
    type: "ir.actions.act_window",
    res_model: "my.model",
    view_mode: "form",
    views: [[false, "form"]],
    target: "new",  // new/current/main
});

// 通知服务
useService("notification").notify("标题", "内容", "success");

// 对话框服务
useService("dialog").add(ConfirmDialog, {
    title: "确认删除",
    body: "确定要删除吗?",
    confirmLabel: "删除",
    cancelLabel: "取消",
    confirmCallback: () => this.doDelete(),
});

// RPC 服务
const result = await useService("rpc").call("/my_route", params);

自定义 Service

// my_service.js
import { registry } from "@web/core/registry";

export const myService = {
    start(env) {
        return {
            async getData(id) {
                return await env.services.orm.call(
                    "my.model", "read", [id]
                );
            },
        };
    },
};

registry.category("services").add("myService", myService);

路由(Router)

配置路由

// __manifest__.py
'assets': {
    'web.assets_backend': [
        'my_module/static/src/js/routes.js',
    ],
},

定义路由

// routes.js
import { registry } from "@web/core/registry";
import { route } from "@web/core/router/route";

registry.category("main").add("my_module.dashboard", 
    route("/my_module/dashboard", MyComponent, {
        load: async (env) => {
            // 加载数据
            return await env.services.orm.call("my.model", "get_data", []);
        },
    })
);

国际化(i18n)

模板中使用翻译

\x3Cspan>这是中文文本\x3C/span>
\x3Cspan t-esc="env._t('Hello World')"/>

JS 中使用翻译

import { useEnv } from "@odoo/owl";

export class MyComponent extends Component {
    setup() {
        const env = useEnv();
        
        const msg = env._t("Hello");
        const formatted = env._t("You have %s items", [count]);
    }
}

样式(SCSS/CSS)

组件样式

// static/src/css/my_component.scss

.my-component {
    padding: 16px;
    background: #fff;
    border-radius: 4px;
    
    &__header {
        font-size: 18px;
        font-weight: 600;
    }
    
    &__list {
        list-style: none;
        
        &-item {
            padding: 8px;
            
            &--active {
                background: #f0f0f0;
            }
        }
    }
    
    &--loading {
        opacity: 0.6;
        pointer-events: none;
    }
}

CSS 变量

.o_my_component {
    --my-color: #{$primary};
    --my-spacing: 16px;
    
    padding: var(--my-spacing);
    color: var(--my-color);
}

测试(Testing)

组件测试

// my_component.test.js
import { describe, expect, test } from "@odoo/owl";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { MyComponent } from "./my_component";

describe("MyComponent", () => {
    test("renders correctly", async () => {
        const env = await makeTestEnv();
        const wrapper = new MyComponent(null, { env });
        
        await wrapper.mount(fixture);
        
        expect(wrapper.el.textContent).toContain("标题");
    });
    
    test("increments counter on click", async () => {
        const env = await makeTestEnv();
        const comp = new MyComponent(null, { env });
        comp.state.count = 0;
        
        await comp.mount(fixture);
        await comp.onClick();
        
        expect(comp.state.count).toBe(1);
    });
});

常用测试工具

import { makeTestEnv, makeFakeCookieService } from "@web/../tests/helpers/mock_env";
import { dom } from "@web/../tests/helpers/dom";

// 模拟 RPC
await mockServer.mockModel("my.model", {
    search_read: () => [{ id: 1, name: "Test" }],
});

调试技巧

开发工具

  1. 浏览器扩展:安装 Odoo Web Developer 浏览器插件
  2. 日志console.log 替换为 console.debug 减少干扰
  3. QWeb 错误:Odoo 会显示模板解析错误位置

常见错误

错误 原因 解决
Template not found __manifest__.py 未注册 assets 检查 assets_backend 配置
Component not rendering t-component 名称错误 确认 registry.category("components").add() 名称
State not updating 直接修改对象而非赋值 this.state.items.push() 不触发更新,需替换
Event not working t-on-click 语法错误 检查是否少了括号 t-on-click="handler"
Props undefined 子组件 Props 未定义 检查 static props 定义

热重载

# Odoo 开发者模式已启用热重载
# 修改 .ts/.xml 文件后,刷新浏览器即可
# 如需完全重载:Ctrl+Shift+R (强制刷新)

最佳实践

代码组织

  • 一个组件一个目录components/my_comp/
  • 模板文件名匹配my_comp.xml + my_comp.ts
  • 组件名 PascalCaseMyComponent
  • 方法名 camelCaseonClick, getData

性能优化

// ✅ 使用 useState 细粒度更新
state = useState({ count: 0, items: [] });

// ✅ 避免在 render 中创建函数
class MyComponent extends Component {
    onClick = (id) => {
        // 使用箭头函数避免重复创建
    };
}

// ✅ 使用 t-key 帮助 OWL 高效更新
\x3Cli t-foreach="items" t-as="item" t-key="item.id">

安全建议

  • 用户输入转义:使用 t-esc 而非 t-raw
  • SQL 注入:永远通过 ORM 操作数据
  • XSS:避免使用 t-raw 处理用户输入

快速参考

常用导入

import { Component, useState, useRef, useEffect, useEnv, useService } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { ormService } from "@web/core/orm_service";
import { actionService } from "@web/core/action_service";

组件装饰器

// 可选:类组件装饰器
@registerComponent("my_component")
export class MyComponent extends Component {
    // ...
}

Props 验证

static props = {
    // 必需
    title: String,
    count: Number,
    
    // 可选
    subtitle: { type: String, optional: true },
    
    // 回调
    onChange: Function,
    
    // 复杂类型
    config: { type: Object },
    items: Array,
}

本技能基于 OWL v2.x(Odoo 16+),适配辉火云企业套件前端开发环境。

安全使用建议
Do not rely on this as a complete approval: the sandbox prevented direct artifact inspection, and the only available signal was clean VirusTotal telemetry.
能力评估
Purpose & Capability
Unable to inspect metadata.json or artifact contents in this run, so no purpose/capability mismatch was confirmed.
Instruction Scope
Unable to review SKILL.md instructions; no evidence-backed scope concern is available from the supplied telemetry alone.
Install Mechanism
Unable to inspect install specs or manifests; VirusTotal telemetry was clean and is not negative evidence.
Credentials
Unable to evaluate requested environment access from artifacts; no concrete overbreadth evidence was available.
Persistence & Privilege
Unable to evaluate persistence or privilege behavior from artifacts; no concrete persistence concern was confirmed.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install huo15-owl-dev
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /huo15-owl-dev 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v1.0.0
Initial release of OWL (Odoo Web Library) front-end development skill. - Covers OWL core concepts, lifecycle, and state management patterns for Odoo 15+ - Explains component structure, registration, and usage with QWeb templates - Includes practical code samples for props, state, and event handling - Provides typical directory and manifest configurations for Odoo modules - Offers a concise reference for QWeb template syntax and OWL best practices
元数据
Slug huo15-owl-dev
版本 1.0.0
许可证 MIT-0
累计安装 0
当前安装数 0
历史版本数 1
常见问题

Huo15 Owl Dev 是什么?

OWL (Odoo Web Library) 开发技能 — Odoo 前端组件开发框架,基于 QWeb 模板、响应式状态管理、OWL 组件生命周期. 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 91 次。

如何安装 Huo15 Owl Dev?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install huo15-owl-dev」即可一键安装,无需额外配置。

Huo15 Owl Dev 是免费的吗?

是的,Huo15 Owl Dev 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。

Huo15 Owl Dev 支持哪些平台?

Huo15 Owl Dev 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。

谁开发了 Huo15 Owl Dev?

由 Job Zhao(@zhaobod1)开发并维护,当前版本 v1.0.0。

💬 留言讨论