核心理念: 將您熟悉的Odoo后端MVC+ORM架構思想,完整映射到前端OWL組件化開發中,讓您在熟悉的概念體系下,快速掌握新的技術棧。
第一部分:核心概念映射與環境搭建
- 內容摘要: 本部分旨在建立后端與前端最核心的概念對應關系,為您后續的學習建立一個穩固的思維模型。我們將把Odoo后端的MVC架構與OWL的組件結構進行直接類比,并完成開發環境的準備工作。
- 后端類比:
- 模型 (Model): 對應 組件的狀態 (State),負責存儲和管理數據。
- 視圖 (View - XML): 對應 OWL模板 (Template - XML),負責界面的聲明式渲染。
- 控制器 (Controller): 對應 組件類 (Component Class - JS),負責處理業務邏輯和用戶交互。
- 學習要點:
1. OWL、Odoo Web框架與后端的關系圖解
在Odoo的架構中,后端(Python)和前端(JavaScript)通過一個明確的RPC(遠程過程調用)邊界進行通信。
- 后端 (Odoo Server): 負責處理業務邏輯、數據庫操作(通過ORM)、權限控制,并通過HTTP Endpoints暴露API。
- 前端 (Web Client): 運行在瀏覽器中,負責UI渲染和用戶交互。OWL (Odoo Web Library) 是Odoo自研的、現代化的前端UI框架,用于構建Web客戶端的界面。
您可以將整個Odoo Web客戶端視為一個大型的單頁面應用(SPA),而OWL組件就是構成這個應用的積木。當一個OWL組件需要數據或執行一個業務操作時,它會通過RPC服務調用后端的控制器方法或模型方法。
2. 開發環境配置
一個高效的OWL開發環境對于提升生產力至關重要。以下是推薦的配置,旨在實現快速迭代和調試。
Odoo服務配置 (odoo.conf
)
為了在開發過程中獲得即時反饋,特別是在修改前端資源(XML, JS, CSS)時,推薦在odoo.conf
文件或啟動命令中加入--dev=all
參數。
--dev=xml
: 這個參數允許Odoo在檢測到XML文件(包括QWeb模板)變化時,無需重啟服務即可重新加載視圖。這對于調整UI布局非常有用。--dev=all
: 這是一個更全面的開發模式,它包含了--dev=xml
的功能,并可能對其他資源(如JS、CSS)提供熱重載或禁用緩存的支持,使得前端開發體驗更加流暢。
同時,激活開發者模式對于前端調試至關重要。您可以通過在URL后附加?debug=assets
來進入開發者模式。這會禁用前端資源的合并與壓縮(minification),讓您在瀏覽器開發者工具中看到原始的、未壓縮的JS和CSS文件,極大地簡化了調試過程。
Docker與Docker Compose
使用Docker是現代Odoo開發的首選方式,它提供了環境一致性、隔離性和可復現性。
docker-compose.yml
:- 服務定義: 通常包含一個
db
服務(PostgreSQL)和一個odoo_web
服務。 - 卷掛載 (Volumes): 這是實現代碼熱重載的關鍵。您需要將本地存放自定義模塊的文件夾(例如
./addons
)掛載到容器內的Odoo addons路徑。這樣,您在本地對代碼的任何修改都會立即反映在容器內。 - 端口映射 (Ports): 將容器的Odoo端口(如8069)映射到本地主機,以便通過瀏覽器訪問。
- 配置文件: 將本地的
odoo.conf
文件掛載到容器中,以便集中管理配置。
- 服務定義: 通常包含一個
一個典型的docker-compose.yml
配置片段如下:
services:odoo_web:image: odoo:17.0 # Or your target versiondepends_on:- dbports:- "8069:8069"volumes:- ./addons:/mnt/extra-addons # Mount your custom addons- ./odoo.conf:/etc/odoo/odoo.conf # Mount your config filecommand: --dev=all # Enable dev modedb:image: postgres:15environment:- POSTGRES_DB=postgres- POSTGRES_PASSWORD=odoo- POSTGRES_USER=odoo
瀏覽器開發者工具
- 常規工具: 熟練使用Chrome DevTools或Firefox Developer Tools是必須的。
Elements
面板用于檢查DOM結構,Console
用于查看日志和執行代碼,Network
用于監控RPC請求,Sources
用于調試JavaScript。 - OWL DevTools插件: Odoo官方提供了一個名為"Odoo OWL Devtools"的Chrome瀏覽器擴展。強烈建議安裝此插件。它為開發者工具增加了一個"OWL"標簽頁,允許您:
- 檢查組件樹: 以層級結構查看頁面上所有渲染的OWL組件。
- 審查組件狀態和屬性: 選中一個組件,可以實時查看其
state
、props
和env
,這對于理解數據流和調試狀態變化至關重要。 - 性能分析: 幫助識別渲染瓶頸。
- 常規工具: 熟練使用Chrome DevTools或Firefox Developer Tools是必須的。
VSCode調試配置
您可以直接在VSCode中為OWL組件的JavaScript代碼設置斷點。這需要配置launch.json
文件以附加調試器到瀏覽器進程。
- 在VSCode中打開您的項目文件夾。
- 進入“運行和調試”側邊欄,創建一個
launch.json
文件。 - 選擇"Chrome: Launch"配置模板。
- 修改配置如下:
{"version": "0.2.0","configurations": [{"type": "chrome","request": "launch","name": "Launch Chrome against localhost","url": "http://localhost:8069/web?debug=assets", // Odoo URL with debug mode"webRoot": "${workspaceFolder}", // Your project's root directory"sourceMaps": true, // Enable source maps if you use them"sourceMapPathOverrides": {"/odoo/addons/*": "${workspaceFolder}/addons/*" // Map server paths to local paths}}]
}
url
: 確保指向您的Odoo實例,并包含?debug=assets
。webRoot
: 指向包含您前端代碼的本地工作區根目錄。sourceMapPathOverrides
: 如果Odoo服務器上的路徑與本地路徑不完全匹配,這個配置非常關鍵,它能幫助調試器正確找到源文件。
配置完成后,啟動您的Odoo服務,然后在VSCode中啟動這個調試配置。VSCode會打開一個新的Chrome窗口。現在,您可以在您的.js
文件中設置斷點,當代碼執行到斷點時,VSCode會暫停執行,讓您能夠檢查變量、調用棧等。
第二部分:“視圖”的演進 - 從QWeb到OWL模板
- 內容摘要: 您對后端的XML視圖定義已經非常熟悉。本部分將以此為基礎,深入講解OWL模板的語法和功能。它本質上是您所了解的QWeb的超集,但為響應式前端賦予了新的能力。
- 后端類比: 后端視圖中的
<field>
,<button>
,t-if
,t-foreach
等指令。 - 學習要點:
OWL模板使用與后端相同的QWeb語法,但它在瀏覽器中實時編譯和渲染,并且與組件的響應式狀態緊密集成。
1. 基礎語法
這些基礎指令與您在后端使用的QWeb完全相同。
t-name
: 定義模板的唯一名稱,例如t-name="my_module.MyComponentTemplate"
。t-esc
: 輸出變量的值并進行HTML轉義,防止XSS攻擊。對應于組件類中的this.state.myValue
或props.myValue
。t-raw
: 輸出變量的原始HTML內容,不進行轉義。請謹慎使用,確保內容來源可靠。t-set
: 在模板作用域內定義一個變量,例如t-set="fullName" t-value="record.firstName + ' ' + record.lastName"
。
2. 控制流指令
這些指令的用法與后端QWeb幾乎一致,但它們現在是根據組件的state
或props
來動態決定渲染內容。
t-if
,t-else
,t-elif
: 根據條件的真假來渲染不同的DOM塊。
<t t-if="state.isLoading"><div>Loading...</div>
</t>
<t t-elif="state.error"><div class="error"><t t-esc="state.error"/></div>
</t>
<t t-else=""><!-- Render content -->
</t>
t-foreach
: 遍歷一個數組或對象,并為每一項渲染一個DOM塊。t-as
: 為循環中的每一項指定一個別名。t-key
: 這是OWL中至關重要的一個屬性。它為列表中的每一項提供一個唯一的、穩定的標識符。OWL使用key
來識別哪些項發生了變化、被添加或被刪除,從而高效地更新DOM,而不是重新渲染整個列表。這類似于React中的key
屬性。在t-foreach
中始終提供一個唯一的t-key
是一個最佳實踐。
<ul><t t-foreach="state.partners" t-as="partner" t-key="partner.id"><li><t t-esc="partner.name"/></li></t>
</ul>
3. 屬性綁定
這是OWL模板相對于后端QWeb的一大增強,用于動態地改變HTML元素的屬性。
- 動態屬性 (
t-att-
): 根據表達式的值來設置一個HTML屬性。
- 動態屬性 (
<!-- 如果 state.imageUrl 存在,則渲染 src="value_of_state_imageUrl" -->
<img t-att-src="state.imageUrl"/>
- 動態屬性格式化 (
t-attf-
): 用于構建包含靜態文本和動態表達式的屬性值。
- 動態屬性格式化 (
<!-- 渲染 id="partner_row_123" -->
<div t-attf-id="partner_row_{{partner.id}}">...</div>
- 動態類名 (
t-class-
): 根據條件的真假來動態添加或移除CSS類。
- 動態類名 (
<!-- 如果 partner.is_active 為真,則添加 'active' 類 -->
<!-- 如果 partner.is_vip 為真,則添加 'vip-customer' 類 -->
<div t-attf-class="base-class {{ partner.is_active ? 'active' : '' }}" t-class-vip-customer="partner.is_vip">...
</div>
這非常適合根據記錄狀態動態改變樣式,例如將已取消的訂單顯示為灰色。
4. 組件插槽 (Slots)
插槽是OWL中實現組件組合和UI靈活性的核心機制。它允許父組件向子組件的預定義位置“填充”內容。
- 后端類比: 您可以將其類比為后端視圖繼承中,通過
<xpath expr="..." position="inside">
向父視圖的某個元素內部添加內容。插槽提供了一種更結構化、更清晰的前端等價物。
- 后端類比: 您可以將其類比為后端視圖繼承中,通過
基本用法
- 子組件 (e.g.,
Card.xml
): 使用<t t-slot="slot_name"/>
定義一個或多個占位符。有一個默認的插槽名為default
。
- 子組件 (e.g.,
<!-- Card.xml -->
<div class="card"><div class="card-header"><t t-slot="header">Default Header</t> <!-- 命名插槽 --></div><div class="card-body"><t t-slot="default"/> <!-- 默認插槽 --></div>
</div>
- 父組件 (e.g.,
Parent.xml
): 在使用子組件時,通過<t t-set-slot="slot_name">
來提供要填充的內容。
- 父組件 (e.g.,
<!-- Parent.xml -->
<Card><t t-set-slot="header"><h3>My Custom Header</h3></t><!-- 默認插槽的內容可以直接放在組件標簽內 --><p>This is the body content for the card.</p>
</Card>
作用域插槽 (Scoped Slots)
這是插槽最高級的用法,它顛覆了單向數據流(父->子),實現了子組件向父組件插槽內容的反向數據傳遞。
- 后端類比: 這沒有直接的后端類比,但可以想象成一個
One2many
字段的行內視圖,該視圖不僅顯示數據,還允許您自定義每一行的操作按鈕,并且這些按鈕能感知到當前行的數據上下文。 - 工作原理: 子組件在定義插槽時,可以傳遞一個上下文對象。父組件在填充插槽時,可以通過
t-slot-scope
來接收這個對象,并在其模板內容中使用。 - 子組件 (e.g.,
CustomList.js/.xml
): 子組件定義插槽,并傳遞數據。
- 后端類比: 這沒有直接的后端類比,但可以想象成一個
// CustomList.js
// ...
this.state = useState({items: [{ id: 1, name: "Item A", active: true },{ id: 2, name: "Item B", active: false },]
});
// ...
<!-- CustomList.xml -->
<ul><t t-foreach="state.items" t-as="item" t-key="item.id"><li><!-- 為每個item渲染插槽,并傳遞item對象和索引 --><t t-slot="itemRenderer" item="item" index="item_index"/></li></t>
</ul>
- 父組件 (e.g.,
Parent.xml
): 父組件使用t-slot-scope
來接收子組件傳遞的數據,并自定義渲染邏輯。
- 父組件 (e.g.,
<!-- Parent.xml -->
<CustomList><t t-set-slot="itemRenderer" t-slot-scope="scope"><!-- 'scope' 現在是一個對象,包含了子組件傳遞的 item 和 index --><!-- scope = { item: { id: ..., name: ... }, index: ... } --><span t-att-class="scope.item.active ? 'text-success' : 'text-danger'"><t t-esc="scope.index + 1"/>. <t t-esc="scope.item.name"/></span><button class="btn btn-sm">Edit <t t-esc="scope.item.name"/></button></t>
</CustomList>
通過作用域插槽,CustomList
組件只負責數據管理和循環邏輯,而將每一項的具體渲染方式完全交由父組件決定。這使得CustomList
成為一個高度可復用的“無頭(headless)”組件,極大地增強了UI的靈活性和組合能力。這在Odoo核心應用中,如Dropdown
或SelectMenu
組件中被廣泛使用,以允許開發者自定義菜單項的顯示。
第三部分:“控制器”的實現 - 組件類與生命周期
- 內容摘要: 后端控制器處理HTTP請求并執行業務邏輯。在OWL中,組件的JavaScript類扮演了這個角色,它驅動著模板的渲染和響應用戶的操作。
- 后端類比:
http.Controller
類中的路由方法 (@http.route
) 和業務邏輯處理。 - 學習要點:
1. 組件定義
一個標準的OWL組件是一個繼承自odoo.owl.Component
的JavaScript類。
/** @odoo-module **/import { Component, useState } from "@odoo/owl";export class MyComponent extends Component {static template = "my_module.MyComponentTemplate"; // 關聯QWeb模板setup() {// 這是組件的入口點,用于初始化狀態、方法和生命周期鉤子this.state = useState({ counter: 0 });// 在這里綁定方法this.incrementCounter = this.incrementCounter.bind(this);}incrementCounter() {this.state.counter++;}
}
static template
: 靜態屬性,指定了該組件渲染時使用的QWeb模板的名稱。setup()
: 組件的構造函數。所有狀態初始化 (useState
)、方法綁定和生命周期鉤子注冊都必須在這里完成。
2. 事件處理
這直接對應后端XML視圖中的<button name="action_method" type="object">
。在OWL中,我們在模板中使用t-on-*
指令來聲明事件監聽,并在組件類中定義處理方法。
- 模板 (XML):
<button t-on-click="incrementCounter">Click Me!</button>
<span>Counter: <t t-esc="state.counter"/></span>
- 組件類 (JS):
// ... (在 MyComponent 類中)
incrementCounter() {// 這個方法在按鈕被點擊時調用this.state.counter++;// 當 state 改變時,OWL會自動重新渲染模板,更新界面上的數字
}
OWL支持所有標準的DOM事件,如click
, keydown
, submit
, input
等。
3. 生命周期鉤子 (Lifecycle Hooks)
生命周期鉤子是OWL框架在組件生命周期的特定時間點自動調用的函數。它們讓您有機會在關鍵時刻執行代碼,例如獲取數據、操作DOM或清理資源。
- 后端類比:
onWillStart
: 類比于模型的_init
或_register_hook
,在組件“啟動”前執行異步準備工作。onMounted
: 類比于一個動作(Action)被執行后,界面完全加載完成的時刻。onWillUnmount
: 類比于Python對象的垃圾回收(__del__
),用于在對象銷毀前釋放資源。
- 后端類比:
完整的生命周期鉤子及其執行順序:
setup()
: 組件實例化的第一步,用于設置一切。onWillStart()
: 異步鉤子。在組件首次渲染之前執行。這是執行異步操作(如RPC數據請求)的最佳位置,因為它可以確保數據在模板首次渲染時就已準備就緒。可以返回一個Promise
,OWL會等待它完成后再繼續。onWillRender()
: 每次組件即將渲染或重新渲染時調用。onRendered()
: 每次組件渲染或重新渲染完成后調用。onMounted()
: 在組件首次渲染并掛載到DOM之后執行。這是執行需要DOM元素存在的操作(如初始化第三方JS庫、手動添加復雜的事件監聽器)的最佳位置。onWillUpdateProps()
: 異步鉤子。當父組件傳遞新的props
時,在組件重新渲染之前調用。onWillPatch()
: 在DOM更新(patching)開始前調用。onPatched()
: 在DOM更新完成后調用。onWillUnmount()
: 在組件從DOM中移除之前調用。這是進行資源清理的關鍵位置,例如移除在onMounted
中添加的事件監聽器、清除setInterval
定時器等,以防止內存泄漏。onWillDestroy()
: 在組件實例被徹底銷毀前調用。無論組件是否掛載,都會執行。onError()
: 捕獲組件或其子組件在渲染或生命周期鉤子中發生的錯誤。
父子組件鉤子調用順序:
- 掛載 (Mounting):
onWillStart
: 父 -> 子onMounted
: 子 -> 父
- 更新 (Updating):
onWillUpdateProps
: 父 -> 子onPatched
: 子 -> 父
- 卸載 (Unmounting):
onWillUnmount
: 父 -> 子onWillDestroy
: 子 -> 父
- 掛載 (Mounting):
實戰示例:
import { Component, useState, onWillStart, onMounted, onWillUnmount } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";export class DataFetcher extends Component {static template = "my_module.DataFetcherTemplate";setup() {this.state = useState({ data: null, timer: 0 });this.orm = useService("orm"); // 獲取ORM服務onWillStart(async () => {// 在渲染前異步獲取初始數據const records = await this.orm.searchRead("res.partner", [], ["name"], { limit: 5 });this.state.data = records;});onMounted(() => {// 掛載后,啟動一個定時器this.interval = setInterval(() => {this.state.timer++;}, 1000);console.log("Component is mounted and timer started.");});onWillUnmount(() => {// 卸載前,必須清理定時器,防止內存泄漏clearInterval(this.interval);console.log("Component will unmount and timer cleared.");});}
}
第四部分:“模型”的再現 - 狀態、屬性與響應式
- 內容摘要: 后端模型 (
models.Model
) 定義了數據的結構和默認值。在OWL中,組件的state
承擔了此角色,并且是“響應式”的——當state
改變時,UI會自動更新。 - 后端類比:
models.Model
中的字段定義 (fields.Char
,fields.Many2one
) 和ORM記錄集 (self
)。 - 學習要點:
1. 狀態 (State) 與響應式
狀態 (state
) 是組件內部的數據存儲。它是可變的,并且是“響應式”的。
- 創建: 狀態必須通過
useState
鉤子在setup()
方法中創建。useState
接收一個對象或數組作為初始值。 - 響應式原理:
useState
的背后是JavaScript的Proxy
對象。它會返回一個代理對象,這個代理會“監聽”對其屬性的任何修改。當您執行this.state.myProperty = 'new value'
時,Proxy
會捕獲這個操作,并通知OWL框架:“嘿,數據變了,與這個數據相關的UI部分需要重新渲染!” - 類比: 這就好像您在后端通過ORM修改了一個記錄的字段值 (
record.name = 'New Name'
),然后刷新瀏覽器,視圖會自動顯示新的值。在OWL中,這個“刷新”過程是自動的、高效的,并且只更新變化的DOM部分。
- 創建: 狀態必須通過
import { Component, useState } from "@odoo/owl";export class Counter extends Component {static template = "my_module.CounterTemplate";setup() {// 使用 useState 創建一個響應式狀態對象this.state = useState({count: 0,log: []});}increment() {this.state.count++;this.state.log.push(`Incremented to ${this.state.count}`);// 每次修改 state 的屬性,模板都會自動更新}
}
關鍵點: 直接修改this.state
的屬性即可觸發更新。您不需要像在React中那樣調用setState
方法。
2. 屬性 (Props)
屬性 (props
) 是父組件傳遞給子組件的數據。它們是實現組件間通信和數據自上而下流動的主要方式。
- 只讀性:
props
對于子組件來說是只讀的。子組件永遠不應該直接修改它接收到的props
。這是為了保證單向數據流,使應用狀態更可預測。如果子組件需要修改數據,它應該通過觸發事件(見第六部分)來通知父組件,由父組件來修改自己的state
,然后新的state
會作為props
再次傳遞給子組件。 - 類比:
- 可以類比于后端中,一個
Many2one
字段從其關聯模型中獲取并顯示數據。表單視圖(子)顯示了來自res.partner
(父)的數據,但不能直接修改res.partner
的原始數據。 - 也可以類比于在調用一個方法時,通過
context
傳遞的參數。
- 可以類比于后端中,一個
- 只讀性:
示例:
- 父組件 (
App.js/.xml
):
- 父組件 (
// App.js
// ...
this.state = useState({userName: "John Doe",userProfile: { age: 30, city: "New York" }
});
// ...
<!-- App.xml -->
<div><!-- 將父組件的 state 作為 props 傳遞給子組件 --><UserProfilename="state.userName"profile="state.userProfile"isAdmin="true"/>
</div>
- 子組件 (
UserProfile.js/.xml
):
- 子組件 (
// UserProfile.js
export class UserProfile extends Component {static template = "my_module.UserProfileTemplate";static props = { // 推薦定義 props 的類型和結構name: { type: String },profile: { type: Object, shape: { age: Number, city: String } },isAdmin: { type: Boolean, optional: true } // 可選屬性};setup() {// 在 setup 中可以通過 this.props 訪問console.log(this.props.name); // "John Doe"}
}
<!-- UserProfile.xml -->
<div><!-- 在模板中可以直接訪問 props --><h1>Profile for <t t-esc="props.name"/></h1><p>Age: <t t-esc="props.profile.age"/></p><p>City: <t t-esc="props.profile.city"/></p><t t-if="props.isAdmin"><span class="badge bg-success">Admin</span></t>
</div>
3. 計算屬性 (Getters)
Getters允許您根據state
或props
派生出新的值,而無需將這些派生值存儲在state
中。它們是響應式的,當其依賴的state
或props
變化時,它們的值會自動重新計算。
- 后端類比: 這完全等同于Odoo模型中使用
@api.depends
的計算字段 (fields.Char(compute='_compute_full_name')
)。
- 后端類比: 這完全等同于Odoo模型中使用
示例:
import { Component, useState } from "@odoo/owl";export class UserForm extends Component {static template = "my_module.UserFormTemplate";setup() {this.state = useState({firstName: "Jane",lastName: "Doe",});}// 使用 get 關鍵字定義一個計算屬性get fullName() {// 當 state.firstName 或 state.lastName 變化時,fullName 會自動更新return `${this.state.firstName} ${this.state.lastName}`;}get canSubmit() {return this.state.firstName && this.state.lastName;}
}
<!-- UserForm.xml -->
<div><input t-model="state.firstName"/><input t-model="state.lastName"/><!-- 直接在模板中使用 getter --><p>Full Name: <t t-esc="fullName"/></p><button t-att-disabled="!canSubmit">Submit</button>
</div>
使用Getters可以使模板邏輯更清晰,并避免在state
中存儲冗余數據。
第五部分:“ORM”的調用 - 服務與RPC
- 內容摘要: 在后端,您通過ORM (
self.env[...]
) 與數據庫交互。在前端,您需要一種機制來調用后端的控制器方法。這就是“服務(Service)”和RPC(遠程過程調用)的作用。 - 后端類比:
self.env['res.partner'].search_read([...])
或調用模型方法record.action_confirm()
。 - 學習要點:
1. 服務 (Services)
服務是Odoo前端架構中的一個核心概念。它是一個可被任何組件注入和使用的單例對象,提供特定的、可復用的功能。
- 后端類比: 您可以將整個
env
對象(this.env
)類比為后端的全局環境self.env
。而env
中的每一個服務,例如rpc
服務、orm
服務、notification
服務,都類似于self.env
中的一個模型代理,如self.env['res.partner']
。它們是訪問框架核心功能的入口。 - 使用: 在OWL組件的
setup()
方法中,通過useService
鉤子來獲取一個服務的實例。
- 后端類比: 您可以將整個
import { useService } from "@web/core/utils/hooks";// ... in setup()
this.rpc = useService("rpc");
this.notification = useService("notification");
this.orm = useService("orm");
- Odoo 18+ 的變化: 在Odoo 18及更高版本中,對于像
rpc
這樣的核心服務,官方推薦直接從模塊導入函數,而不是使用useService
。這使得代碼更清晰,依賴關系更明確。
- Odoo 18+ 的變化: 在Odoo 18及更高版本中,對于像
import { rpc } from "@web/core/network/rpc";
2. 使用rpc
服務調用后端
rpc
服務是前端與后端進行通信的基石。它允許您調用任何定義了type='json'
的后端HTTP控制器方法。
API 簽名
rpc(route, params = {}, settings = {})
route
(string): 要調用的后端路由URL,例如'/my_module/my_route'
。params
(object): 一個包含要傳遞給后端方法參數的JavaScript對象。settings
(object): 可選的配置對象,例如{ silent: true }
可以在發生錯誤時不顯示默認的錯誤對話框。
調用后端控制器 (Controller)
這是最直接的RPC調用方式。
- 后端 Python (
controllers/main.py
):
- 后端 Python (
from odoo import http
from odoo.http import requestclass MyApiController(http.Controller):@http.route('/my_app/get_initial_data', type='json', auth='user')def get_initial_data(self, partner_id, include_details=False):# ... 業務邏輯 ...partner = request.env['res.partner'].browse(partner_id)data = {'name': partner.name}if include_details:data['email'] = partner.emailreturn data
- 前端 JavaScript (OWL Component):
import { rpc } from "@web/core/network/rpc";// ... in an async method
async fetchData() {try {const partnerData = await rpc('/my_app/get_initial_data', {partner_id: 123,include_details: true});this.state.partner = partnerData;} catch (e) {// 錯誤處理console.error("Failed to fetch partner data", e);}
}
調用模型方法 (ORM)
雖然您可以使用orm
服務(useService("orm")
)來更方便地調用ORM方法(如this.orm.searchRead(...)
),但理解其底層原理很重要。orm
服務本身也是通過rpc
服務調用一個通用的后端路由/web/dataset/call_kw
來實現的。直接使用rpc
調用模型方法能讓您更好地控制參數。
- Route: 固定為
/web/dataset/call_kw/{model}/{method}
或直接使用/web/dataset/call_kw
并在參數中指定。 - Params: 必須包含
model
,method
,args
, 和kwargs
。 - 后端模型方法 (Python):
- Route: 固定為
class MyModel(models.Model):_name = 'my.model'def my_custom_action(self, param1, kw_param2='default'):# self 是一個記錄集# ...return len(self)
- 前端調用:
// 示例:調用 search_read
async searchPartners() {const partners = await rpc("/web/dataset/call_kw/res.partner/search_read", {model: 'res.partner',method: 'search_read',args: [[['is_company', '=', true]], // domain['name', 'email'] // fields],kwargs: {limit: 10,order: 'name asc'}});this.state.partners = partners;
}// 示例:調用自定義模型方法
async executeCustomAction() {// 假設我們要在ID為 5 和 7 的記錄上執行方法const recordIds = [5, 7];const result = await rpc("/web/dataset/call_kw/my.model/my_custom_action", {model: 'my.model',method: 'my_custom_action',args: [recordIds, // 'self' 在后端對應這些記錄'value_for_param1'],kwargs: {kw_param2: 'custom_value'}});console.log(`Action affected ${result} records.`);
}
3. 實戰演練:加載狀態與錯誤處理
一個健壯的組件必須處理RPC調用過程中的加載狀態和潛在的錯誤。
/** @odoo-module **/
import { Component, useState, onWillStart } from "@odoo/owl";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";export class CustomerDashboard extends Component {static template = "my_module.CustomerDashboard";setup() {this.state = useState({customers: [],isLoading: true, // 1. 初始化加載狀態error: null, // 2. 初始化錯誤狀態});this.notification = useService("notification");onWillStart(async () => {await this.loadCustomers();});}async loadCustomers() {this.state.isLoading = true; // 3. RPC 調用前,設置加載中this.state.error = null;try {const data = await rpc('/web/dataset/call_kw/res.partner/search_read', {model: 'res.partner',method: 'search_read',args: [[['customer_rank', '>', 0]], ['name', 'email']],kwargs: { limit: 5 }});this.state.customers = data;} catch (e) {// 4. 捕獲錯誤console.error("Error loading customers:", e);// Odoo 的 UserError/ValidationError 通常包含在 e.message.data 中const errorMessage = e.message?.data?.message || "An unknown error occurred.";this.state.error = errorMessage;this.notification.add(errorMessage, { type: 'danger' });} finally {// 5. 無論成功或失敗,最后都結束加載狀態this.state.isLoading = false;}}
}
對應的QWeb模板 (my_module.CustomerDashboard.xml
):
<templates><t t-name="my_module.CustomerDashboard"><div><button t-on-click="loadCustomers" t-att-disabled="state.isLoading">Reload</button><t t-if="state.isLoading"><div class="fa fa-spinner fa-spin"/> Loading...</t><t t-elif="state.error"><div class="alert alert-danger" t-esc="state.error"/></t><t t-else=""><ul><t t-foreach="state.customers" t-as="customer" t-key="customer.id"><li><t t-esc="customer.name"/> (<t t-esc="customer.email"/>)</li></t></ul></t></div></t>
</templates>
這個完整的模式展示了如何在組件啟動時 (onWillStart
) 通過RPC獲取數據,并管理加載中、錯誤和成功三種UI狀態。
第六部分:架構的對比 - 組件組合 vs 模型繼承
- 內容摘要: 后端通過模型繼承 (
_inherit
) 來擴展功能。前端的主流思想是“組合優于繼承”。本部分將教您如何通過組合小型、獨立的組件來構建復雜的用戶界面。 - 后端類比: 使用
_inherit
擴展模型字段和方法,以及使用One2many
和Many2many
字段組織數據關系。 - 學習要點:
在Odoo后端,當您想給res.partner
模型增加一個字段或修改一個方法時,您會使用_inherit = 'res.partner'
。這種繼承模式非常強大,但也可能導致類變得龐大和復雜。
在現代前端開發中,更推崇組合模式:將UI拆分成一系列獨立的、可復用的組件,然后像搭積木一樣將它們組合起來構建更復雜的界面。
1. 父子組件通信
有效的組件間通信是組合模式的核心。
父 -> 子: 通過Props
傳遞數據
這在第四部分已經詳細介紹過。父組件通過屬性(props)將數據和配置單向地傳遞給子組件。這是最常見和最直接的通信方式。
子 -> 父: 通過自定義事件 (this.trigger
)
當子組件需要通知父組件某件事發生了(例如用戶點擊了按鈕、輸入了數據),或者需要請求父組件執行一個操作時,它應該觸發一個自定義事件。
- 后端類比: 這非常類似于在一個向導(Wizard)中點擊一個按鈕,然后返回一個
ir.actions.act_window
類型的字典來關閉向導并刷新主視圖。子組件(向導)不直接操作主視圖,而是通過一個標準化的“動作”或“事件”來通知框架,由框架或父級(主視圖)來響應這個動作。
- 后端類比: 這非常類似于在一個向導(Wizard)中點擊一個按鈕,然后返回一個
工作流程:
- 子組件 (
SearchBar.js
): 使用this.trigger()
觸發一個帶有名稱和數據負載(payload)的事件。
- 子組件 (
export class SearchBar extends Component {static template = "my_module.SearchBar";setup() {this.state = useState({ query: "" });}onSearchClick() {// 觸發一個名為 'search-requested' 的事件// 將當前查詢作為 payload 傳遞出去this.trigger('search-requested', {query: this.state.query});}
}
<!-- SearchBar.xml -->
<div><input type="text" t-model="state.query" placeholder="Search..."/><button t-on-click="onSearchClick">Search</button>
</div>
- 父組件 (
ProductList.js/.xml
): 在模板中使用t-on-<event-name>
來監聽子組件的事件,并將其綁定到一個處理方法上。
- 父組件 (
<!-- ProductList.xml -->
<div><!-- 監聽 SearchBar 組件的 'search-requested' 事件 --><!-- 當事件觸發時,調用父組件的 handleSearch 方法 --><SearchBar t-on-search-requested="handleSearch"/><!-- ... 顯示產品列表 ... --><ul><t t-foreach="state.products" t-as="product" t-key="product.id"><li><t t-esc="product.name"/></li></t></ul>
</div>
// ProductList.js
export class ProductList extends Component {static template = "my_module.ProductList";setup() {this.state = useState({ products: [] });this.orm = useService("orm");// ...}// 這個方法會接收到子組件傳遞的 payloadasync handleSearch(ev) {const payload = ev.detail; // 事件的 payload 存儲在 event.detail 中const searchQuery = payload.query;const domain = searchQuery ? [['name', 'ilike', searchQuery]] : [];const products = await this.orm.searchRead('product.product', domain, ['name']);this.state.products = products;}
}
通過這種模式,SearchBar
組件變得完全獨立和可復用。它不關心搜索邏輯如何實現,只負責收集用戶輸入并發出通知。父組件ProductList
則負責響應這個通知,執行具體的業務邏輯(RPC調用),并更新自己的狀態。
2. 構建可復用組件:思想的轉變
- 從繼承到組合:
- 繼承思維 (后端): “我需要一個類似
res.partner
的東西,但要加點功能。” ->class NewPartner(models.Model): _inherit = 'res.partner'
- 組合思維 (前端): “我需要一個顯示產品列表的頁面,這個頁面需要一個搜索功能和一個篩選功能。” -> 構建一個獨立的
<SearchBar>
組件和一個獨立的<FilterPanel>
組件,然后在<ProductPage>
組件中將它們組合起來。
- 繼承思維 (后端): “我需要一個類似
- 單一職責原則: 每個組件應該只做好一件事。
<SearchBar>
只管搜索,<ProductList>
只管展示列表,<ProductPage>
只管協調它們。這使得代碼更容易理解、測試和維護。 - 事件修飾符: OWL還提供了控制事件傳播的修飾符,這在復雜的嵌套組件中非常有用。
.stop
: 阻止事件冒泡到更高層的組件。t-on-click.stop="myMethod"
。.prevent
: 阻止事件的默認瀏覽器行為,例如阻止表單提交時的頁面刷新。t-on-submit.prevent="myMethod"
。.self
: 僅當事件直接在該元素上觸發時才調用方法,忽略來自子元素的冒泡事件。
- 從繼承到組合:
第七部分:高級主題與生態系統
- 內容摘要: 掌握了基礎之后,本部分將帶您了解OWL的高級特性和它在Odoo生態中的位置,類比于您在后端可能接觸到的高級緩存、注冊表機制和部署知識。
- 后端類比: Odoo注冊表 (
odoo.registry
)、服務端動作 (ir.actions.server
)、資源打包與部署。 - 學習要點:
1. 全局狀態管理 (useStore
)
當多個不直接相關的組件需要共享和響應同一份數據時(例如,購物車狀態、用戶偏好設置),通過props
層層傳遞會變得非常繁瑣(稱為"prop drilling")。這時,就需要一個全局的狀態管理方案。
- 后端類比:
useStore
可以類比于后端的request.session
或一個全局共享的context
字典。它是一個所有組件都可以訪問和修改的中央數據源。 useState
vsuseStore
:useState
: 用于管理組件本地的狀態。數據歸組件所有,只能通過props
向下傳遞。useStore
: 用于管理跨組件共享的全局或應用級狀態。
- 工作流程:
- 創建 Store: 定義一個全局的響應式
store
。這通常在一個單獨的文件中完成。
- 創建 Store: 定義一個全局的響應式
- 后端類比:
// /my_module/static/src/store.js
import { reactive } from "@odoo/owl";export const cartStore = reactive({items: [],addItem(product) {this.items.push(product);},get totalItems() {return this.items.length;}
});
- 在根組件中提供 Store: 將
store
添加到應用的env
中。
- 在根組件中提供 Store: 將
// 在應用啟動的地方
const env = { ... };
env.cart = cartStore;
myApp.mount(target, { env });
- 在組件中使用
useStore
:useStore
鉤子訂閱store
的一部分,當這部分數據變化時,只有訂閱了它的組件會重新渲染。
- 在組件中使用
import { useStore } from "@odoo/owl";
import { cartStore } from "/my_module/static/src/store.js";// 在一個組件的 setup() 中
// 這里的 selector 函數 (s) => s.totalItems 告訴 useStore
// 這個組件只關心 totalItems 的變化。
this.cart = useStore((s) => s.totalItems);// 在另一個組件中
this.cartItems = useStore((s) => s.items);// 在模板中
// <span>Cart: <t t-esc="cart"/> items</span>
- 設計模式: 為了避免單一巨大的全局
store
,最佳實踐是按功能模塊劃分store
。例如,一個cartStore
,一個userPreferenceStore
等。
- 設計模式: 為了避免單一巨大的全局
2. Odoo前端注冊表 (Registry)
這是前端與后端odoo.registry
最直接的類比。前端注冊表是Odoo框架發現、加載和組織所有前端代碼(組件、服務、動作等)的核心機制。它是一個全局的、按類別劃分的鍵值對集合。
- 核心注冊表類別:
components
: 注冊通用的OWL組件。public_components
(Odoo 17+): 專門用于注冊在網站/門戶頁面上通過<owl-component>
標簽使用的組件。services
: 注冊服務,如rpc
,notification
等。actions
: 注冊客戶端動作(ir.actions.client
)。當用戶點擊一個菜單項觸發一個tag
為my_custom_action
的客戶端動作時,框架會在此注冊表中查找同名的鍵,并加載其對應的OWL組件。fields
: 注冊字段微件(Field Widgets)。systray
: 注冊系統托盤項。
- 注冊方法:
- 核心注冊表類別:
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { MyAwesomeComponent } from "./my_awesome_component";
import { myService } from "./my_service";// 獲取 'actions' 類別,并添加一個新條目
registry.category("actions").add("my_app.my_client_action_tag", MyAwesomeComponent);// 注冊一個服務
registry.category("services").add("myServiceName", myService);// 注冊一個字段微件
registry.category("fields").add("my_special_widget", MyAwesomeComponent);
- 與
__manifest__.py
的關聯: 您的JS文件本身不會被Odoo自動發現。您必須在模塊的__manifest__.py
文件的assets
字典中聲明它。
- 與
'assets': {'web.assets_backend': ['my_module/static/src/js/my_awesome_component.js','my_module/static/src/xml/my_awesome_component.xml','my_module/static/src/js/my_service.js',],
},
當Odoo加載web.assets_backend
資源包時,它會包含并執行這些JS文件。文件中的registry.add(...)
代碼隨之執行,從而將您的組件和服務“注冊”到框架中,使其在需要時可以被調用。
3. 與舊框架(Widget)的互操作性
在實際項目中,您不可避免地會遇到舊的、基于AbstractWidget
的框架代碼。
- 在舊視圖中使用OWL組件: 這是最常見和最受支持的方式。Odoo 16+中,字段微件(Field Widgets)本身已經完全是OWL組件。您可以創建一個OWL組件,將其注冊到
fields
注冊表中,然后在舊的XML表單或列表視圖中通過widget="my_owl_widget_name"
來使用它。 - 在OWL組件中使用舊Widget: 這是一種應該極力避免的反模式。它違背了OWL的聲明式和響應式原則。如果必須這樣做,您可能需要在OWL組件的
onMounted
鉤子中,手動獲取一個DOM元素作為掛載點,然后用JavaScript實例化并啟動舊的Widget。這將導致您需要手動管理舊Widget的生命周期和通信,非常復雜且容易出錯。正確的做法是逐步將舊Widget的功能重構為新的OWL組件。 - 通信橋梁: 如果OWL組件和舊Widget必須共存并通信,最佳方案是創建一個共享的Odoo服務。舊Widget和新OWL組件都可以訪問這個服務,通過調用服務的方法或監聽服務上的事件來進行通信,從而實現解耦。
- 在舊視圖中使用OWL組件: 這是最常見和最受支持的方式。Odoo 16+中,字段微件(Field Widgets)本身已經完全是OWL組件。您可以創建一個OWL組件,將其注冊到
4. 前端資源打包與優化 (Asset Bundles)
這與您在__manifest__.py
中定義assets
直接相關。
- 開發模式 (
?debug=assets
): Odoo會按文件逐個加載JS和CSS,不進行壓縮。這便于調試。 - 生產模式 (默認): Odoo會將一個資源包(如
web.assets_backend
)中的所有JS文件和所有CSS文件分別合并成一個大的JS文件和一個大的CSS文件,并對它們進行壓縮(minification)。這大大減少了HTTP請求的數量和資源體積,加快了生產環境的加載速度。
- 開發模式 (
理解這一點有助于您排查問題:如果您的組件在開發模式下工作正常,但在生產模式下失效,通常是由于您的JS/XML文件沒有被正確地添加到assets
定義中,導致在打包時被遺漏。