构建 Odoo 18 移动端导航:深入解析 OWL 框架、操作与服务
第一节:架构基础: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
访问。这是数据传递的主要渠道。
params
vs.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 标准视图时 | 用于所有面向用户的“返回”按钮 | 在单个操作内构建流畅的多屏幕移动应用时 |