Odoo 19 前端 OWL 框架开发指南

并入教材:本指南全文已合并进 32-OWL框架.md(第三十二章,含教材体练习与截图占位)。印刷或统一维护请以该章为准;本文件可作独立副本保留。

版本: Odoo 19.0 | 日期: 2026 年 3 月


Odoo 19 的前端架构深度依赖于其自研的 OWL(Odoo Web Library) 框架。OWL 是一个现代化的声明式组件系统,其设计理念在很大程度上受到了 Vue 和 React 的启发。随着 Odoo 版本的演进,旧版的 Widget 系统正逐步被原生 JavaScript 类和 OWL 组件全面替代。Odoo 官方明确建议:所有新开发工作都应尽可能使用 OWL 完成 1

本指南将深入探讨 Odoo 19 前端开发中的五大核心主题:生命周期与 Hooks注册表系统(Registries)服务系统(Services)代码补丁机制(Patching) 以及 SCSS 继承体系,旨在为开发者提供全面、专业的技术参考。


第一章 生命周期与 Hooks

在 OWL 组件的开发中,生命周期钩子(Lifecycle Hooks)赋予了开发者在组件不同阶段执行特定逻辑的能力。通过合理使用这些钩子,可以精准控制组件的初始化、数据加载、DOM 操作以及资源清理 2

1.1 核心生命周期钩子

OWL 组件的生命周期由多个关键阶段组成,每个阶段都有对应的钩子函数供开发者调用。以下是 Odoo 19 中最常用的生命周期钩子及其应用场景 3

钩子函数 执行时机 典型应用场景与注意事项
setup() 组件实例化后立即执行 组件的基石。用于初始化状态、引入服务以及注册其他生命周期钩子。此时 DOM 尚未生成。继承时必须调用 super.setup()
onWillStart() 首次渲染前执行 支持异步操作,阻塞渲染直到完成。常用于 RPC 请求或加载配置。
onMounted() 组件挂载到 DOM 后执行 所有 HTML 元素均可访问。适用于初始化第三方库、设置焦点或绑定事件监听器。
onWillUpdateProps() 接收新 Props 前执行 允许在重新渲染前对属性变化做出响应,避免旧数据残留。
onPatched() 组件更新并重新渲染后执行 可用于重新计算元素尺寸或刷新依赖 DOM 的第三方插件。
onWillUnmount() 组件从 DOM 移除前执行 清除定时器、取消事件订阅或销毁外部库实例,防止内存泄漏。

完整生命周期示例:以下代码展示了一个综合使用多个生命周期钩子的 OWL 组件:

/** @odoo-module **/
import { Component, useState, onWillStart, onMounted, onWillUnmount } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";

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

    setup() {
        // 1. 初始化响应式状态
        this.state = useState({
            orders: [],
            isLoading: true,
        });

        // 2. 引入 ORM 服务
        this.orm = useService("orm");

        // 3. 注册生命周期钩子
        onWillStart(async () => {
            // 首次渲染前从后端加载数据
            const orders = await this.orm.searchRead(
                "sale.order",
                [["state", "=", "sale"]],
                ["name", "amount_total", "partner_id"]
            );
            this.state.orders = orders;
            this.state.isLoading = false;
        });

        onMounted(() => {
            // DOM 就绪后初始化图表库
            console.log("Dashboard mounted, DOM is ready.");
            this._initChart();
        });

        onWillUnmount(() => {
            // 组件销毁前清理资源
            if (this.chartInstance) {
                this.chartInstance.destroy();
            }
            console.log("Dashboard unmounted, resources cleaned.");
        });
    }

    _initChart() {
        // 初始化第三方图表(如 Chart.js)
        const canvas = document.querySelector(".sales-chart");
        if (canvas) {
            this.chartInstance = new Chart(canvas, { /* ... */ });
        }
    }
}

1.2 Odoo 专用 Hooks

除了标准的生命周期钩子,Odoo 框架还提供了一系列实用的自定义 Hooks,用于解决特定的业务需求 4

Hook 名称 所在模块路径 功能描述
useAssets @web/core/assets 按需懒加载静态资源,优化初始加载速度。
useAutofocus @web/core/utils/hooks 自动聚焦 t-ref="autofocus" 元素。
useBus @web/core/utils/hooks 简化事件总线订阅,组件卸载时自动清理。
usePager @web/search/pager_hook 渲染和管理视图控制面板的分页器。
usePosition @web/core/position_hook 将元素相对于目标进行定位,支持自动更新。
useSpellCheck @web/core/utils/hooks 为输入框激活拼写检查,失焦时移除样式。
useService @web/core/utils/hooks 在组件中引入注册表中定义的服务。

useBus 实战示例:监听全局事件总线上的自定义事件:

/** @odoo-module **/
import { Component } from "@odoo/owl";
import { useBus } from "@web/core/utils/hooks";

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

    setup() {
        // 订阅全局事件总线,组件卸载时自动取消
        useBus(this.env.bus, "CUSTOM_ORDER_CREATED", (event) => {
            console.log("New order created:", event.detail);
            this._refreshData();
        });
    }

    _refreshData() {
        // 刷新当前视图数据
    }
}

usePosition 实战示例:创建一个相对定位的弹出菜单:

/** @odoo-module **/
import { Component, useRef, xml } from "@odoo/owl";
import { usePosition } from "@web/core/position_hook";

export class DropdownMenu extends Component {
    static template = xml`
        <button t-ref="toggler" t-on-click="toggle">Options</button>
        <div t-ref="popper" t-if="state.isOpen" class="dropdown-content">
            <t t-slot="default"/>
        </div>
    `;

    setup() {
        const toggler = useRef("toggler");
        usePosition(
            () => toggler.el,
            {
                popper: "popper",
                position: "bottom-start",
                margin: 4,
                onPositioned: (el, { direction }) => {
                    el.classList.toggle("drop-up", direction === "top");
                },
            }
        );
    }
}

第二章 注册表系统 (Registries)

注册表(Registries)是 Odoo 前端框架扩展性的核心机制。它本质上是一个有序的键值对映射字典,用于存储特定类型的对象(如组件、服务、格式化函数等)。当框架需要某个定义时,会直接在相应的注册表中进行查找。正如官方文档所述:

Registries are (ordered) key/value maps. They are the main web client extension points: many features provided by the Odoo javascript framework simply look up into a registry whenever it needs a definition for some object. Customizing the web client is then simply done by adding specific values in the correct registry. 5

2.1 Registry API 基础

开发者可以通过 @web/core/registry 导入全局 registry 对象,然后使用 category() 方法访问不同的子注册表。以下是 Registry 类的核心 API:

方法 参数 说明
add(key, value, options) key: 字符串键名;value: 任意值;options.force: 是否覆盖;options.sequence: 排序权重 插入新条目。支持链式调用。触发 UPDATE 事件。
getAll() 返回按 sequence 升序排列的所有值列表。
get(key, defaultValue) key: 键名;defaultValue: 默认值 获取指定键的值,不存在时返回默认值或抛出异常。
contains(key) key: 键名 检查键是否存在,返回布尔值。
remove(key) key: 键名 移除指定键的条目。
category(name) name: 子注册表名称 获取或创建一个子注册表。

基本使用示例

import { registry } from "@web/core/registry";

// 获取不同类别的子注册表
const fieldRegistry = registry.category("fields");
const serviceRegistry = registry.category("services");
const viewRegistry = registry.category("views");

// 向字段注册表添加自定义字段组件
fieldRegistry.add("my_custom_widget", MyCustomFieldComponent);

// 使用 sequence 控制排序
registry.category("systray").add("my_addon.clock", {
    Component: ClockSystrayItem,
}, { sequence: 25 }); // 数字越小越靠右

2.2 核心内置注册表

Odoo 19 预定义了众多类别的注册表 5,以下是开发中最常接触的几个:

注册表类别 存储内容说明
main_components 顶层组件。WebClient 通过 MainComponentsContainer 全局渲染这些组件。
services 系统服务。Odoo 启动时自动实例化并激活其中的所有服务。
systray 系统托盘组件。显示在导航栏右上角,sequence 越小越靠右。
user_menuitems 用户菜单项。点击右上角用户名时的下拉菜单选项。
fields 字段组件。表单和列表视图中使用的各类字段控件。
views 视图组件。看板、列表、表单、日历等核心视图的定义。
effects 图形效果。如操作成功时的彩虹人动画。
formatters 值格式化函数。将字段值转换为可显示的字符串。
parsers 值解析函数。将用户输入的字符串解析为有效值。

注册 Systray 组件示例

/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";

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

    setup() {
        this.state = useState({ count: 0 });
        this.orm = useService("orm");
        this._loadCount();
    }

    async _loadCount() {
        this.state.count = await this.orm.searchCount(
            "project.task",
            [["user_ids", "in", [this.env.services.user.userId]]]
        );
    }
}

// 注册到 systray 注册表
registry.category("systray").add("my_module.TaskCounter", {
    Component: TaskCounterSystray,
}, { sequence: 30 });

注册用户菜单项示例

/** @odoo-module **/
import { registry } from "@web/core/registry";

registry.category("user_menuitems").add("my_module.settings", (env) => {
    return {
        description: env._t("My Custom Settings"),
        callback: () => {
            env.services.action.doAction({
                type: "ir.actions.act_window",
                res_model: "res.config.settings",
                views: [[false, "form"]],
            });
        },
        sequence: 45,
    };
});

第三章 服务系统 (Services)

服务(Services)是提供特定功能的长期运行的代码块,是 Odoo 前端依赖注入(DI)系统的基础。与组件不同,服务没有 UI 界面,主要负责处理后台逻辑、数据通信或全局状态管理。官方建议:

Most code that is not a component should be packaged in a service, in particular if it performs some side effect. This is very useful for testing purposes: tests can choose which services are active, so there are less chance for unwanted side effects interfering with the code being tested. 6

3.1 服务的定义与注册

一个标准的服务需要实现特定的接口规范,主要包含 dependencies(依赖声明)和 start(启动函数)两个部分 6

自定义服务完整示例:以下代码定义了一个用于管理全局计时器的服务:

/** @odoo-module **/
import { registry } from "@web/core/registry";

const timerService = {
    dependencies: ["notification"],

    start(env, { notification }) {
        let timers = {};
        let nextId = 1;

        function createTimer(name, intervalMs, callback) {
            const id = nextId++;
            timers[id] = {
                name,
                intervalId: setInterval(() => {
                    callback();
                    notification.add(`Timer "${name}" ticked.`, {
                        type: "info",
                    });
                }, intervalMs),
            };
            return id;
        }

        function clearTimer(id) {
            if (timers[id]) {
                clearInterval(timers[id].intervalId);
                delete timers[id];
            }
        }

        function clearAll() {
            for (const id of Object.keys(timers)) {
                clearTimer(id);
            }
        }

        return { createTimer, clearTimer, clearAll };
    },
};

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

在组件中使用自定义服务

/** @odoo-module **/
import { Component } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";

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

    setup() {
        this.timerService = useService("timerService");
    }

    startAutoRefresh() {
        this.timerId = this.timerService.createTimer(
            "auto-refresh",
            30000,
            () => this._refreshDashboard()
        );
    }

    stopAutoRefresh() {
        this.timerService.clearTimer(this.timerId);
    }
}

3.2 核心内置服务参考

Odoo 19 提供了丰富的内置服务 6,开发者可以通过 useService("service_name") 轻松调用:

服务名称 核心功能描述 典型用法
rpc 与 Odoo 后端进行远程过程调用 await rpc("/my/controller", { params })
orm ORM 操作的高级封装 await this.orm.call("res.partner", "read", [[1]])
notification 显示全局通知消息 notification.add("Success!", { type: "success" })
effect 显示图形动画效果 effect.add({ type: "rainbow_man", message: "Done!" })
action 执行 Odoo 动作(打开视图、向导等) action.doAction("module.action_xml_id")
cookie 读写浏览器 Cookies cookie.setCookie("key", "value", 3600)
router 管理浏览器 URL 状态 router.pushState({ action: 123 })
user 当前用户信息 user.userId, user.context
title 读写窗口标题 title.setParts({ action: "Sales" })
http 底层 HTTP 请求 await http.get("/api/data")

3.3 异步服务与组件生命周期

当服务提供异步 API 时,需要特别注意组件的生命周期问题。如果一个组件在异步调用完成之前被销毁,继续处理返回值可能导致错误。通过在服务定义中设置 async: trueasync: ["method1", "method2"],可以让框架自动处理这种情况——当调用方组件被销毁时,未完成的异步调用会被安全地忽略。


第四章 代码补丁机制 (Patching)

在进行 Odoo 二次开发时,经常需要修改或扩展核心模块提供的组件或类的行为。由于不应直接修改源码,Odoo 提供了强大的 patch 工具函数来实现原地代码补丁 7

4.1 Patch 函数 API

patch 函数位于 @web/core/utils/patch 模块中,其签名如下:

import { patch } from "@web/core/utils/patch";

const unpatch = patch(objToPatch, extension);
// 调用 unpatch() 可以撤销本次补丁
参数 类型 说明
objToPatch object 需要打补丁的目标对象或类的原型
extension object 包含需要覆盖或新增的属性/方法的对象
返回值 function 调用后撤销本次补丁的函数

4.2 补丁普通对象

对于简单的 JavaScript 对象,可以直接传入对象进行补丁。在补丁方法中使用 super 关键字可以调用原始方法 7

import { patch } from "@web/core/utils/patch";

const calculator = {
    value: 0,
    add(n) {
        this.value += n;
    },
};

patch(calculator, {
    add(n) {
        console.log(`Adding ${n} to ${this.value}`);
        super.add(n); // 调用原始的 add 方法
        console.log(`New value: ${this.value}`);
    },
});

calculator.add(5);
// 输出: Adding 5 to 0
// 输出: New value: 5

4.3 补丁 JavaScript 类

对于 ES6 类,需要区分静态方法实例方法。静态方法直接补丁类本身,实例方法则必须补丁类的 prototype 7

import { patch } from "@web/core/utils/patch";

class SaleOrderLine {
    static getDefaultValues() {
        return { quantity: 1, discount: 0 };
    }

    computeTotal() {
        return this.quantity * this.unitPrice;
    }
}

// 补丁静态方法
patch(SaleOrderLine, {
    getDefaultValues() {
        const defaults = super.getDefaultValues();
        return { ...defaults, discount: 10 }; // 默认折扣改为 10%
    },
});

// 补丁实例方法(注意:必须补丁 prototype)
patch(SaleOrderLine.prototype, {
    computeTotal() {
        const subtotal = super.computeTotal();
        return subtotal * (1 - this.discount / 100); // 加入折扣计算
    },
});

4.4 补丁 OWL 组件

补丁 OWL 组件是 Odoo 二次开发中最常见的场景。由于 JavaScript 原生机制决定了 constructor 无法被补丁,OWL 组件的最佳实践是将所有初始化逻辑放在 setup() 方法中 7

/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { FormController } from "@web/views/form/form_controller";
import { useService } from "@web/core/utils/hooks";

// 补丁 FormController 的 setup 方法
patch(FormController.prototype, {
    setup() {
        super.setup(...arguments); // 必须保留原始逻辑

        // 添加自定义的通知服务
        this.notification = useService("notification");

        // 扩展保存行为
        const originalOnSave = this.onRecordSaved?.bind(this);
        this.onRecordSaved = async (record) => {
            if (originalOnSave) {
                await originalOnSave(record);
            }
            this.notification.add("Record saved successfully!", {
                type: "success",
                sticky: false,
            });
        };
    },
});

4.5 补丁的高级技巧

使用工厂函数避免 extension 复用问题:由于 super 的绑定机制,同一个 extension 对象不能被多次用于不同的补丁操作 7。解决方案是使用工厂函数:

function createLoggingExtension(label) {
    return {
        process() {
            console.log(`[${label}] Before process`);
            super.process();
            console.log(`[${label}] After process`);
        },
    };
}

// 每次调用都生成新的 extension 对象
patch(serviceA, createLoggingExtension("ServiceA"));
patch(serviceB, createLoggingExtension("ServiceB"));

补丁 Getter/Setterpatch 同样支持对属性的 getter 和 setter 进行补丁:

patch(myObject, {
    get displayName() {
        return `[Custom] ${super.displayName}`;
    },
    set displayName(value) {
        super.displayName = value.trim();
    },
});

第五章 SCSS 继承与样式管理

Odoo 的前端资产(Assets)管理系统非常庞大,其 SCSS 继承体系旨在兼顾模块化、主题定制(如暗黑模式)以及社区版与企业版的差异化设计 8

5.1 资产类型与 Bundles

Odoo 19 的前端资产分为三种类型 9代码.js 文件)、样式.css.scss 文件)和模板.xml 文件)。这些资产通过 Bundles(资产包)进行组织管理,在模块的 __manifest__.py 中声明。

Bundle 名称 用途说明
web.assets_common 通用资产,被 Web Client、网站和 POS 共享。包含底层框架代码。
web.assets_backend Web Client(后端管理界面)专用的代码和样式。
web.assets_frontend 公共网站专用的资产,包括电商、门户、论坛等。
web.assets_unit_tests JavaScript 单元测试代码。

__manifest__.py 中声明资产

{
    'name': 'My Custom Module',
    'assets': {
        'web.assets_backend': [
            # 追加文件(默认操作)
            'my_module/static/src/js/**/*',
            'my_module/static/src/scss/**/*',
            'my_module/static/src/xml/**/*',

            # 在指定文件之前插入
            ('before', 'web/static/src/scss/primary_variables.scss',
             'my_module/static/src/scss/my_variables.scss'),

            # 替换文件
            ('replace', 'web/static/src/views/list/list_view.js',
             'my_module/static/src/views/list/custom_list_view.js'),

            # 移除文件
            ('remove', 'web/static/src/legacy/some_old_widget.js'),

            # 包含子 Bundle
            ('include', 'my_module._assets_shared'),
        ],
    },
}

5.2 !default 指令的妙用

在复杂的 SCSS 编译环境中,直接重写变量往往会导致不可预期的级联错误。Odoo 强烈推荐使用 SCSS 的 !default 指令 8

当一个变量使用 !default 声明时,编译器只会在该变量尚未被定义的情况下为其赋值。由于 Odoo 严格控制了资产包的加载顺序,这使得上层模块可以优先定义变量,从而优雅地覆盖底层库的默认设置。

正确的变量覆盖方式

// my_module/static/src/scss/primary_variables.scss
// 此文件通过 (before, ...) 指令在 Bootstrap 变量之前加载

$o-brand-primary: #7C4DFF !default;  // 自定义品牌主色
$o-navbar-height: 52px !default;      // 自定义导航栏高度
$border-radius: 0.5rem !default;      // 覆盖 Bootstrap 默认圆角
// 错误示例 —— 不要这样做!
$o-brand-primary: #7C4DFF;  // 缺少 !default,可能导致级联错误

5.3 SCSS 编译顺序与架构

理解 Odoo 的 SCSS 编译顺序对于定制 UI 至关重要 8。系统按照以下层级自上而下编译:

↓ [编译开始]
│
↓ web.dark_mode_variables
│   ├─ 基础色彩变量
│   └─ 组件变量
│
↓ web._assets_primary_variables
│   ├─ 企业版主题变量
│   ├─ 企业版组件变量
│   ├─ 社区版主题变量
│   └─ 社区版组件变量
│
↓ web._assets_bootstrap
│   └─ Bootstrap 框架(吸收上层自定义变量)
│
↓ web.assets_backend / web.assets_frontend
│   ├─ CSS 变量定义
│   └─ CSS 变量上下文适配
│
● [屏幕上的视觉结果]

5.4 实战:自定义主题模块

以下是一个完整的自定义主题模块示例,展示如何正确利用 SCSS 继承体系:

__manifest__.py

{
    'name': 'Custom Theme',
    'depends': ['web'],
    'assets': {
        'web._assets_primary_variables': [
            ('before', 'web/static/src/scss/primary_variables.scss',
             'custom_theme/static/src/scss/primary_variables.scss'),
        ],
        'web.assets_backend': [
            'custom_theme/static/src/scss/backend_overrides.scss',
        ],
    },
}

primary_variables.scss

// 品牌色彩体系
$o-brand-primary: #1B5E20 !default;
$o-brand-secondary: #4CAF50 !default;

// 布局参数
$o-navbar-height: 48px !default;
$o-horizontal-padding: 24px !default;

// 字体
$o-font-family-sans-serif: 'Inter', 'Noto Sans SC', sans-serif !default;

backend_overrides.scss

// 利用已定义的变量进行组件级样式定制
.o_main_navbar {
    background: linear-gradient(135deg, $o-brand-primary, darken($o-brand-primary, 10%));
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

.o_form_view {
    .o_form_sheet {
        border-radius: 12px;
        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
    }
}

附录 A:WebClient 架构概览

Odoo 19 的 WebClient 是一个 OWL 单页应用(SPA),其核心模板结构如下 1

<t t-name="web.WebClient">
    <body class="o_web_client">
        <NavBar/>
        <ActionContainer/>
        <MainComponentsContainer/>
    </body>
</t>

其中 ActionContainer 负责渲染当前活动的动作控制器(如表单视图或看板视图),MainComponentsContainer 则渲染所有注册在 main_components 注册表中的全局组件。

Environment 对象:每个 OWL 组件都可以通过 this.env 访问共享的环境对象 1

键名 值说明
bus 全局事件总线,用于协调跨组件通信
services 所有已部署的服务实例
debug 调试模式标识(非空字符串表示已启用)
_t 国际化翻译函数
isSmall 是否处于移动端模式(屏幕宽度 ≤ 767px)

附录 B:常见开发模式速查

开发需求 推荐方案
添加全局顶层组件 注册到 main_components 注册表
添加系统托盘图标 注册到 systray 注册表
自定义字段控件 注册到 fields 注册表
修改已有组件行为 使用 patch 补丁其 prototype
添加后台全局逻辑 定义并注册自定义 Service
覆盖 Bootstrap 变量 _assets_primary_variables 中用 !default 声明
添加自定义 CSS 追加到 web.assets_backend
监听全局事件 使用 useBus(this.env.bus, ...)
懒加载大型资源 使用 useAssets Hook

参考文献