Coding

Huo15 Owl Dev

Try it

OWL (Odoo Web Library) 开发技能 — Odoo 前端组件开发框架,基于 QWeb 模板、响应式状态管理、OWL 组件生命周期

What it does

用户提到以下内容时激活本技能: OWL 组件开发 Odoo 前端 / Web 开发 QWeb 模板 OWL 组件生命周期 Odoo JS/Web 组件 / Odoo 15+ 前端架构

The skill document

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 XMLQWeb 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)

<?xml version="1.0" encoding="utf-8"?>

    
        
            
            
        
    

注册到 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);

在另一个组件中使用

<-- parent.xml -->


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 {
    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 模板

基础语法


    
        
            
            
            <!-- 条件渲染 -->
            加载中...
            
            <!-- 循环 -->
            
                
                    
                
            
            
            <!-- 事件绑定 -->
            点击
            仅自身
            
            <!-- 子组件 -->
            
        
    

常用指令

指令用法说明
t-esc``转义文本
t-raw``原始 HTML
t-if``条件渲染
t-else``否则
t-foreach``循环
t-keyt-key="item.id"循环 key
t-on-*t-on-click="handler"事件绑定
t-modelt-model="state.inputValue"双向绑定
t-ref``DOM 引用
t-set``设置变量
t-call``引用模板
t-translationt-translation="on"翻译标记

事件绑定

<!-- 基础 -->
点击

<!-- 阻止冒泡 -->
阻止冒泡

<!-- 阻止默认 -->
阻止默认

<!-- 修饰符组合 -->
组合

<!-- 传参数 -->
 onClick(42, ev)">传参
 onClick(42)">箭头函数

<!-- 事件对象 -->
访问事件: ev.target

双向绑定(t-model)

<!-- 输入框 -->


<!-- 多行文本 -->


<!-- 复选框 -->


<!-- 下拉选择 -->

    请选择
    


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;
    }
}

// 模板中使用

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)

模板中使用翻译

这是中文文本

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 renderingt-component 名称错误确认 registry.category("components").add() 名称
State not updating直接修改对象而非赋值this.state.items.push() 不触发更新,需替换
Event not workingt-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 高效更新

安全建议

  • 用户输入转义:使用 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+),适配辉火云企业套件前端开发环境。

Related skills

Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.

by nssa.io1.0k installs47 stars

Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.

by johnpatternai21 installs8 stars

Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.

by Iván555 installs18 stars

Read and write Excel workbooks, worksheets, ranges, tables, and charts in OneDrive through Microsoft Graph with managed OAuth.

by byungkyu800 installs42 stars

Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.

by Iván854 installs69 stars

More from zhaobod1

Browse all skills

Generate enterprise Word and native PDF documents across 39 format presets, with 7 contract subtypes.

by zhaobod149 installs

Bootstrap a fresh OpenClaw workspace with a guided 4-step setup that writes five identity and preference files.

by zhaobod130 installs

Adds Claude Code-style agent experience to OpenClaw without modifying the host.

by zhaobod144 installs

麻省理工学院48小时学习法技能(青岛火一五信息科技有限公司)。完整还原 Ihtesham Ali 原始三问框架 + 反馈循环 + 完整 48h 三阶段时间线,叠加网上最佳实践(synthesis / contradictions / gaps / Feynman teach-back / weakness ana...

by zhaobod133 installs2 stars

通过火山方舟Ark API调用Seedance 2.0生成第一人称带货短视频,v2 新增剧本驱动的配音(edge-tts/火山TTS)、背景音乐自动混音、字幕烧录,内置 8 套人设模板(传统女、时尚主播、老中医、厨房主妇、美妆博主、健身教练、户外探店、数码博主)。触发词:生成视频、带货视频、产品视频、拍视频、剧本...

by zhaobod126 installs1 stars

规范 + 时尚的思维导图生成。输入 Markdown 大纲 / JSON / OPML / XMind,输出 XMind 2021+ (.xmind)、OPML、FreeMind (.mm)、Markdown、PNG、PDF、SVG;内置 13 种风格(modern / classic / dark / xiao...

by zhaobod136 installs