构建 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(全屏模式)。对于移动应用,currentfullscreen 是最常用的选项。

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 在页面间传递数据:contextparams

当一个操作被执行时,其 contextparams 字典会被传递给新创建的组件,并通过 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 组件使用带 paramsdoAction 来启动 DetailPageDetailPageonWillStart 生命周期钩子中从其 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 模块是自定义路由器实现的绝佳范例。其架构可以解构如下:

  1. 整个自助点餐应用通过单个 ir.actions.client 启动。
  2. 在这个主组件内部,一个 Router 组件监听 router 服务的变化。
  3. 它使用 URL 哈希(例如 #/products, #/cart, #/payment)来条件性地渲染不同的子组件(ProductScreen, CartScreen 等)。
  4. 这创造了一种多页面的体验,却从未离开初始的客户端操作,从而实现了非常快速和流畅的导航。

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 标准视图集成到何种程度?
  • 应用内屏幕切换的性能有多重要?

根据答案,可以推荐以下三种模式之一:

  1. 简单操作模式:使用多个 ir.actions.client 记录,并通过 doAction 在它们之间导航。最适合简单的 UI 或需要与 Odoo 面包屑系统深度集成的情况。
  2. 混合 SPA 模式(移动端推荐):使用单个 ir.actions.client 作为外壳,通过 router 服务管理内部导航。仅在需要导航到应用外部时才使用 doAction。这是实现高性能和流畅移动体验的最佳选择。
  3. 状态驱动模式(无路由器):对于一个组件内非常简单的多步骤向导,完全可以不使用 router 服务,仅通过 useState 来控制模板的哪个部分可见。

5.2 Odoo 移动导航最佳实践

  • 始终使用 useService:避免直接访问全局管理器。
  • 实现自定义“返回”按钮:将其功能绑定到 history_back 操作。不要依赖设备的本机返回按钮。
  • 集中化数据获取:在 onWillStart 钩子中根据导航时传递的 props 来加载数据。
  • 谨慎管理状态:对于混合 SPA 模式下跨多个屏幕的共享状态,可以考虑创建一个自定义服务或从主容器组件通过 props 向下传递状态。
  • 提供用户反馈:在数据获取或操作执行期间,使用 notification 服务或加载指示器(ui 服务)来改善用户体验。

5.3 导航机制对比分析

下表总结了本文中讨论的各种导航机制的权衡,为架构决策提供了一个高密度的、可扫描的参考。

表 1: Odoo 18 导航机制对比

特性

action.doAction (正向导航)

history_back / action.restore (后退导航)

router.navigate (SPA 导航)

主要用例

在 Odoo 主要应用/视图之间导航

返回到堆栈中的前一个视图

在单个客户端操作内进行应用内导航

机制

执行服务器定义的操作记录

从内部 ActionManager 堆栈中弹出一个操作

操作浏览器 URL 哈希并触发客户端重渲染

URL 影响

通常改变主 URL 路径 (如 /web#action=...)

将 URL 恢复到上一个操作的状态

仅改变哈希片段 (如 /web#...&hash=settings)

历史管理

推入 Odoo ActionManager 堆栈

从 Odoo ActionManager 堆栈中弹出

推入浏览器的 History API (在哈希内管理)

数据传递

通过操作定义中的 contextparams,在 props 中可用

不适用(恢复先前的状态)

通过哈希参数或共享的组件状态/服务

性能

较慢(涉及服务器通信和完整的组件销毁/重挂载)

中等(客户端操作,但仍是完整的重挂载)

最快(纯客户端操作,通常仅重渲染子组件)

使用时机

导航到不同的顶层功能或 Odoo 标准视图时

用于所有面向用户的“返回”按钮

在单个操作内构建流畅的多屏幕移动应用时

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值