提到 Odoo 前端跳转,很多人第一反应就是一句:
this.actionService.doAction(...)
这当然没错,但只说到这一步,其实什么都还没解释。
因为 doAction() 在 Odoo 里根本不是“执行一个统一页面跳转”的单函数,而更像一个动作分发器。看 addons/web/static/src/webclient/actions/action_service.js 就会发现,它真正做的是:
- 先加载 action;
- 再按
action.type分流执行; - 不同类型进入完全不同的 UI 装配路径;
- 其中
ir.actions.client又是一套单独的 registry 与 controller 生成逻辑。
所以这篇文章想讲透的不是 router 状态,而是:
Odoo 为什么要把“执行动作”设计成 doAction 分流,而不是单一路由跳转。
一、doAction() 的第一身份是 dispatcher,不是 navigator
源码里 doAction(actionRequest, options) 的关键结构非常直白:
_loadAction()先把 request 解析成完整 action;_preprocessAction()做预处理;- 然后按
action.type进入不同分支: ir.actions.act_urlir.actions.act_windowir.actions.act_window_closeir.actions.clientir.actions.serverir.actions.report
这已经说明一个事实:
Odoo 眼里的 action 不是“页面地址”,而是“可执行的前端/后端动作描述”。
也正因为如此,doAction() 本质上更接近 command dispatcher,而不是 Vue Router / React Router 那类纯路径导航器。
二、为什么 act_window 和 client action 必须分开看
很多文章会把它们都归为“前端跳转”,但在 Odoo 内部,这两类 action 的装配过程差异非常大。
act_window 更像“围绕模型视图建立 controller”
它通常要处理:
- 视图列表;
- 当前 viewType;
- model / resId / domain / context;
- 是否需要多记录视图;
- 是否要切换现有 controller。
client action 更像“按 tag 从 registry 取前端入口组件”
在 _executeClientAction() 里,官方会:
- 用
action.tag去actionRegistry取 client action; - 继承 registry 项上的
path或target; - 若它是一个 OWL
Component,就执行extractProps?.(action); - 用
_makeController()与_getActionInfo()包成 controller; - 最后
_updateUI(controller, options)。
这就很关键。
它说明 client action 不是“后端视图的一种别名”,而是通过 registry 挂进 Web Client 的前端动作入口。
三、extractProps 暴露了 client action 的真正边界
_executeClientAction() 里有一行非常值得注意:
const props = clientAction.extractProps?.(action) || {}
很多人会忽略它,但它其实和字段系统里的 extractProps 是同一种哲学:
action 描述不是组件 props,必须先翻译一层。
这说明一个 client action 的设计边界很清楚:
- 后端 action 负责声明“要执行什么”;
- registry 项负责把 action 描述翻译成组件所需 props;
- controller 与 UI 系统负责把它接进当前 Web Client。
所以写 client action 时,如果一上来就把业务逻辑塞进全局 service 或外层页面,而不尊重这层翻译边界,后面就很难维护。
四、clearUncommittedChanges() 说明跳转不是纯渲染行为
无论是 act_window 还是很多 client action 路径,Odoo 都会在合适时机调用 clearUncommittedChanges()。
这一步非常重要,因为它揭示了 action service 的真实职责:
- 不只是生成下一个页面;
- 还要协调当前页面是否允许离开。
也就是说,doAction 不是“立刻换屏”,而是带有工作流责任的导航入口。
这和桌面 ERP 的体验很一致:
- 你当前编辑了东西;
- 跳走前要么保存、要么确认、要么阻止离开;
- 导航必须服从当前控制器状态。
五、为什么 client action 适合承接“非传统视图页面”
一旦理解 _executeClientAction() 的结构,就会明白 client action 特别适合什么:
- 仪表盘;
- 报表容器;
- 配置向导;
- 完全前端驱动的交互页面;
- 某些不以标准 form/list/kanban 为核心的入口。
因为它不是从 view arch 编译出来,而是直接以 registry 组件作为运行入口。
但这也意味着它要自己更明确地处理:
- props 结构;
- target 行为;
- 与 breadcrumbs / title / action stack 的接入方式。
六、开发时最容易犯的两个误区
误区一:把所有跳转都理解成 router 改 URL
其实 router 只是状态承载层之一。真正决定怎么执行的是 action service 对 action type 的分流。
误区二:把 client action 当“特殊页面组件”,不当成动作契约
如果只把 client action 看成“某个组件直接打开”,就容易漏掉:
- registry 注册;
extractProps;target约束;- UI 更新与 controller 包装;
- 离开保护。
这样写出来的东西通常一开始能跑,后面很快和 Odoo Web Client 的整体导航契约脱节。
结语
doAction() 真正厉害的地方,不是把所有跳转统一成一个 API,而是把不同动作类型放进统一入口、分流执行:
act_window负责标准视图工作流;client action负责 registry 驱动的前端入口;- 其他 action 再走各自执行器。
所以更准确地说:
Odoo 前端导航不是“调用 doAction 结束”,而是“从 doAction 开始,进入一套按动作语义分流的 UI 执行契约”。
这也是为什么 Odoo 的 action system 比普通单页路由更像一套工作流引擎。
DISCUSSION
评论区