第一节:架构基础:Odoo 的操作驱动导航模型
在 Odoo 18 中使用 Odoo Web 库 (OWL) 构建移动端或任何复杂的前端应用时,理解其核心导航范式至关重要。与许多传统单页应用 (SPA) 框架不同,Odoo 的导航并非主要由客户端的 URL 路由驱动,而是由一个在服务器端定义的、名为“操作 (Action)”的系统来精心编排。对于习惯了纯前端路由的开发者而言,这是一个根本性的思维转变。本节将深入剖析 Odoo 的操作体系,并将 ir.actions.client 模型定位为所有定制化 OWL 应用的基石与入口。
1.1 解构 Odoo 操作体系
Odoo 的用户界面本质上是通过“操作”来协调的。这些操作是存储在数据库中的记录,它们定义了系统应如何响应用户的交互(例如点击菜单项)。这是一种以服务器为中心的模式,由后端决定前端可用的导航路径和行为。为了更好地理解其上下文,有必要对比几种核心操作类型:
- 窗口操作 (
ir.actions.act_window): 这是最经典的操作类型,用于显示特定模型的视图(如列表、表单、看板视图)。 - 服务操作 (
ir.actions.server): 用于在服务器端执行预定义的 Python 代码。 - URL 操作 (
ir.actions.act_url): 用于在新标签页或当前窗口打开内部或外部的 URL。 - 客户端操作 (
ir.actions.client): 这是本文的焦点。此操作类型将控制权完全委托给一个客户端(即 JavaScript)组件。它是启动定制 OWL 应用的官方指定机制。
1.2 客户端操作 (ir.actions.client):通往 OWL 应用的门户
客户端操作是在 ir.actions.client 模型中定义的一条记录,它扮演着连接 Odoo 后端(如菜单项点击事件)与特定 JavaScript 组件的桥梁角色。
关键字段:
name: 操作的人类可读名称,将显示在 UI 上。tag: 这是最关键的字段。它是一个唯一的字符串标识符,用于将 XML 中定义的操作记录与在客户端actions注册表中注册的 JavaScript 组件关联起来。params: 一个可选的字典,用于向客户端组件传递静态或动态数据。这是初始化页面时传递数据的主要方式之一。target: 定义操作的显示方式。常见的值包括current(在主内容区打开,替换当前视图)、new(在对话框或弹窗中打开)和fullscreen(全屏模式)。对于移动应用,current或fullscreen是最常用的选项。
1.3 实现演练:创建您的第一个可导航“页面”
本小节将通过一个完整的步骤指南,演示如何创建一个可启动的基础 OWL 组件,我们将其视为移动应用中的一个“页面”。
步骤 1:在 XML 中定义客户端操作
首先,需要创建一个 ir.actions.client 记录,并通常会创建一个 menuitem 来触发它,完成后端配置。
代码示例:
<record id="action_my_mobile_app_main" model="ir.actions.client">
<field name="name">My Mobile App</field>
<field name="tag">my_mobile_app.MainScreen</field>
</record>
<menuitem id="menu_my_mobile_app_root"
name="My Mobile App"
action="action_my_mobile_app_main"
sequence="10"/>
步骤 2:创建 OWL 组件
接下来,为应用的主屏幕定义 JavaScript 类。
代码示例:
/** @odoo-module **/
import { Component } from "@odoo/owl";
export class MainScreen extends Component {
static template = "my_mobile_app.MainScreenTemplate";
// 组件逻辑将在此处实现
}
步骤 3:定义组件的模板
使用 QWeb 模板为组件提供 HTML 结构。
代码示例:
<templates xml:space="preserve">
<t t-name="my_mobile_app.MainScreenTemplate" owl="1">
<div class="o_my_mobile_app">
<h1>Welcome to My Mobile App</h1>
</div>
</t>
</templates>
步骤 4:将组件注册为操作
这是将 XML 中的 tag 与 JavaScript 组件连接起来的关键一步。
代码示例:
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Component } from "@odoo/owl";
export class MainScreen extends Component {
static template = "my_mobile_app.MainScreenTemplate";
}
registry.category("actions").add("my_mobile_app.MainScreen", MainScreen);
步骤 5:定义资源文件
最后,确保 Odoo 的资源管理系统能够加载这些新建的 JS 和 XML 文件。
代码示例:
# 位于 __manifest__.py
'assets': {
'web.assets_backend': [
'my_module/static/src/components/**/*.js',
'my_module/static/src/xml/**/*.xml',
],
},
此流程的底层逻辑可以被理解为一种“命令模式”的实现。当用户点击菜单项时,该交互与一个操作 ID 相关联。服务器随后将此操作的定义(一个字典)返回给客户端。客户端的 ActionManager(操作管理器)接收此字典,并检查其 type 字段。如果类型是 ir.actions.client,它会使用 tag 字段作为关键索引,在 actions 注册表 (registry.category("actions")) 中查找对应的 OWL 组件类。找到后,ActionManager 会实例化该组件并将其挂载到 UI 中。这个过程清晰地将请求(菜单点击)与执行(渲染 OWL 组件)解耦,为 Odoo 提供了巨大的灵活性和可扩展性。因此,为了让移动应用与Odoo 的其余部分无缝集成,开发者应在顶层导航(如从菜单打开应用)中拥抱这种操作驱动的模型,而不是试图从一开始就强行引入纯客户端的路由解决方案。
第二节:编程方式的正向导航:掌握 action 服务
本节将从静态的入口点转向动态的应用内导航。我们将引入 action 服务,它是 OWL 组件中用于触发导航到另一个页面、视图或操作的主要工具。我们将探讨如何使用它,以及在这些转换过程中如何传递数据。
2.1 Odoo JavaScript 服务简介
服务 (Services) 是在整个客户端应用中可用的单例对象,它们提供了核心功能,如显示通知、发起 RPC 调用,或者在本例中,执行操作。使用服务的标准、惯用方式是在 OWL 组件的 setup 方法中使用 useService 钩子。
代码示例:
import { useService } from "@web/core/utils/hooks";
//... 在一个 OWL 组件类内部
setup() {
this.actionService = useService("action");
this.notificationService = useService("notification");
}
2.2 doAction 方法:导航的主力
actionService.doAction() 是从客户端以编程方式触发任何 Odoo 操作的方法。它可以执行窗口操作、服务器操作或其他客户端操作。
doAction 方法可以接受一个操作的 xml_id(字符串)或一个操作的字典定义(对象),后者在构建动态导航时更为灵活。
用例 1:导航到标准的 Odoo 视图
此示例演示了如何打开特定记录的表单视图。
代码示例:
// 在一个 OWL 组件的方法中
async openPartnerForm(partnerId) {
await this.actionService.doAction({
type: 'ir.actions.act_window',
res_model: 'res.partner',
res_id: partnerId,
views: [[false, 'form']],
target: 'current', // 'current' 会替换主视图内容
context: { /* 如果需要,可以添加额外的上下文 */ },
});
}
用例 2:导航到另一个 OWL 组件(页面)
这是移动应用导航的核心。通过触发另一个 ir.actions.client 来实现页面跳转。
代码示例:
// 在一个 OWL 组件的方法中,导航到 "DetailPage" 组件
async openDetailPage(itemId) {
await this.actionService.doAction({
type: 'ir.actions.client',
tag: 'my_mobile_app.DetailPage',
params: {
record_id: itemId,
some_other_info: 'hello'
}
});
}
2.3 在页面间传递数据:context 与 params
当一个操作被执行时,其 context 和 params 字典会被传递给新创建的组件,并通过 this.props 访问。这是数据传递的主要渠道。
paramsvs.context:params: 通常用于在客户端操作之间传递客户端特定的信息。context: 是一个标准的 Odoo 概念。它会在 RPC 调用中被传递到服务器,并能影响服务器端的逻辑(例如,设置默认值、过滤域等)。
在目标组件中接收数据:
来自操作的 params 和 context 会被合并到组件的 props 对象中。
代码示例 (在 DetailPage.js 中):
import { onWillStart, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
//...
export class DetailPage extends Component {
static template = "my_mobile_app.DetailPageTemplate";
setup() {
this.state = useState({ recordData: null });
this.orm = useService("orm");
onWillStart(async () => {
// 通过 this.props 访问传递的参数
const recordId = this.props.params?.record_id;
if (recordId) {
// 使用该 ID 从服务器获取数据
const data = await this.orm.read("my.model", [recordId], ["name", "description"]);
this.state.recordData = data;
}
});
}
}
这个例子展示了一个完整的“导航并获取数据”的周期。MainScreen 组件使用带 params 的 doAction 来启动 DetailPage。DetailPage 在 onWillStart 生命周期钩子中从其 props 中访问传递的 record_id,并在初次渲染前使用 orm 服务获取相关数据。
在 Odoo 的发展过程中,action 服务是 ActionManager 的一个现代抽象层。它为 OWL 开发提供了一个简洁的、基于钩子的 API (useService),隐藏了旧版本 Odoo 中基于事件的、更复杂的通信机制。这种设计遵循了现代框架(如 React 的钩子和 Angular 的依赖注入)的模式,使得 Odoo 前端开发对新开发者来说更易于维护和理解。因此,开发者在 OWL 组件中应始终优先使用
useService("action") 进行导航,这是现代、受支持且符合惯例的方法。
第三节:实现后退导航:操作堆栈与 restore
本节将解决用户查询中的“返回”部分。Odoo 维护着自己的导航堆栈,并提供了一个特定的机制——action.restore()——来实现后退功能。与依赖浏览器原生历史记录相比,这是一种更为健壮的方案。
3.1 ActionManager 的内部堆栈
Odoo 的 ActionManager 不仅仅是执行操作,它还维护着一个操作堆栈。当使用 doAction 时,一个新的操作通常会被推入这个堆栈,代表了用户在应用内的导航历史。Odoo UI 顶部的面包屑导航就是这个操作堆栈的一个可视化表现。操作的 target 属性可以影响堆栈行为,例如,target: 'main' 可能会重置堆栈,从而清空面包屑。
3.2 用于编程“后退”的 action.restore() 方法
action.restore() 方法是以编程方式从堆栈中弹出当前操作并“恢复”前一个操作的方式,其功能等同于一个“后退”按钮。
3.3 内置的 history_back 客户端操作
Odoo 框架提供了一个预定义的、标签为 history_back 的客户端操作。这个操作专门用于处理后退导航。触发此操作是实现后退按钮最可靠且面向未来的方式。它封装了对 action.restore() 的调用,确保了即使 restore 的底层实现在未来的 Odoo 版本中发生变化,history_back 操作的行为仍将保持稳定。
实现方式:
可以在移动 UI 中创建一个“返回”按钮,并将其 t-on-click 事件绑定到一个调用该操作的方法上。
模板代码:
<button class="btn btn-secondary" t-on-click="goBack">返回</button>
JavaScript 代码:
// 在 setup() 中: this.actionService = useService("action");
async goBack() {
// 只需执行 'history_back' 客户端操作即可
await this.actionService.doAction({ type: 'ir.actions.client', tag: 'history_back' });
}
3.4 陷阱:浏览器返回按钮 vs. action.restore()
依赖浏览器的原生返回按钮 (window.history.back()) 是不可靠的,并且可能导致错误。一个 GitHub 问题描述了一个场景:在一个未完成的表单上使用浏览器的返回按钮会导致整个 UI 卡死。这是因为浏览器的历史记录与 Odoo 的内部状态(例如,必填字段的验证状态)变得不同步。
Odoo 的操作堆栈是导航状态的“唯一真实来源”。action.restore()(通过 history_back 操作)正确地与这个真实来源交互,允许 Odoo 妥善管理组件的生命周期和状态转换。将浏览器历史记录与应用的内部操作堆栈分离是 Odoo 的一个刻意架构选择,旨在创建比典型网站更健壮、状态感知更强的用户体验。当 action.restore() 被调用时,它不仅仅是“返回”,而是一个受控的、对当前操作的销毁过程,以及一个对前一个操作的受控的重新挂载过程。因此,对于基于 Odoo 构建的移动应用,开发者必须提供自己的 UI 控件(例如,页眉中的 < 返回 按钮)并将其连接到 history_back 操作。不应假设用户会或能够使用设备的本机返回功能,因为它可能未与 Odoo 的操作堆栈集成。
第四节:使用 router 服务实现高级 SPA 路由
本节将探讨一种更高级的导航模式,用于在单个客户端操作内部创建流畅的单页应用 (SPA) 体验。这对于包含许多内部屏幕的复杂移动应用是理想选择,因为在这种场景下,为每次屏幕切换都触发一个完整的 Odoo 操作会显得缓慢和笨重。
4.1 router 服务简介
router 服务提供了一个较低级别的接口,用于与浏览器的 URL 哈希 (#) 和历史 API (pushState) 进行交互。
关键方法:
router.current: 一个包含当前哈希信息(路径、参数)的对象。router.navigate(hash): 向浏览器历史记录中推送一个新状态并更新 URL 哈希。router.redirect(hash): 替换浏览器历史记录中的当前状态。
访问服务:
// 在 setup() 中
this.router = useService("router");
4.2 案例研究:pos_self_order 应用
Odoo 自身的 pos_self_order 模块是自定义路由器实现的绝佳范例。其架构可以解构如下:
- 整个自助点餐应用通过单个
ir.actions.client启动。 - 在这个主组件内部,一个
Router组件监听router服务的变化。 - 它使用 URL 哈希(例如
#/products,#/cart,#/payment)来条件性地渲染不同的子组件(ProductScreen,CartScreen等)。 - 这创造了一种多页面的体验,却从未离开初始的客户端操作,从而实现了非常快速和流畅的导航。
4.3 构建一个简单的自定义路由器
本小节提供了一个实用指南,用于实现 pos_self_order 模式的简化版本。
步骤 1:主应用组件(外壳)
此组件将包含路由逻辑。
代码示例:
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { HomeScreen } from "./home_screen";
import { SettingsScreen } from "./settings_screen";
class MobileAppContainer extends Component {
static template = "my_mobile_app.AppContainer";
static components = { HomeScreen, SettingsScreen };
setup() {
this.router = useService("router");
// 要显示的组件派生自 URL 哈希
this.state = useState({
get currentScreen() {
// 如果哈希为空或未知,则默认为主页
switch (this.router.current.hash.path) {
case "settings": return "SettingsScreen";
default: return "HomeScreen";
}
}
});
}
}
registry.category("actions").add("my_mobile_app.Container", MobileAppContainer);
步骤 2:带有条件渲染的模板
使用 t-if 或 t-component 在不同屏幕之间切换。
代码示例:
<t t-name="my_mobile_app.AppContainer" owl="1">
<div class="mobile-app-container">
<t t-if="state.currentScreen === 'HomeScreen'">
<HomeScreen />
</t>
<t t-if="state.currentScreen === 'SettingsScreen'">
<SettingsScreen />
</t>
</div>
</t>
步骤 3:触发导航
子组件使用 router.navigate 来改变屏幕。
代码示例 (在 HomeScreen.js 中):
// 在 setup() 中: this.router = useService("router");
goToSettings() {
this.router.navigate({ path: "settings" });
}
action 服务和 router 服务并非互斥;它们在不同的抽象层次上运作,可以结合使用以创建复杂的应用程序。action 服务用于粗粒度的、应用级别的导航(例如,打开销售应用、打开库存应用、打开我的移动应用),它管理 Odoo 的主操作堆栈和面包屑。而 router 服务用于细粒度的、应用内部的导航(例如,在我的移动应用内,从主列表转到设置屏幕),它管理浏览器的 URL 哈希和本地组件状态。
对于复杂的移动应用,最佳架构是一种混合模式:使用单个 ir.actions.client 作为入口点(“外壳”)。在该外壳内部,使用 router 服务和条件渲染来管理应用的内部屏幕。如果应用需要导航到 Odoo 的一个完全不同的部分(例如,打开一个标准的产品表单),它仍然可以使用 action 服务的 doAction 方法。因此,开发者应根据导航的范围选择合适的工具。
第五节:综合与架构建议
本节将前述概念综合成一套清晰的建议和最佳实践,提供一个决策框架,帮助开发者根据具体需求选择正确的导航策略。
5.1 决策框架:选择您的导航策略
为指导开发者做出选择,可以考虑以下问题:
- 您的功能是一个孤立的单屏幕,还是一个多屏幕的应用?
- 您的导航是否需要反映在 URL 中以便于收藏或分享?
- 您的应用需要与 Odoo 标准视图集成到何种程度?
- 应用内屏幕切换的性能有多重要?
根据答案,可以推荐以下三种模式之一:
- 简单操作模式:使用多个
ir.actions.client记录,并通过doAction在它们之间导航。最适合简单的 UI 或需要与 Odoo 面包屑系统深度集成的情况。 - 混合 SPA 模式(移动端推荐):使用单个
ir.actions.client作为外壳,通过router服务管理内部导航。仅在需要导航到应用外部时才使用doAction。这是实现高性能和流畅移动体验的最佳选择。 - 状态驱动模式(无路由器):对于一个组件内非常简单的多步骤向导,完全可以不使用
router服务,仅通过useState来控制模板的哪个部分可见。
5.2 Odoo 移动导航最佳实践
- 始终使用
useService:避免直接访问全局管理器。 - 实现自定义“返回”按钮:将其功能绑定到
history_back操作。不要依赖设备的本机返回按钮。 - 集中化数据获取:在
onWillStart钩子中根据导航时传递的props来加载数据。 - 谨慎管理状态:对于混合 SPA 模式下跨多个屏幕的共享状态,可以考虑创建一个自定义服务或从主容器组件通过
props向下传递状态。 - 提供用户反馈:在数据获取或操作执行期间,使用
notification服务或加载指示器(ui服务)来改善用户体验。
5.3 导航机制对比分析
下表总结了本文中讨论的各种导航机制的权衡,为架构决策提供了一个高密度的、可扫描的参考。
表 1: Odoo 18 导航机制对比
|
特性 |
|
|
|
|
主要用例 |
在 Odoo 主要应用/视图之间导航 |
返回到堆栈中的前一个视图 |
在单个客户端操作内进行应用内导航 |
|
机制 |
执行服务器定义的操作记录 |
从内部 ActionManager 堆栈中弹出一个操作 |
操作浏览器 URL 哈希并触发客户端重渲染 |
|
URL 影响 |
通常改变主 URL 路径 (如 |
将 URL 恢复到上一个操作的状态 |
仅改变哈希片段 (如 |
|
历史管理 |
推入 Odoo ActionManager 堆栈 |
从 Odoo ActionManager 堆栈中弹出 |
推入浏览器的 |
|
数据传递 |
通过操作定义中的 |
不适用(恢复先前的状态) |
通过哈希参数或共享的组件状态/服务 |
|
性能 |
较慢(涉及服务器通信和完整的组件销毁/重挂载) |
中等(客户端操作,但仍是完整的重挂载) |
最快(纯客户端操作,通常仅重渲染子组件) |
|
使用时机 |
导航到不同的顶层功能或 Odoo 标准视图时 |
用于所有面向用户的“返回”按钮 |
在单个操作内构建流畅的多屏幕移动应用时 |
3115

被折叠的 条评论
为什么被折叠?



