1. 业务功能
解决的问题
在Odoo 19之前,前端组件主要使用**类组件(Class Components)**开发,存在三大痛点:
- 代码重复:相同逻辑(如错误处理)在多个组件中重复编写
- 逻辑分散 :相关功能代码被生命周期方法切割(如
onMounted和onPatched中都有尺寸计算) - 复用困难:业务逻辑难以像积木一样在不同组件间共享
Hooks的出现就像给乐高积木添加了连接点,让开发者能:
- 将状态逻辑 与UI渲染分离
- 像搭积木一样组合功能
- 减少50%以上的重复代码
适用场景
- POS收银系统 :支付防重复、网络错误处理(如提供的
hooks.js示例) - 表单验证:统一处理必填项、格式校验
- 数据加载:跟踪加载状态(加载中/成功/失败)
- 屏幕适配:自动检测元素是否超出容器
💡 业务价值:组件开发效率提升40%,维护成本降低60%
2. 核心概念解析
2.1 什么是Hooks?
Hooks是特殊的函数 ,它们让你"钩入"Odoo组件的状态 和生命周期,而无需创建类组件。
类比理解
|----------|------------|-------------|
| 概念 | 传统类组件 | Hooks组件 |
| 房子结构 | 整体浇筑的混凝土房屋 | 预制模块化房屋 |
| 功能添加 | 需要敲墙改造 | 直接插拔功能模块 |
| 代码组织 | 按生命周期划分 | 按业务逻辑划分 |
✅ 关键特征 :
必须以use开头(如useState、useRef)
只能在组件顶层 调用(不能在条件语句中)
只能用于OWL框架的函数组件
2.2 Hooks设计原则
- 逻辑复用优先
不是为复用UI,而是复用状态逻辑(如错误处理、防重复提交)
- 关注点分离
将组件拆分为更小的功能单元:
// 传统写法:所有逻辑混在一起
class OrderScreen {
onMounted() { /* 尺寸计算 + 错误处理 + 自动聚焦 */ }
onPatched() { /* 同上 */ }
}
// Hooks写法:逻辑清晰分离
function OrderScreen() {
useIsChildLarger(container); // 专注尺寸
useErrorHandlers(); // 专注错误
useAutoFocusToLast(); // 专注输入
}
- 无破坏性变更
与旧版class组件完全兼容,可逐步迁移
3. 核心Hooks详解
3.1 Odoo基础Hooks
|------------------|---------------|------------------|--------------------------------------------|
| Hook | 作用 | 业务场景 | 示例 |
| useState | 管理组件状态 | 跟踪订单状态(加载/成功/失败) | const state = useState({status: "idle"}) |
| useRef | 保存可变值(不触发重渲染) | 引用DOM元素(如输入框) | const input = useRef("quantity") |
| useComponent | 获取当前组件实例 | 在自定义Hook中访问组件 | const comp = useComponent() |
| onMounted | 组件挂载后执行 | 初始化数据加载 | onMounted(() => fetchData()) |
| onPatched | DOM更新后执行 | 调整元素尺寸 | onPatched(resizeElements) |
3.2 自定义Hooks开发指南
自定义Hooks是业务逻辑的封装,必须遵循:
开发步骤
- 确定功能边界
一个Hook只做一件事(如useErrorHandlers只处理错误)
- 提取公共逻辑
将重复代码移到Hook中:
// 传统写法:每个组件重复编写
onMounted(() => {
window.addEventListener("resize", computeSize);
});
onPatched(computeSize);
// Hooks写法:封装为useIsChildLarger
useIsChildLarger(container);
- 返回必要接口
只暴露需要的属性/方法:
// 返回可读状态和控制方法
return {
get isLarger() { return state.isLarger; },
reload: () => computeSize()
};
代码结构模板
/**
* @description 用一句话说明Hook用途
* @param {Object} 参数说明
* @returns {Object} 返回值说明
*/
export function useYourCustomHook(param) {
// 1. 获取基础Hook
const component = useComponent();
const env = useEnv();
// 2. 定义状态
const state = useState({ /* 初始状态 */ });
// 3. 设置副作用
onMounted(() => { /* 初始化 */ });
onPatched(() => { /* DOM更新后 */ });
// 4. 定义方法
const yourMethod = () => { /* 业务逻辑 */ };
// 5. 返回API
return {
get status() { return state.status; },
yourMethod
};
}
4. 实战案例分析
4.1 错误处理Hook深度解析
export function useErrorHandlers() {
// 步骤1: 获取必要服务
const component = useComponent();
const dialog = useEnv().services.dialog; // 对话框服务
// 步骤2: 注入错误处理方法到组件
component._handlePushOrderError = async function (error) {
// 业务场景1: 需要后台开票
if (error.message === "Backend Invoice") {
dialog.add(ConfirmationDialog, {
title: _t("请从后台打印发票"),
body: _t("订单已同步,请在后台开票:") + error.data.order.name
});
}
// 业务场景2: 网络中断(最常见)
else if (error.code < 0) {
dialog.add(ConfirmationDialog, {
title: _t("无法同步订单"),
body: _t("请检查网络,点击右上角红色WiFi按钮重试")
});
}
// ...其他场景
};
}
教学要点:
- 服务获取 :通过
useEnv()获取全局服务(如对话框) - 方法注入 :将处理函数挂载到组件实例(
component._xxx) - 场景分类:按业务场景而非技术错误码分类
- 用户语言 :所有提示用
_t()包裹,避免技术术语
💡 为什么这样设计 ?
收银员不是IT人员!提示必须像"请检查打印机电源"而不是"Error 500"
4.2 防重复提交Hook原理
export function useAsyncLockedMethod(method) {
let called = false; // 🔒 核心:锁状态
return async (...args) => {
if (called) { // 1. 检查是否已锁定
return; // 已锁定则直接返回
}
try {
called = true; // 2. 锁定
return await method.call(component, ...args); // 3. 执行
} finally {
called = false; // 4. 无论成功失败都解锁
}
};
}
在POS支付中的应用:
// 组件中使用
this.pay = useAsyncLockedMethod(async () => {
await this.rpc("/pos/payment"); // 调用支付API
this.showReceipt(); // 显示小票
});
// 模板中绑定
<button t-on-click="pay">支付</button>
执行流程:
- 顾客点击"支付"按钮 → 触发
pay() called设为true(上锁)- 发送支付请求
- 同时 顾客再次点击 →
called为true直接返回 - 支付完成 →
called设为false(解锁)
⚠️ 关键安全设计 :
finally确保即使支付失败也能解锁,避免"永久锁定"导致无法再次支付
5. 最佳实践与常见错误
5.1 Hooks使用黄金法则
- 只在顶层调用
✘ 错误:if (condition) { useState() }
✔ 正确:始终在组件函数最外层调用
- 命名规范
- 自定义Hook必须以
use开头(如usePayment) - 返回对象的getter方法用
get status()
- 自定义Hook必须以
- 状态最小化
只存储必要的状态,避免过度使用useState
5.2 新手常见陷阱
|------------|---------------|--------------------------|
| 问题 | 现象 | 解决方案 |
| 重复执行 | 网络请求发送多次 | 使用useAsyncLockedMethod |
| 状态不同步 | UI未更新 | 确保修改的是useState返回的状态 |
| 内存泄漏 | 页面关闭后仍执行回调 | 在onWillUnmount中清理定时器 |
| DOM未就绪 | ref.el为null | 在onMounted后使用 |
5.3 性能优化技巧
- 防抖处理
对频繁触发的操作(如搜索)添加防抖:
const search = useDebounced((query) => {
this.rpc("/search", {query});
}, 300); // 300ms内只执行最后一次
- 避免重复渲染
对复杂计算使用useMemo:
const total = useMemo(() => {
return order.lines.reduce((sum, line) => sum + line.price, 0);
}, [order.lines]);
- 按需加载
对非关键功能延迟加载:
const { showAnalytics } = useTrackedAsync(() =>
import("./analytics").then(mod => mod.show())
);
5.4 迁移路线图
|--------|----------|----------------|
| 步骤 | 操作 | 建议 |
| 1 | 识别重复逻辑 | 找出3个以上组件共有的代码 |
| 2 | 创建基础Hook | 从简单功能开始(如尺寸检测) |
| 3 | 逐步替换 | 先在新组件使用,再重构旧组件 |
| 4 | 建立规范 | 制定团队Hook开发标准 |
💡 教学总结 :
Odoo 19 Hooks不是技术升级,而是开发思维的转变 :
从"组件是什么" → "组件能做什么"
从"如何实现" → "如何复用"
掌握Hooks,你将像搭积木一样高效构建Odoo应用!